@aria_asi/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/bin/aria.js +168 -0
  2. package/dist/aria-connector/src/auth-commands.d.ts +28 -0
  3. package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
  4. package/dist/aria-connector/src/auth-commands.js +129 -0
  5. package/dist/aria-connector/src/auth-commands.js.map +1 -0
  6. package/dist/aria-connector/src/auth.d.ts +12 -0
  7. package/dist/aria-connector/src/auth.d.ts.map +1 -0
  8. package/dist/aria-connector/src/auth.js +31 -0
  9. package/dist/aria-connector/src/auth.js.map +1 -0
  10. package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
  11. package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
  12. package/dist/aria-connector/src/auto-mcp.js +994 -0
  13. package/dist/aria-connector/src/auto-mcp.js.map +1 -0
  14. package/dist/aria-connector/src/chat.d.ts +21 -0
  15. package/dist/aria-connector/src/chat.d.ts.map +1 -0
  16. package/dist/aria-connector/src/chat.js +332 -0
  17. package/dist/aria-connector/src/chat.js.map +1 -0
  18. package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
  19. package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
  20. package/dist/aria-connector/src/codebase-scanner.js +6 -0
  21. package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
  22. package/dist/aria-connector/src/cognition-log.d.ts +17 -0
  23. package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
  24. package/dist/aria-connector/src/cognition-log.js +19 -0
  25. package/dist/aria-connector/src/cognition-log.js.map +1 -0
  26. package/dist/aria-connector/src/config.d.ts +41 -0
  27. package/dist/aria-connector/src/config.d.ts.map +1 -0
  28. package/dist/aria-connector/src/config.js +50 -0
  29. package/dist/aria-connector/src/config.js.map +1 -0
  30. package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
  31. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
  32. package/dist/aria-connector/src/connectors/claude-code.js +204 -0
  33. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
  34. package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
  35. package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
  36. package/dist/aria-connector/src/connectors/cursor.js +63 -0
  37. package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
  38. package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
  39. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
  40. package/dist/aria-connector/src/connectors/opencode.js +102 -0
  41. package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
  42. package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
  43. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
  44. package/dist/aria-connector/src/connectors/shell.js +58 -0
  45. package/dist/aria-connector/src/connectors/shell.js.map +1 -0
  46. package/dist/aria-connector/src/garden-client.d.ts +19 -0
  47. package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
  48. package/dist/aria-connector/src/garden-client.js +85 -0
  49. package/dist/aria-connector/src/garden-client.js.map +1 -0
  50. package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
  51. package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
  52. package/dist/aria-connector/src/garden-control-plane.js +43 -0
  53. package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
  54. package/dist/aria-connector/src/harness-client.d.ts +166 -0
  55. package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
  56. package/dist/aria-connector/src/harness-client.js +344 -0
  57. package/dist/aria-connector/src/harness-client.js.map +1 -0
  58. package/dist/aria-connector/src/hive-client.d.ts +32 -0
  59. package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
  60. package/dist/aria-connector/src/hive-client.js +69 -0
  61. package/dist/aria-connector/src/hive-client.js.map +1 -0
  62. package/dist/aria-connector/src/index.d.ts +19 -0
  63. package/dist/aria-connector/src/index.d.ts.map +1 -0
  64. package/dist/aria-connector/src/index.js +13 -0
  65. package/dist/aria-connector/src/index.js.map +1 -0
  66. package/dist/aria-connector/src/install-hooks.d.ts +18 -0
  67. package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
  68. package/dist/aria-connector/src/install-hooks.js +224 -0
  69. package/dist/aria-connector/src/install-hooks.js.map +1 -0
  70. package/dist/aria-connector/src/model-context.d.ts +8 -0
  71. package/dist/aria-connector/src/model-context.d.ts.map +1 -0
  72. package/dist/aria-connector/src/model-context.js +83 -0
  73. package/dist/aria-connector/src/model-context.js.map +1 -0
  74. package/dist/aria-connector/src/persona.d.ts +27 -0
  75. package/dist/aria-connector/src/persona.d.ts.map +1 -0
  76. package/dist/aria-connector/src/persona.js +86 -0
  77. package/dist/aria-connector/src/persona.js.map +1 -0
  78. package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
  79. package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
  80. package/dist/aria-connector/src/providers/anthropic.js +92 -0
  81. package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
  82. package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
  83. package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
  84. package/dist/aria-connector/src/providers/deepseek.js +28 -0
  85. package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
  86. package/dist/aria-connector/src/providers/google.d.ts +3 -0
  87. package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
  88. package/dist/aria-connector/src/providers/google.js +38 -0
  89. package/dist/aria-connector/src/providers/google.js.map +1 -0
  90. package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
  91. package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
  92. package/dist/aria-connector/src/providers/ollama.js +28 -0
  93. package/dist/aria-connector/src/providers/ollama.js.map +1 -0
  94. package/dist/aria-connector/src/providers/openai.d.ts +4 -0
  95. package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
  96. package/dist/aria-connector/src/providers/openai.js +84 -0
  97. package/dist/aria-connector/src/providers/openai.js.map +1 -0
  98. package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
  99. package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
  100. package/dist/aria-connector/src/providers/openrouter.js +30 -0
  101. package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
  102. package/dist/aria-connector/src/providers/types.d.ts +20 -0
  103. package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
  104. package/dist/aria-connector/src/providers/types.js +2 -0
  105. package/dist/aria-connector/src/providers/types.js.map +1 -0
  106. package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
  107. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
  108. package/dist/aria-connector/src/setup-wizard.js +140 -0
  109. package/dist/aria-connector/src/setup-wizard.js.map +1 -0
  110. package/dist/aria-connector/src/types.d.ts +30 -0
  111. package/dist/aria-connector/src/types.d.ts.map +1 -0
  112. package/dist/aria-connector/src/types.js +5 -0
  113. package/dist/aria-connector/src/types.js.map +1 -0
  114. package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
  115. package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
  116. package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
  117. package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
  118. package/dist/cli-0.2.0.tgz +0 -0
  119. package/dist/install.sh +13 -0
  120. package/hooks/aria-harness-via-sdk.mjs +317 -0
  121. package/hooks/aria-pre-tool-gate.mjs +596 -0
  122. package/hooks/aria-preprompt-consult.mjs +175 -0
  123. package/hooks/aria-stop-gate.mjs +222 -0
  124. package/package.json +47 -0
  125. package/src/__tests__/auth-commands.test.ts +132 -0
  126. package/src/auth-commands.ts +175 -0
  127. package/src/auth.ts +33 -0
  128. package/src/auto-mcp.ts +1172 -0
  129. package/src/chat.ts +387 -0
  130. package/src/codebase-scanner.ts +18 -0
  131. package/src/cognition-log.ts +30 -0
  132. package/src/config.ts +94 -0
  133. package/src/connectors/claude-code.ts +213 -0
  134. package/src/connectors/cursor.ts +75 -0
  135. package/src/connectors/opencode.ts +115 -0
  136. package/src/connectors/shell.ts +72 -0
  137. package/src/garden-client.ts +98 -0
  138. package/src/garden-control-plane.ts +108 -0
  139. package/src/harness-client.ts +454 -0
  140. package/src/hive-client.ts +104 -0
  141. package/src/index.ts +26 -0
  142. package/src/install-hooks.ts +259 -0
  143. package/src/model-context.ts +88 -0
  144. package/src/persona.ts +113 -0
  145. package/src/providers/anthropic.ts +120 -0
  146. package/src/providers/deepseek.ts +40 -0
  147. package/src/providers/google.ts +57 -0
  148. package/src/providers/ollama.ts +43 -0
  149. package/src/providers/openai.ts +108 -0
  150. package/src/providers/openrouter.ts +42 -0
  151. package/src/providers/types.ts +35 -0
  152. package/src/setup-wizard.ts +177 -0
  153. package/src/types.ts +32 -0
@@ -0,0 +1,1730 @@
1
+ /**
2
+ * Auto codebase discovery scanner — produces a compressed "schema image"
3
+ * that eliminates cold starts for any LLM.
4
+ *
5
+ * Scans a directory and detects language, framework, package manager, database,
6
+ * ORM, test framework, entry points, project structure, dependencies, and
7
+ * architecture patterns. Outputs a SchemaImage object and a human-readable
8
+ * compressed text block.
9
+ *
10
+ * Zero external dependencies — uses only Node.js built-ins (fs, path, crypto,
11
+ * child_process, events). Optional chokidar support for file watching.
12
+ *
13
+ * @module codebase-scanner
14
+ */
15
+ import { createHash } from 'crypto';
16
+ import { execSync } from 'child_process';
17
+ import { promises as fsp, watch as fsWatch } from 'fs';
18
+ import * as path from 'path';
19
+ // ---------------------------------------------------------------------------
20
+ // Constants — detection maps
21
+ // ---------------------------------------------------------------------------
22
+ const IGNORE_DIRS = new Set([
23
+ 'node_modules',
24
+ '.git',
25
+ '.svn',
26
+ '.hg',
27
+ '.next',
28
+ '.nuxt',
29
+ 'dist',
30
+ 'build',
31
+ 'out',
32
+ 'target',
33
+ '__pycache__',
34
+ '.tox',
35
+ '.venv',
36
+ 'venv',
37
+ '.env',
38
+ 'vendor',
39
+ '.turbo',
40
+ '.cache',
41
+ 'coverage',
42
+ '.nyc_output',
43
+ '.sass-cache',
44
+ 'tmp',
45
+ 'temp',
46
+ '.idea',
47
+ '.vscode',
48
+ '.DS_Store',
49
+ ]);
50
+ const MAX_FILE_SIZE = 1_048_576; // 1 MB
51
+ const MAX_SCAN_DEPTH = 4;
52
+ const CACHE_TTL_MS = 30_000;
53
+ const LANG_MAP = {
54
+ typescript: {
55
+ configFile: 'tsconfig.json',
56
+ extPriority: 'ts,tsx',
57
+ // version read from package.json devDependencies in detectLanguageAndPackageManager
58
+ },
59
+ javascript: {
60
+ configFile: 'package.json',
61
+ extPriority: 'js,jsx,mjs,cjs',
62
+ },
63
+ python: {
64
+ configFile: 'pyproject.toml',
65
+ extPriority: 'py',
66
+ versionFile: '.python-version',
67
+ versionFromConfig: () => '',
68
+ },
69
+ rust: {
70
+ configFile: 'Cargo.toml',
71
+ extPriority: 'rs',
72
+ versionFromConfig: (raw) => {
73
+ const m = raw.match(/edition\s*=\s*"(\d+)"/);
74
+ return m ? `edition ${m[1]}` : '';
75
+ },
76
+ },
77
+ go: {
78
+ configFile: 'go.mod',
79
+ extPriority: 'go',
80
+ versionFromConfig: (raw) => {
81
+ const m = raw.match(/^go\s+(\S+)/m);
82
+ return m ? `Go ${m[1]}` : '';
83
+ },
84
+ },
85
+ ruby: {
86
+ configFile: 'Gemfile',
87
+ extPriority: 'rb',
88
+ versionFile: '.ruby-version',
89
+ versionFromConfig: () => '',
90
+ },
91
+ java: {
92
+ configFile: 'pom.xml',
93
+ extPriority: 'java,kt',
94
+ versionFromConfig: () => '',
95
+ },
96
+ kotlin: {
97
+ configFile: 'build.gradle.kts',
98
+ extPriority: 'kt,kts',
99
+ },
100
+ cpp: {
101
+ configFile: 'CMakeLists.txt',
102
+ extPriority: 'cpp,cxx,h,hpp,cc',
103
+ },
104
+ csharp: {
105
+ configFile: '*.csproj',
106
+ extPriority: 'cs',
107
+ },
108
+ swift: {
109
+ configFile: 'Package.swift',
110
+ extPriority: 'swift',
111
+ },
112
+ php: {
113
+ configFile: 'composer.json',
114
+ extPriority: 'php',
115
+ },
116
+ };
117
+ // framework detection: dependency name in package.json → framework label
118
+ const JS_FRAMEWORK_MAP = {
119
+ next: 'Next.js',
120
+ nuxt: 'Nuxt',
121
+ 'react-scripts': 'Create React App',
122
+ gatsby: 'Gatsby',
123
+ svelte: 'SvelteKit',
124
+ '@sveltejs/kit': 'SvelteKit',
125
+ '@remix-run/react': 'Remix',
126
+ '@remix-run/serve': 'Remix',
127
+ '@angular/core': 'Angular',
128
+ astro: 'Astro',
129
+ express: 'Express',
130
+ fastify: 'Fastify',
131
+ koa: 'Koa',
132
+ '@hapi/hapi': 'Hapi',
133
+ '@nestjs/core': 'NestJS',
134
+ 'strapi-admin': 'Strapi',
135
+ egg: 'Egg.js',
136
+ 'adonisjs/core': 'AdonisJS',
137
+ };
138
+ const PY_FRAMEWORK_MAP = {
139
+ fastapi: 'FastAPI',
140
+ django: 'Django',
141
+ flask: 'Flask',
142
+ 'aiohttp.web': 'aiohttp',
143
+ starlette: 'Starlette',
144
+ litestar: 'Litestar',
145
+ pyramid: 'Pyramid',
146
+ falcon: 'Falcon',
147
+ sanic: 'Sanic',
148
+ bottle: 'Bottle',
149
+ };
150
+ const RS_FRAMEWORK_MAP = {
151
+ axum: 'Axum',
152
+ actix: 'Actix Web',
153
+ rocket: 'Rocket',
154
+ warp: 'Warp',
155
+ tide: 'Tide',
156
+ poem: 'Poem',
157
+ salvo: 'Salvo',
158
+ };
159
+ const RB_FRAMEWORK_MAP = {
160
+ rails: 'Rails',
161
+ sinatra: 'Sinatra',
162
+ grape: 'Grape',
163
+ hanami: 'Hanami',
164
+ };
165
+ const GO_FRAMEWORK_MAP = {
166
+ gin: 'Gin',
167
+ echo: 'Echo',
168
+ fiber: 'Fiber',
169
+ chi: 'Chi',
170
+ gorilla: 'Gorilla Mux',
171
+ iris: 'Iris',
172
+ beego: 'Beego',
173
+ };
174
+ // Package manager lock files
175
+ const PKG_MANAGER_MAP = {
176
+ 'pnpm-lock.yaml': 'pnpm',
177
+ 'yarn.lock': 'yarn',
178
+ 'package-lock.json': 'npm',
179
+ 'bun.lockb': 'bun',
180
+ 'bun.lock': 'bun',
181
+ 'Cargo.lock': 'cargo',
182
+ 'go.sum': 'go mod',
183
+ 'Gemfile.lock': 'bundler',
184
+ 'poetry.lock': 'poetry',
185
+ 'Pipfile.lock': 'pipenv',
186
+ 'composer.lock': 'composer',
187
+ };
188
+ // Database provider indicators (from env / Prisma / connection strings)
189
+ const DB_INDICATORS = [
190
+ { pattern: /postgres(?:ql)?:\/\//i, label: 'Postgres' },
191
+ { pattern: /mysql:\/\//i, label: 'MySQL' },
192
+ { pattern: /mongodb(?:\+srv)?:\/\//i, label: 'MongoDB' },
193
+ { pattern: /sqlite:/i, label: 'SQLite' },
194
+ { pattern: /redis:\/\//i, label: 'Redis' },
195
+ { pattern: /DATABASE_URL\s*=\s*postgres/i, label: 'Postgres' },
196
+ { pattern: /DATABASE_URL\s*=\s*mysql/i, label: 'MySQL' },
197
+ { pattern: /REDIS_URL/i, label: 'Redis' },
198
+ { pattern: /MONGODB_URI/i, label: 'MongoDB' },
199
+ ];
200
+ const ORM_MAP = {
201
+ prisma: 'Prisma',
202
+ '@prisma/client': 'Prisma',
203
+ typeorm: 'TypeORM',
204
+ mikro: 'MikroORM',
205
+ '@mikro-orm/core': 'MikroORM',
206
+ sequelize: 'Sequelize',
207
+ knex: 'Knex',
208
+ objection: 'Objection',
209
+ bookshelf: 'Bookshelf',
210
+ mongoose: 'Mongoose',
211
+ drizzle: 'Drizzle ORM',
212
+ 'drizzle-orm': 'Drizzle ORM',
213
+ sqlalchemy: 'SQLAlchemy',
214
+ diesel: 'Diesel',
215
+ 'sqlx-core': 'SQLx',
216
+ gorm: 'GORM',
217
+ activerecord: 'ActiveRecord',
218
+ };
219
+ const TEST_FRAMEWORK_MAP = {
220
+ jest: 'Jest',
221
+ vitest: 'Vitest',
222
+ '@playwright/test': 'Playwright',
223
+ mocha: 'Mocha',
224
+ jasmine: 'Jasmine',
225
+ ava: 'Ava',
226
+ tap: 'Tap',
227
+ cypress: 'Cypress',
228
+ };
229
+ const TEST_FILE_PATTERNS = [
230
+ /\.test\./,
231
+ /\.spec\./,
232
+ /_test\./,
233
+ /test_/,
234
+ /\.test\.tsx?$/,
235
+ /\.spec\.tsx?$/,
236
+ /\.test\.jsx?$/,
237
+ /\.spec\.jsx?$/,
238
+ /_test\.py$/,
239
+ /test_.*\.py$/,
240
+ /_test\.go$/,
241
+ /_spec\.rb$/,
242
+ ];
243
+ // ---------------------------------------------------------------------------
244
+ // Directory-purpose inference
245
+ // ---------------------------------------------------------------------------
246
+ const DIR_PURPOSE_MAP = [
247
+ { names: ['pages', 'routes', 'app'], purpose: 'routes' },
248
+ { names: ['components', 'component', 'ui'], purpose: 'components' },
249
+ { names: ['lib', 'utils', 'utility', 'util', 'helpers', 'helper', 'shared'], purpose: 'utilities' },
250
+ { names: ['services', 'service'], purpose: 'services' },
251
+ { names: ['hooks', 'hook'], purpose: 'hooks' },
252
+ { names: ['models', 'model', 'entities', 'entity'], purpose: 'models' },
253
+ { names: ['controllers', 'controller'], purpose: 'controllers' },
254
+ { names: ['middleware', 'middlewares'], purpose: 'middleware' },
255
+ { names: ['store', 'stores', 'state'], purpose: 'store' },
256
+ { names: ['types', 'type', 'interfaces', '@types', 'typedefs'], purpose: 'types' },
257
+ { names: ['config', 'configs', 'configuration', 'settings'], purpose: 'config' },
258
+ { names: ['styles', 'style', 'css', 'scss', 'less'], purpose: 'styles' },
259
+ { names: ['public', 'static', 'assets', 'images', 'img'], purpose: 'assets' },
260
+ { names: ['tests', 'test', '__tests__', 'spec', 'e2e', 'integration', 'cypress'], purpose: 'tests' },
261
+ { names: ['e2e', 'cypress', 'playwright'], purpose: 'e2e-tests' },
262
+ { names: ['docs', 'doc', 'documentation'], purpose: 'docs' },
263
+ { names: ['scripts', 'script', 'bin', 'tools'], purpose: 'scripts' },
264
+ { names: ['migrations', 'migration', 'migrate', 'seed', 'seeds'], purpose: 'migrations' },
265
+ { names: ['infra', 'infrastructure', 'k8s', 'kubernetes', 'terraform', 'helm', 'docker'], purpose: 'infrastructure' },
266
+ { names: ['prisma'], purpose: 'prisma' },
267
+ { names: ['api'], purpose: 'api' },
268
+ { names: ['db', 'database', 'databases', 'sql'], purpose: 'db' },
269
+ ];
270
+ const KEY_PATTERN_RULES = [
271
+ {
272
+ label: 'JWT authentication',
273
+ files: ['auth.ts', 'auth.js', 'auth.py', 'authorization.ts', 'jwt.ts'],
274
+ deps: ['jsonwebtoken', 'jose', 'pyjwt', 'jwt'],
275
+ contentRegex: /\b(jwt|jsonwebtoken|jose)\b/i,
276
+ },
277
+ {
278
+ label: 'OAuth / social login',
279
+ deps: ['next-auth', '@auth/core', 'oauthlib', 'omniauth'],
280
+ contentRegex: /\b(oauth|openid\s*connect)\b/i,
281
+ },
282
+ {
283
+ label: 'Role-based access control (RBAC)',
284
+ files: ['roles.ts', 'permissions.ts', 'rbac.ts', 'authorization.ts', 'guards.ts'],
285
+ contentRegex: /\b(role|permission|rbac|hasRole|checkPermission|Ability)\b/i,
286
+ },
287
+ {
288
+ label: 'Input validation with schema library',
289
+ deps: ['zod', 'yup', 'joi', 'pydantic', 'marshmallow', 'class-validator'],
290
+ contentRegex: /\b(z\.|yup\.|joi\.|BaseModel|@IsString|@IsInt)\b/i,
291
+ },
292
+ {
293
+ label: 'Server actions for mutations',
294
+ files: ['actions.ts', 'actions.tsx'],
295
+ contentRegex: /['"]use server['"]/,
296
+ },
297
+ {
298
+ label: 'API route handlers',
299
+ files: ['route.ts', 'route.tsx', 'api.ts', 'handler.ts'],
300
+ },
301
+ {
302
+ label: 'Middleware chain',
303
+ files: ['middleware.ts', 'middleware.js', 'middlewares.ts'],
304
+ contentRegex: /\b(app\.use|router\.use|@Middleware|middleware)\b/i,
305
+ },
306
+ {
307
+ label: 'Webhook handling',
308
+ files: ['webhook.ts', 'webhooks.ts', 'webhook.py'],
309
+ deps: ['stripe', 'github-webhook-handler'],
310
+ contentRegex: /\b(webhook|stripe\.webhooks)\b/i,
311
+ },
312
+ {
313
+ label: 'Payment processing',
314
+ deps: ['stripe', 'paypal', '@stripe/stripe-js', 'braintree', 'square'],
315
+ contentRegex: /\b(stripe|payment|checkout|invoice)\b/i,
316
+ },
317
+ {
318
+ label: 'Email sending',
319
+ deps: ['nodemailer', 'resend', '@sendgrid/mail', 'mailgun'],
320
+ contentRegex: /\b(sendMail|transporter\.send|sendEmail|mail\.send)\b/i,
321
+ },
322
+ {
323
+ label: 'Job queue',
324
+ deps: ['bull', 'bullmq', 'bee-queue', 'celery', 'rq', 'sidekiq'],
325
+ contentRegex: /\b(Queue|Job|enqueue|Worker|processJob)\b/i,
326
+ },
327
+ {
328
+ label: 'Caching layer',
329
+ deps: ['redis', 'ioredis', 'lru-cache', 'cache-manager', '@nestjs/cache-manager'],
330
+ contentRegex: /\b(cache|caching|redis\.|createCache)\b/i,
331
+ },
332
+ {
333
+ label: 'Data fetching / React Query',
334
+ deps: ['@tanstack/react-query', 'swr', '@trpc/client', 'react-query'],
335
+ },
336
+ {
337
+ label: 'State management',
338
+ deps: ['zustand', 'jotai', 'redux', '@reduxjs/toolkit', 'recoil', 'mobx', 'valtio', 'pinia', 'vuex'],
339
+ },
340
+ {
341
+ label: 'WebSocket communication',
342
+ deps: ['ws', 'socket.io', '@nestjs/websockets', 'uWebSockets.js', 'sockjs'],
343
+ contentRegex: /\b(WebSocket|socket\.io|ws\.on|socket\.emit)\b/i,
344
+ },
345
+ {
346
+ label: 'GraphQL API',
347
+ deps: ['graphql', '@apollo/server', 'apollo-server', '@nestjs/graphql', 'graphene', 'strawberry-graphql'],
348
+ contentRegex: /\b(gql|graphql|ApolloServer|Query|Mutation)\b/i,
349
+ },
350
+ {
351
+ label: 'REST API',
352
+ files: ['api.ts', 'api.js', 'api.py', 'routes.py', 'router.ts'],
353
+ },
354
+ {
355
+ label: 'Internationalization (i18n)',
356
+ deps: ['i18next', 'next-i18next', 'react-intl', 'vue-i18n', 'gettext'],
357
+ files: ['i18n.ts', 'locales/', 'translations/'],
358
+ },
359
+ {
360
+ label: 'Feature flags',
361
+ deps: ['launchdarkly', '@launchdarkly/node-server-sdk', 'unleash', 'flagsmith'],
362
+ },
363
+ {
364
+ label: 'CLI interface',
365
+ deps: ['commander', 'yargs', 'clack', '@inquirer/prompts', 'clap', 'click'],
366
+ files: ['cli.ts', 'cli.js', 'cli.py', 'main.go'],
367
+ },
368
+ ];
369
+ // ---------------------------------------------------------------------------
370
+ // Entry-point file names (ordered by likelihood)
371
+ // ---------------------------------------------------------------------------
372
+ const ENTRY_POINT_CANDIDATES = [
373
+ 'src/app/layout.tsx',
374
+ 'src/app/page.tsx',
375
+ 'src/pages/_app.tsx',
376
+ 'src/pages/_app.ts',
377
+ 'src/pages/index.tsx',
378
+ 'src/pages/index.ts',
379
+ 'src/index.ts',
380
+ 'src/index.tsx',
381
+ 'src/main.ts',
382
+ 'src/main.tsx',
383
+ 'src/main.js',
384
+ 'src/server.ts',
385
+ 'src/server.js',
386
+ 'src/app.ts',
387
+ 'src/app.js',
388
+ 'server.ts',
389
+ 'server.js',
390
+ 'index.ts',
391
+ 'index.js',
392
+ 'main.ts',
393
+ 'main.js',
394
+ 'app.ts',
395
+ 'app.js',
396
+ 'src/main.rs',
397
+ 'main.rs',
398
+ 'src/main.go',
399
+ 'main.go',
400
+ 'src/app.py',
401
+ 'app.py',
402
+ 'main.py',
403
+ 'src/__main__.py',
404
+ 'src/main.rb',
405
+ 'main.rb',
406
+ 'src/Main.java',
407
+ 'src/main/java',
408
+ 'next.config.js',
409
+ 'next.config.ts',
410
+ 'next.config.mjs',
411
+ ];
412
+ const scanCache = new Map();
413
+ function cacheKey(rootPath) {
414
+ return createHash('md5').update(rootPath).digest('hex');
415
+ }
416
+ function getCached(rootPath) {
417
+ const key = cacheKey(rootPath);
418
+ const entry = scanCache.get(key);
419
+ if (!entry)
420
+ return null;
421
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
422
+ scanCache.delete(key);
423
+ return null;
424
+ }
425
+ return entry.image;
426
+ }
427
+ function setCached(rootPath, image) {
428
+ scanCache.set(cacheKey(rootPath), {
429
+ image,
430
+ timestamp: Date.now(),
431
+ hash: '',
432
+ });
433
+ }
434
+ function clearCache() {
435
+ scanCache.clear();
436
+ }
437
+ // ---------------------------------------------------------------------------
438
+ // Helpers
439
+ // ---------------------------------------------------------------------------
440
+ async function pathExists(p) {
441
+ try {
442
+ await fsp.access(p);
443
+ return true;
444
+ }
445
+ catch {
446
+ return false;
447
+ }
448
+ }
449
+ async function safeReadFile(filePath) {
450
+ try {
451
+ const stat = await fsp.stat(filePath);
452
+ if (stat.size > MAX_FILE_SIZE)
453
+ return '';
454
+ return await fsp.readFile(filePath, 'utf-8');
455
+ }
456
+ catch {
457
+ return '';
458
+ }
459
+ }
460
+ async function safeReadJson(filePath) {
461
+ const raw = await safeReadFile(filePath);
462
+ if (!raw)
463
+ return null;
464
+ try {
465
+ return JSON.parse(raw);
466
+ }
467
+ catch {
468
+ return null;
469
+ }
470
+ }
471
+ function readFirstLines(raw, count) {
472
+ const lines = raw.split('\n');
473
+ if (lines.length <= count)
474
+ return raw;
475
+ return lines.slice(0, count).join('\n');
476
+ }
477
+ /** Read .gitignore patterns from a file and return a set of ignore globs. */
478
+ function parseGitignore(content) {
479
+ return content
480
+ .split('\n')
481
+ .map((l) => l.trim())
482
+ .filter((l) => l && !l.startsWith('#'));
483
+ }
484
+ /** Check if a relative path should be ignored based on ignore patterns and built-in ignore dirs. */
485
+ function shouldIgnorePath(relativePath, ignorePatterns, options) {
486
+ const parts = relativePath.split(path.sep);
487
+ for (const part of parts) {
488
+ if (options.ignoreDirs.has(part))
489
+ return true;
490
+ if (part.startsWith('.'))
491
+ return true;
492
+ }
493
+ for (const pattern of ignorePatterns) {
494
+ if (pattern.startsWith('/')) {
495
+ const clean = pattern.slice(1);
496
+ if (clean === relativePath)
497
+ return true;
498
+ if (clean.endsWith('/') && relativePath.startsWith(clean))
499
+ return true;
500
+ if (relativePath === clean)
501
+ return true;
502
+ if (relativePath.startsWith(clean + '/'))
503
+ return true;
504
+ }
505
+ else if (pattern.endsWith('/')) {
506
+ const clean = pattern.slice(0, -1);
507
+ if (relativePath === clean)
508
+ return true;
509
+ if (relativePath.startsWith(clean + '/'))
510
+ return true;
511
+ if (parts.includes(clean))
512
+ return true;
513
+ }
514
+ else {
515
+ if (relativePath === pattern)
516
+ return true;
517
+ if (relativePath.endsWith('/' + pattern))
518
+ return true;
519
+ if (parts.includes(pattern))
520
+ return true;
521
+ }
522
+ }
523
+ return false;
524
+ }
525
+ /** Infer the purpose of a directory based on its name. */
526
+ function inferDirectoryPurpose(dirName, parentPurpose) {
527
+ const lower = dirName.toLowerCase();
528
+ for (const entry of DIR_PURPOSE_MAP) {
529
+ if (entry.names.includes(lower)) {
530
+ return entry.purpose;
531
+ }
532
+ }
533
+ if (parentPurpose === 'routes')
534
+ return 'routes';
535
+ if (dirName.startsWith('page') || dirName.startsWith('route'))
536
+ return 'routes';
537
+ return 'root';
538
+ }
539
+ /** Check if a file is a test file. */
540
+ function isTestFile(fileName) {
541
+ return TEST_FILE_PATTERNS.some((p) => p.test(fileName));
542
+ }
543
+ async function detectLanguageAndPackageManager(rootPath) {
544
+ let language = 'unknown';
545
+ let version = '';
546
+ let configFile = '';
547
+ let packageManager = 'unknown';
548
+ // Check config files for language detection
549
+ const configChecks = [];
550
+ for (const [lang, entry] of Object.entries(LANG_MAP)) {
551
+ const fp = path.join(rootPath, entry.configFile);
552
+ configChecks.push([lang, fp]);
553
+ }
554
+ for (const [lang, fp] of configChecks) {
555
+ if (await pathExists(fp)) {
556
+ const entry = LANG_MAP[lang];
557
+ language = lang;
558
+ configFile = entry.configFile;
559
+ // Detect version
560
+ if (entry.versionFromConfig) {
561
+ const raw = await safeReadFile(fp);
562
+ if (raw)
563
+ version = entry.versionFromConfig(raw);
564
+ }
565
+ if (!version && entry.versionFile) {
566
+ const verRaw = await safeReadFile(path.join(rootPath, entry.versionFile));
567
+ if (verRaw) {
568
+ version = verRaw.trim().replace('v', '');
569
+ }
570
+ }
571
+ break;
572
+ }
573
+ }
574
+ // If no config file found, fall back to extension counting
575
+ if (language === 'unknown') {
576
+ language = await detectLanguageByExtensions(rootPath);
577
+ }
578
+ // Typescript vs JavaScript refinement
579
+ if (language === 'javascript') {
580
+ const tsconfig = await pathExists(path.join(rootPath, 'tsconfig.json'));
581
+ if (tsconfig) {
582
+ language = 'typescript';
583
+ configFile = 'tsconfig.json';
584
+ }
585
+ }
586
+ // TypeScript version from package.json (not from tsconfig.json)
587
+ if (!version && (language === 'typescript' || language === 'javascript')) {
588
+ const pkgRaw = await safeReadFile(path.join(rootPath, 'package.json'));
589
+ if (pkgRaw) {
590
+ try {
591
+ const pkg = JSON.parse(pkgRaw);
592
+ const deps = { ...(pkg.devDependencies || {}), ...(pkg.dependencies || {}) };
593
+ if (deps.typescript) {
594
+ version = deps.typescript;
595
+ }
596
+ }
597
+ catch {
598
+ // ignore parse errors
599
+ }
600
+ }
601
+ }
602
+ // Package manager detection
603
+ for (const [lockFile, manager] of Object.entries(PKG_MANAGER_MAP)) {
604
+ if (await pathExists(path.join(rootPath, lockFile))) {
605
+ packageManager = manager;
606
+ break;
607
+ }
608
+ }
609
+ // Fallback: check for requirements.txt (pip)
610
+ if (packageManager === 'unknown') {
611
+ if (await pathExists(path.join(rootPath, 'requirements.txt'))) {
612
+ packageManager = 'pip';
613
+ }
614
+ else if (await pathExists(path.join(rootPath, 'pyproject.toml'))) {
615
+ packageManager = 'poetry';
616
+ }
617
+ else if (await pathExists(path.join(rootPath, 'Pipfile'))) {
618
+ packageManager = 'pipenv';
619
+ }
620
+ }
621
+ return { language, version, configFile, packageManager };
622
+ }
623
+ async function detectLanguageByExtensions(rootPath) {
624
+ const extCounts = {};
625
+ let scanned = 0;
626
+ const maxScan = 500;
627
+ try {
628
+ const entries = await fsp.readdir(rootPath);
629
+ for (const entry of entries) {
630
+ if (scanned > maxScan)
631
+ break;
632
+ const fullPath = path.join(rootPath, entry);
633
+ try {
634
+ const stat = await fsp.stat(fullPath);
635
+ if (stat.isDirectory()) {
636
+ if (IGNORE_DIRS.has(entry))
637
+ continue;
638
+ const subExts = await countExtensionsInDir(fullPath, 2, maxScan - scanned);
639
+ scanned += subExts.scanned;
640
+ for (const [ext, count] of Object.entries(subExts.exts)) {
641
+ extCounts[ext] = (extCounts[ext] || 0) + count;
642
+ }
643
+ }
644
+ else {
645
+ const ext = path.extname(entry).toLowerCase();
646
+ if (ext)
647
+ extCounts[ext] = (extCounts[ext] || 0) + 1;
648
+ scanned++;
649
+ }
650
+ }
651
+ catch {
652
+ // skip unreadable
653
+ }
654
+ }
655
+ }
656
+ catch {
657
+ // empty directory
658
+ }
659
+ // Map extensions to languages
660
+ const langScores = {};
661
+ const extToLang = {
662
+ '.ts': 'typescript',
663
+ '.tsx': 'typescript',
664
+ '.js': 'javascript',
665
+ '.jsx': 'javascript',
666
+ '.mjs': 'javascript',
667
+ '.cjs': 'javascript',
668
+ '.py': 'python',
669
+ '.rs': 'rust',
670
+ '.go': 'go',
671
+ '.rb': 'ruby',
672
+ '.java': 'java',
673
+ '.kt': 'kotlin',
674
+ '.kts': 'kotlin',
675
+ '.cpp': 'cpp',
676
+ '.cxx': 'cpp',
677
+ '.hpp': 'cpp',
678
+ '.h': 'cpp',
679
+ '.c': 'cpp',
680
+ '.cs': 'csharp',
681
+ '.swift': 'swift',
682
+ '.php': 'php',
683
+ };
684
+ for (const [ext, count] of Object.entries(extCounts)) {
685
+ const lang = extToLang[ext];
686
+ if (lang) {
687
+ langScores[lang] = (langScores[lang] || 0) + count;
688
+ }
689
+ }
690
+ let bestLang = 'unknown';
691
+ let bestScore = 0;
692
+ for (const [lang, score] of Object.entries(langScores)) {
693
+ if (score > bestScore) {
694
+ bestScore = score;
695
+ bestLang = lang;
696
+ }
697
+ }
698
+ return bestLang;
699
+ }
700
+ async function countExtensionsInDir(dirPath, depth, remaining) {
701
+ const exts = {};
702
+ let scanned = 0;
703
+ if (depth <= 0 || remaining <= 0)
704
+ return { exts, scanned };
705
+ try {
706
+ const entries = await fsp.readdir(dirPath);
707
+ for (const entry of entries) {
708
+ if (scanned >= remaining)
709
+ break;
710
+ const full = path.join(dirPath, entry);
711
+ try {
712
+ const stat = await fsp.stat(full);
713
+ if (stat.isDirectory()) {
714
+ if (IGNORE_DIRS.has(entry))
715
+ continue;
716
+ const sub = await countExtensionsInDir(full, depth - 1, remaining - scanned);
717
+ for (const [ext, count] of Object.entries(sub.exts)) {
718
+ exts[ext] = (exts[ext] || 0) + count;
719
+ }
720
+ scanned += sub.scanned;
721
+ }
722
+ else {
723
+ const ext = path.extname(entry).toLowerCase();
724
+ if (ext)
725
+ exts[ext] = (exts[ext] || 0) + 1;
726
+ scanned++;
727
+ }
728
+ }
729
+ catch {
730
+ // skip
731
+ }
732
+ }
733
+ }
734
+ catch {
735
+ // skip
736
+ }
737
+ return { exts, scanned };
738
+ }
739
+ // ---------------------------------------------------------------------------
740
+ // Framework detection
741
+ // ---------------------------------------------------------------------------
742
+ async function detectFramework(rootPath, language) {
743
+ if (language === 'typescript' || language === 'javascript') {
744
+ const pkg = await safeReadJson(path.join(rootPath, 'package.json'));
745
+ if (!pkg)
746
+ return '';
747
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
748
+ for (const [name, label] of Object.entries(JS_FRAMEWORK_MAP)) {
749
+ if (deps[name]) {
750
+ const version = deps[name].replace(/^\^|~/, '');
751
+ // Detect App Router vs Pages Router for Next.js
752
+ if (name === 'next' && (await pathExists(path.join(rootPath, 'src/app/layout.tsx')) || await pathExists(path.join(rootPath, 'src/app/page.tsx')))) {
753
+ return `Next.js ${version} (App Router)`;
754
+ }
755
+ if (name === 'next' && (await pathExists(path.join(rootPath, 'src/pages/_app.tsx')))) {
756
+ return `Next.js ${version} (Pages Router)`;
757
+ }
758
+ return `${label} ${version}`;
759
+ }
760
+ }
761
+ return '';
762
+ }
763
+ if (language === 'python') {
764
+ const pyproject = await safeReadFile(path.join(rootPath, 'pyproject.toml'));
765
+ const reqs = await safeReadFile(path.join(rootPath, 'requirements.txt'));
766
+ const combined = (pyproject + '\n' + reqs).toLowerCase();
767
+ for (const [name, label] of Object.entries(PY_FRAMEWORK_MAP)) {
768
+ if (combined.includes(name))
769
+ return label;
770
+ }
771
+ return '';
772
+ }
773
+ if (language === 'rust') {
774
+ const cargo = await safeReadFile(path.join(rootPath, 'Cargo.toml')).then((r) => r.toLowerCase());
775
+ if (!cargo)
776
+ return '';
777
+ for (const [name, label] of Object.entries(RS_FRAMEWORK_MAP)) {
778
+ if (cargo.includes(name))
779
+ return label;
780
+ }
781
+ return '';
782
+ }
783
+ if (language === 'go') {
784
+ const gomod = await safeReadFile(path.join(rootPath, 'go.mod')).then((r) => r.toLowerCase());
785
+ if (!gomod)
786
+ return '';
787
+ for (const [name, label] of Object.entries(GO_FRAMEWORK_MAP)) {
788
+ if (gomod.includes(name))
789
+ return label;
790
+ }
791
+ return '';
792
+ }
793
+ if (language === 'ruby') {
794
+ const gemfile = await safeReadFile(path.join(rootPath, 'Gemfile')).then((r) => r.toLowerCase());
795
+ if (!gemfile)
796
+ return '';
797
+ for (const [name, label] of Object.entries(RB_FRAMEWORK_MAP)) {
798
+ if (gemfile.includes(name))
799
+ return label;
800
+ }
801
+ return '';
802
+ }
803
+ return '';
804
+ }
805
+ // ---------------------------------------------------------------------------
806
+ // Database & ORM detection
807
+ // ---------------------------------------------------------------------------
808
+ async function detectDatabaseAndORM(rootPath, language) {
809
+ let database = null;
810
+ let orm = null;
811
+ // Check .env files for database URLs
812
+ for (const envFile of ['.env', '.env.local', '.env.development', '.env.production']) {
813
+ const envRaw = await safeReadFile(path.join(rootPath, envFile));
814
+ if (envRaw) {
815
+ for (const indicator of DB_INDICATORS) {
816
+ if (indicator.pattern.test(envRaw)) {
817
+ if (!database)
818
+ database = indicator.label;
819
+ }
820
+ }
821
+ }
822
+ }
823
+ // Check Prisma schema for provider
824
+ const prismaSchema = await safeReadFile(path.join(rootPath, 'prisma/schema.prisma'));
825
+ if (prismaSchema) {
826
+ const providerMatch = prismaSchema.match(/provider\s*=\s*"(\w+)"/);
827
+ if (providerMatch) {
828
+ const prov = providerMatch[1];
829
+ const providerMap = {
830
+ postgresql: 'Postgres',
831
+ mysql: 'MySQL',
832
+ sqlite: 'SQLite',
833
+ sqlserver: 'SQL Server',
834
+ mongodb: 'MongoDB',
835
+ cockroachdb: 'CockroachDB',
836
+ };
837
+ database = providerMap[prov] || prov;
838
+ }
839
+ const modelCount = (prismaSchema.match(/model\s+\w+\s*\{/g) || []).length;
840
+ orm = `Prisma (${modelCount} ${modelCount === 1 ? 'model' : 'models'})`;
841
+ }
842
+ // Check for ORM in package.json (JS/TS projects)
843
+ if (!orm && (language === 'typescript' || language === 'javascript')) {
844
+ const pkg = await safeReadJson(path.join(rootPath, 'package.json'));
845
+ if (pkg) {
846
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
847
+ for (const [name, label] of Object.entries(ORM_MAP)) {
848
+ if (deps[name]) {
849
+ orm = label;
850
+ if (!database && name === 'mongoose')
851
+ database = 'MongoDB';
852
+ break;
853
+ }
854
+ }
855
+ // Infer database from packages
856
+ if (!database) {
857
+ if (deps['pg'])
858
+ database = 'Postgres';
859
+ else if (deps['mysql2'])
860
+ database = 'MySQL';
861
+ else if (deps['mongodb'] || deps['mongoose'])
862
+ database = 'MongoDB';
863
+ else if (deps['better-sqlite3'] || deps['sqlite3'])
864
+ database = 'SQLite';
865
+ else if (deps['redis'] || deps['ioredis'])
866
+ database = 'Redis';
867
+ }
868
+ }
869
+ }
870
+ // Check Python ORM/Database
871
+ if (language === 'python') {
872
+ const reqs = await safeReadFile(path.join(rootPath, 'requirements.txt'));
873
+ const pyproject = await safeReadFile(path.join(rootPath, 'pyproject.toml'));
874
+ const combined = (reqs + '\n' + pyproject).toLowerCase();
875
+ if (combined.includes('sqlalchemy')) {
876
+ if (!orm)
877
+ orm = 'SQLAlchemy';
878
+ }
879
+ if (combined.includes('django') && !orm) {
880
+ orm = 'Django ORM';
881
+ }
882
+ if (combined.includes('psycopg2') && !database)
883
+ database = 'Postgres';
884
+ if (combined.includes('pymongo') && !database)
885
+ database = 'MongoDB';
886
+ if (combined.includes('redis') && !database)
887
+ database = 'Redis';
888
+ }
889
+ // Check Rust ORM
890
+ if (language === 'rust') {
891
+ const cargo = (await safeReadFile(path.join(rootPath, 'Cargo.toml'))).toLowerCase();
892
+ if (cargo.includes('diesel') && !orm)
893
+ orm = 'Diesel';
894
+ if (cargo.includes('sqlx') && !orm)
895
+ orm = 'SQLx';
896
+ }
897
+ return { database, orm };
898
+ }
899
+ // ---------------------------------------------------------------------------
900
+ // Test framework detection
901
+ // ---------------------------------------------------------------------------
902
+ async function detectTestFramework(rootPath, language) {
903
+ if (language === 'typescript' || language === 'javascript') {
904
+ const pkg = await safeReadJson(path.join(rootPath, 'package.json'));
905
+ if (!pkg)
906
+ return null;
907
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
908
+ for (const [name, label] of Object.entries(TEST_FRAMEWORK_MAP)) {
909
+ if (deps[name]) {
910
+ if (name === 'vitest' && deps['@playwright/test'])
911
+ return 'Vitest + Playwright';
912
+ if (name === 'jest' && deps['@playwright/test'])
913
+ return 'Jest + Playwright';
914
+ return label;
915
+ }
916
+ }
917
+ // Config file checks
918
+ if (await pathExists(path.join(rootPath, 'jest.config.ts')) || await pathExists(path.join(rootPath, 'jest.config.js')))
919
+ return 'Jest';
920
+ if (await pathExists(path.join(rootPath, 'vitest.config.ts')) || await pathExists(path.join(rootPath, 'vitest.config.js')))
921
+ return 'Vitest';
922
+ if (await pathExists(path.join(rootPath, 'playwright.config.ts')))
923
+ return 'Playwright';
924
+ return null;
925
+ }
926
+ if (language === 'python') {
927
+ if (await pathExists(path.join(rootPath, 'pytest.ini')) ||
928
+ await pathExists(path.join(rootPath, 'pyproject.toml'))) {
929
+ const pyproject = await safeReadFile(path.join(rootPath, 'pyproject.toml'));
930
+ if (pyproject && pyproject.includes('[tool.pytest'))
931
+ return 'pytest';
932
+ }
933
+ const reqs = await safeReadFile(path.join(rootPath, 'requirements.txt'));
934
+ if (reqs && /pytest/.test(reqs))
935
+ return 'pytest';
936
+ return null;
937
+ }
938
+ if (language === 'rust')
939
+ return 'cargo test';
940
+ if (language === 'go')
941
+ return 'go test';
942
+ return null;
943
+ }
944
+ // ---------------------------------------------------------------------------
945
+ // Entry-point detection
946
+ // ---------------------------------------------------------------------------
947
+ async function detectEntryPoints(rootPath) {
948
+ const found = [];
949
+ for (const candidate of ENTRY_POINT_CANDIDATES) {
950
+ if (found.length >= 8)
951
+ break;
952
+ if (await pathExists(path.join(rootPath, candidate))) {
953
+ found.push(candidate);
954
+ }
955
+ }
956
+ // If nothing found, look for any index/main files
957
+ if (found.length === 0) {
958
+ const srcDir = path.join(rootPath, 'src');
959
+ if (await pathExists(srcDir)) {
960
+ try {
961
+ const entries = await fsp.readdir(srcDir);
962
+ for (const e of entries) {
963
+ if (/^(index|main|app|server|cli)\./.test(e)) {
964
+ found.push(`src/${e}`);
965
+ }
966
+ }
967
+ }
968
+ catch {
969
+ // skip
970
+ }
971
+ }
972
+ }
973
+ return found;
974
+ }
975
+ // ---------------------------------------------------------------------------
976
+ // Dependency extraction
977
+ // ---------------------------------------------------------------------------
978
+ async function extractDependencies(rootPath, language) {
979
+ const deps = [];
980
+ if (language === 'typescript' || language === 'javascript') {
981
+ const pkg = await safeReadJson(path.join(rootPath, 'package.json'));
982
+ if (pkg) {
983
+ const prod = pkg.dependencies;
984
+ const dev = pkg.devDependencies;
985
+ if (prod) {
986
+ for (const [name, version] of Object.entries(prod)) {
987
+ deps.push({ name, version, type: 'production' });
988
+ }
989
+ }
990
+ if (dev) {
991
+ for (const [name, version] of Object.entries(dev)) {
992
+ deps.push({ name, version, type: 'dev' });
993
+ }
994
+ }
995
+ }
996
+ }
997
+ if (language === 'rust') {
998
+ const cargoRaw = await safeReadFile(path.join(rootPath, 'Cargo.toml'));
999
+ if (cargoRaw) {
1000
+ const inDeps = cargoRaw.includes('[dependencies]');
1001
+ let inSection = false;
1002
+ for (const line of cargoRaw.split('\n')) {
1003
+ const trimmed = line.trim();
1004
+ if (trimmed === '[dependencies]') {
1005
+ inSection = true;
1006
+ continue;
1007
+ }
1008
+ if (trimmed.startsWith('[') && inSection) {
1009
+ inSection = false;
1010
+ continue;
1011
+ }
1012
+ if (inSection && trimmed && !trimmed.startsWith('#')) {
1013
+ const depMatch = trimmed.match(/^(\S+)\s*=\s*"([^"]+)"/);
1014
+ if (depMatch) {
1015
+ deps.push({ name: depMatch[1], version: depMatch[2], type: 'production' });
1016
+ }
1017
+ else {
1018
+ const nameOnly = trimmed.split('=')[0]?.trim();
1019
+ if (nameOnly && !nameOnly.startsWith('[')) {
1020
+ deps.push({ name: nameOnly, version: '', type: 'production' });
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ if (language === 'go') {
1028
+ const gomod = await safeReadFile(path.join(rootPath, 'go.mod'));
1029
+ if (gomod) {
1030
+ const lines = gomod.split('\n');
1031
+ for (const line of lines) {
1032
+ const trimmed = line.trim();
1033
+ if (trimmed.startsWith('require ')) {
1034
+ const parts = trimmed.slice(8).split(/\s+/);
1035
+ if (parts.length >= 2) {
1036
+ deps.push({ name: parts[0], version: parts[1], type: 'production' });
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ if (language === 'ruby') {
1043
+ const gemfile = await safeReadFile(path.join(rootPath, 'Gemfile'));
1044
+ if (gemfile) {
1045
+ for (const line of gemfile.split('\n')) {
1046
+ const m = line.match(/gem\s+['"](\S+)['"](?:,\s*['"]([^'"]+)['"])?/);
1047
+ if (m) {
1048
+ deps.push({ name: m[1], version: m[2] || '', type: 'production' });
1049
+ }
1050
+ }
1051
+ }
1052
+ }
1053
+ if (language === 'python') {
1054
+ const reqs = await safeReadFile(path.join(rootPath, 'requirements.txt'));
1055
+ if (reqs) {
1056
+ for (const line of reqs.split('\n')) {
1057
+ const trimmed = line.trim();
1058
+ if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('-')) {
1059
+ const pair = trimmed.split(/[=<>~!]+/);
1060
+ const name = pair[0].trim();
1061
+ const ver = trimmed.slice(name.length).trim().replace(/^[=<>~!]+/, '');
1062
+ if (name)
1063
+ deps.push({ name, version: ver, type: 'production' });
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+ return deps;
1069
+ }
1070
+ // ---------------------------------------------------------------------------
1071
+ // Directory structure scanning
1072
+ // ---------------------------------------------------------------------------
1073
+ async function scanDirectoryStructure(dirPath, relativePath, depth, options, ignorePatterns) {
1074
+ const name = path.basename(dirPath);
1075
+ const result = {
1076
+ path: relativePath || '.',
1077
+ name,
1078
+ purpose: inferDirectoryPurpose(name),
1079
+ itemCount: 0,
1080
+ notableFiles: [],
1081
+ children: [],
1082
+ };
1083
+ if (depth > options.maxDepth)
1084
+ return result;
1085
+ let entries;
1086
+ try {
1087
+ entries = await fsp.readdir(dirPath);
1088
+ }
1089
+ catch {
1090
+ return result;
1091
+ }
1092
+ const files = [];
1093
+ const dirs = [];
1094
+ for (const entry of entries) {
1095
+ const relEntry = relativePath ? path.join(relativePath, entry) : entry;
1096
+ if (shouldIgnorePath(relEntry, ignorePatterns, options))
1097
+ continue;
1098
+ try {
1099
+ const fullPath = path.join(dirPath, entry);
1100
+ const stat = await fsp.stat(fullPath);
1101
+ if (stat.isDirectory()) {
1102
+ dirs.push(entry);
1103
+ }
1104
+ else if (stat.isFile()) {
1105
+ files.push(entry);
1106
+ }
1107
+ }
1108
+ catch {
1109
+ // skip unreadable
1110
+ }
1111
+ }
1112
+ result.itemCount = dirs.length + files.length;
1113
+ // Sort and collect notable files
1114
+ const sortedFiles = files.sort();
1115
+ const keyFiles = sortedFiles.filter((f) => {
1116
+ const lf = f.toLowerCase();
1117
+ return (!lf.startsWith('.') &&
1118
+ !lf.endsWith('.map') &&
1119
+ !lf.endsWith('.lock') &&
1120
+ !lf.endsWith('.json') &&
1121
+ lf !== 'readme.md' &&
1122
+ lf !== 'license');
1123
+ });
1124
+ result.notableFiles = keyFiles.slice(0, 12);
1125
+ // Recurse into subdirectories
1126
+ for (const dirName of dirs.sort()) {
1127
+ const fullPath = path.join(dirPath, dirName);
1128
+ const relPath = relativePath ? path.join(relativePath, dirName) : dirName;
1129
+ const child = await scanDirectoryStructure(fullPath, relPath, depth + 1, options, ignorePatterns);
1130
+ result.children.push(child);
1131
+ }
1132
+ return result;
1133
+ }
1134
+ // ---------------------------------------------------------------------------
1135
+ // Architecture detection
1136
+ // ---------------------------------------------------------------------------
1137
+ async function detectArchitecture(rootPath, structure) {
1138
+ // Check for monorepo structure
1139
+ if (await pathExists(path.join(rootPath, 'packages')) ||
1140
+ await pathExists(path.join(rootPath, 'apps'))) {
1141
+ return 'monorepo';
1142
+ }
1143
+ // Check for microservices
1144
+ if (await pathExists(path.join(rootPath, 'docker-compose.yml'))) {
1145
+ const dc = await safeReadFile(path.join(rootPath, 'docker-compose.yml'));
1146
+ if (dc) {
1147
+ const serviceCount = (dc.match(/^\s{2}\w+:[ \t]*$/gm) || []).length;
1148
+ if (serviceCount >= 3)
1149
+ return 'microservices';
1150
+ }
1151
+ }
1152
+ // Check for serverless
1153
+ if (await pathExists(path.join(rootPath, 'serverless.yml')) ||
1154
+ await pathExists(path.join(rootPath, 'serverless.ts')) ||
1155
+ await pathExists(path.join(rootPath, 'template.yaml'))) {
1156
+ return 'serverless';
1157
+ }
1158
+ // Check for MVC pattern
1159
+ const hasControllers = structure.some((n) => n.purpose === 'controllers');
1160
+ const hasModels = structure.some((n) => n.purpose === 'models');
1161
+ const hasRoutes = structure.some((n) => n.purpose === 'routes');
1162
+ if (hasControllers && (hasModels || hasRoutes))
1163
+ return 'mvc';
1164
+ // Check for layered architecture
1165
+ const hasServices = structure.some((n) => n.purpose === 'services');
1166
+ const hasLib = structure.some((n) => n.purpose === 'utilities');
1167
+ if (hasServices && hasLib)
1168
+ return 'layered';
1169
+ return 'monolith';
1170
+ }
1171
+ // ---------------------------------------------------------------------------
1172
+ // Key pattern detection
1173
+ // ---------------------------------------------------------------------------
1174
+ async function detectKeyPatterns(rootPath, language, deps, structure) {
1175
+ const patterns = new Set();
1176
+ const depNames = new Set(deps.map((d) => d.name.toLowerCase()));
1177
+ // Collect all files across the structure
1178
+ function collectFiles(nodes) {
1179
+ const files = [];
1180
+ for (const node of nodes) {
1181
+ files.push(...node.notableFiles, ...node.notableFiles.map((f) => node.path + '/' + f));
1182
+ files.push(...collectFiles(node.children));
1183
+ }
1184
+ return files;
1185
+ }
1186
+ const allFiles = collectFiles(structure);
1187
+ const fileSet = new Set(allFiles.map((f) => f.toLowerCase()));
1188
+ for (const rule of KEY_PATTERN_RULES) {
1189
+ let detected = false;
1190
+ // Check file names
1191
+ if (rule.files) {
1192
+ for (const fileName of rule.files) {
1193
+ for (const f of allFiles) {
1194
+ if (f.toLowerCase().includes(fileName.toLowerCase())) {
1195
+ detected = true;
1196
+ break;
1197
+ }
1198
+ }
1199
+ if (detected)
1200
+ break;
1201
+ }
1202
+ }
1203
+ // Check dependencies
1204
+ if (!detected && rule.deps) {
1205
+ for (const depName of rule.deps) {
1206
+ if (depNames.has(depName.toLowerCase())) {
1207
+ detected = true;
1208
+ break;
1209
+ }
1210
+ }
1211
+ }
1212
+ // Check content in key files (sample only)
1213
+ if (!detected && rule.contentRegex) {
1214
+ // Sample a few known config/auth/middleware files
1215
+ const sampleFiles = ['middleware.ts', 'auth.ts', 'config.ts', 'api.ts', 'app.ts', 'main.ts', 'server.ts'];
1216
+ for (const sf of sampleFiles) {
1217
+ const content = await safeReadFile(path.join(rootPath, sf));
1218
+ if (content && rule.contentRegex.test(content)) {
1219
+ detected = true;
1220
+ break;
1221
+ }
1222
+ if (await pathExists(path.join(rootPath, 'src', sf))) {
1223
+ const srcContent = await safeReadFile(path.join(rootPath, 'src', sf));
1224
+ if (srcContent && rule.contentRegex.test(srcContent)) {
1225
+ detected = true;
1226
+ break;
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+ if (detected) {
1232
+ patterns.add(rule.label);
1233
+ }
1234
+ }
1235
+ return Array.from(patterns);
1236
+ }
1237
+ // ---------------------------------------------------------------------------
1238
+ // File & test counting
1239
+ // ---------------------------------------------------------------------------
1240
+ async function countFiles(rootPath, options, ignorePatterns) {
1241
+ let fileCount = 0;
1242
+ let testCount = 0;
1243
+ async function walk(dirPath, relativePath, depth) {
1244
+ if (depth > 10)
1245
+ return;
1246
+ let entries;
1247
+ try {
1248
+ entries = await fsp.readdir(dirPath);
1249
+ }
1250
+ catch {
1251
+ return;
1252
+ }
1253
+ for (const entry of entries) {
1254
+ const fullPath = path.join(dirPath, entry);
1255
+ const relPath = relativePath ? path.join(relativePath, entry) : entry;
1256
+ if (shouldIgnorePath(relPath, ignorePatterns, options))
1257
+ continue;
1258
+ try {
1259
+ const stat = await fsp.stat(fullPath);
1260
+ if (stat.isDirectory()) {
1261
+ await walk(fullPath, relPath, depth + 1);
1262
+ }
1263
+ else if (stat.isFile()) {
1264
+ fileCount++;
1265
+ if (isTestFile(entry))
1266
+ testCount++;
1267
+ }
1268
+ }
1269
+ catch {
1270
+ // skip
1271
+ }
1272
+ }
1273
+ }
1274
+ await walk(rootPath, '', 0);
1275
+ return { fileCount, testCount };
1276
+ }
1277
+ // ---------------------------------------------------------------------------
1278
+ // Main scan function
1279
+ // ---------------------------------------------------------------------------
1280
+ /**
1281
+ * Scan a codebase directory and produce a compressed schema image.
1282
+ *
1283
+ * The schema image captures language, framework, package manager, database,
1284
+ * ORM, test framework, entry points, project structure, dependencies,
1285
+ * architecture pattern, and key patterns — everything an LLM needs to
1286
+ * understand a codebase without reading all the files.
1287
+ *
1288
+ * @param rootPath - Absolute or relative path to the project root.
1289
+ * @returns A SchemaImage with all detected information.
1290
+ */
1291
+ export async function scanCodebase(rootPath) {
1292
+ const resolvedPath = path.resolve(rootPath);
1293
+ const cached = getCached(resolvedPath);
1294
+ if (cached)
1295
+ return cached;
1296
+ if (!(await pathExists(resolvedPath))) {
1297
+ throw new Error(`Path does not exist: ${resolvedPath}`);
1298
+ }
1299
+ const options = {
1300
+ maxDepth: MAX_SCAN_DEPTH,
1301
+ maxFileSize: MAX_FILE_SIZE,
1302
+ ignoreDirs: IGNORE_DIRS,
1303
+ };
1304
+ // Parse .gitignore
1305
+ let ignorePatterns = [];
1306
+ const gitignore = await safeReadFile(path.join(resolvedPath, '.gitignore'));
1307
+ if (gitignore) {
1308
+ ignorePatterns = parseGitignore(gitignore);
1309
+ }
1310
+ // Also read .dockerignore for additional patterns
1311
+ const dkrignore = await safeReadFile(path.join(resolvedPath, '.dockerignore'));
1312
+ if (dkrignore) {
1313
+ ignorePatterns = ignorePatterns.concat(parseGitignore(dkrignore));
1314
+ }
1315
+ // Detect language, package manager
1316
+ const lang = await detectLanguageAndPackageManager(resolvedPath);
1317
+ let languageLabel = lang.language;
1318
+ if (lang.version) {
1319
+ const displayMap = {
1320
+ typescript: `TypeScript ^${lang.version.replace(/\^|~/g, '')}`,
1321
+ javascript: 'JavaScript',
1322
+ python: `Python ${lang.version}`,
1323
+ rust: `Rust (${lang.version})`,
1324
+ go: lang.version,
1325
+ ruby: `Ruby ${lang.version}`,
1326
+ java: 'Java',
1327
+ kotlin: 'Kotlin',
1328
+ };
1329
+ languageLabel = displayMap[lang.language] || lang.language;
1330
+ }
1331
+ else {
1332
+ const simpleMap = {
1333
+ typescript: 'TypeScript',
1334
+ javascript: 'JavaScript',
1335
+ python: 'Python',
1336
+ rust: 'Rust',
1337
+ go: 'Go',
1338
+ ruby: 'Ruby',
1339
+ java: 'Java',
1340
+ kotlin: 'Kotlin',
1341
+ };
1342
+ languageLabel = simpleMap[lang.language] || lang.language;
1343
+ }
1344
+ // Project name
1345
+ let projectName = path.basename(resolvedPath);
1346
+ const pkg = lang.configFile === 'package.json'
1347
+ ? await safeReadJson(path.join(resolvedPath, 'package.json'))
1348
+ : null;
1349
+ if (pkg && typeof pkg.name === 'string') {
1350
+ projectName = pkg.name;
1351
+ }
1352
+ // Framework
1353
+ const framework = await detectFramework(resolvedPath, lang.language);
1354
+ // Database & ORM
1355
+ const dbOrm = await detectDatabaseAndORM(resolvedPath, lang.language);
1356
+ // Test framework
1357
+ const testFramework = await detectTestFramework(resolvedPath, lang.language);
1358
+ // Entry points
1359
+ const entryPoints = await detectEntryPoints(resolvedPath);
1360
+ // Structure — rootNode.children gives top-level dirs as DirectoryNode[]
1361
+ const rootNode = await scanDirectoryStructure(resolvedPath, '', 0, options, ignorePatterns);
1362
+ const structure = rootNode.children;
1363
+ // Dependencies
1364
+ const dependencies = await extractDependencies(resolvedPath, lang.language);
1365
+ // File counts
1366
+ const { fileCount, testCount } = await countFiles(resolvedPath, options, ignorePatterns);
1367
+ // Architecture
1368
+ const architecture = await detectArchitecture(resolvedPath, rootNode.children);
1369
+ // Key patterns
1370
+ const keyPatterns = await detectKeyPatterns(resolvedPath, lang.language, dependencies, rootNode.children);
1371
+ const image = {
1372
+ projectName,
1373
+ rootPath: resolvedPath,
1374
+ language: languageLabel,
1375
+ framework: framework || languageLabel,
1376
+ packageManager: lang.packageManager,
1377
+ database: dbOrm.database,
1378
+ orm: dbOrm.orm,
1379
+ testFramework,
1380
+ entryPoints,
1381
+ structure,
1382
+ dependencies,
1383
+ architecture,
1384
+ keyPatterns,
1385
+ fileCount,
1386
+ testCount,
1387
+ lastScan: new Date().toISOString(),
1388
+ };
1389
+ setCached(resolvedPath, image);
1390
+ return image;
1391
+ }
1392
+ // ---------------------------------------------------------------------------
1393
+ // Schema image → text compression
1394
+ // ---------------------------------------------------------------------------
1395
+ /**
1396
+ * Compress a SchemaImage into a human-readable text block optimized for LLM
1397
+ * context windows. This is the "schema image" text that should be injected
1398
+ * into system prompts to eliminate cold starts.
1399
+ *
1400
+ * @param image - The schema image from {@link scanCodebase}.
1401
+ * @returns A compressed text block describing the codebase.
1402
+ */
1403
+ export function schemaImageToText(image) {
1404
+ const lines = [];
1405
+ lines.push('[CODEBASE SCHEMA — auto-detected]');
1406
+ lines.push(`Project: ${image.projectName}`);
1407
+ lines.push(`Language: ${image.language}`);
1408
+ if (image.framework && image.framework !== image.language) {
1409
+ lines.push(`Framework: ${image.framework}`);
1410
+ }
1411
+ lines.push(`Package: ${image.packageManager}`);
1412
+ if (image.database)
1413
+ lines.push(`Database: ${image.database}${image.orm ? ` (via ${image.orm})` : ''}`);
1414
+ else if (image.orm)
1415
+ lines.push(`ORM: ${image.orm}`);
1416
+ if (image.testFramework) {
1417
+ const testLabel = image.testCount > 0
1418
+ ? `${image.testFramework} (${image.testCount} test files)`
1419
+ : image.testFramework;
1420
+ lines.push(`Tests: ${testLabel}`);
1421
+ }
1422
+ const epDisplay = image.entryPoints.slice(0, 5);
1423
+ if (epDisplay.length > 0)
1424
+ lines.push(`Entry: ${epDisplay.join(', ')}`);
1425
+ // Structure — flatten to readable format
1426
+ if (image.structure.length > 0) {
1427
+ lines.push('');
1428
+ lines.push('Structure:');
1429
+ const PURPOSE_LABELS = {
1430
+ routes: 'routes',
1431
+ components: 'components',
1432
+ utilities: 'utilities',
1433
+ services: 'services',
1434
+ hooks: 'hooks',
1435
+ models: 'models',
1436
+ controllers: 'controllers',
1437
+ middleware: 'middleware',
1438
+ store: 'state',
1439
+ types: 'types',
1440
+ config: 'config',
1441
+ styles: 'styles',
1442
+ assets: 'static assets',
1443
+ tests: 'tests',
1444
+ 'e2e-tests': 'e2e tests',
1445
+ docs: 'docs',
1446
+ scripts: 'scripts',
1447
+ migrations: 'migrations',
1448
+ infrastructure: 'infra',
1449
+ prisma: 'Prisma schema',
1450
+ api: 'API handlers',
1451
+ db: 'database',
1452
+ root: '',
1453
+ };
1454
+ function flattenStructure(nodes, indent, maxDepth, depth, maxChildren) {
1455
+ if (depth > maxDepth)
1456
+ return;
1457
+ const sorted = nodes
1458
+ .filter((n) => n.name !== '.' && n.name !== 'node_modules' && !n.name.startsWith('.'))
1459
+ .slice(0, depth === 0 ? 15 : 6);
1460
+ for (const node of sorted) {
1461
+ const purposeLabel = PURPOSE_LABELS[node.purpose] || '';
1462
+ const purposeStr = purposeLabel ? ` — ${purposeLabel}` : '';
1463
+ const fileSample = node.notableFiles
1464
+ .filter((f) => f.length < 40)
1465
+ .slice(0, 3)
1466
+ .join(', ');
1467
+ const itemStr = fileSample
1468
+ ? `${node.itemCount} items (${fileSample}${node.notableFiles.length > 3 ? ', ...' : ''})`
1469
+ : `${node.itemCount} items`;
1470
+ lines.push(`${indent} ${node.name}/${purposeStr} — ${itemStr}`);
1471
+ const children = node.children.filter((c) => !c.name.startsWith('.')).slice(0, maxChildren);
1472
+ if (children.length > 0) {
1473
+ flattenStructure(children, indent + ' ', maxDepth - 1, depth + 1, Math.max(2, maxChildren - 2));
1474
+ }
1475
+ }
1476
+ }
1477
+ flattenStructure(image.structure, '', 3, 0, 6);
1478
+ }
1479
+ // Key patterns
1480
+ if (image.keyPatterns.length > 0) {
1481
+ lines.push('');
1482
+ lines.push('Key patterns:');
1483
+ for (const pat of image.keyPatterns.slice(0, 10)) {
1484
+ lines.push(` - ${pat}`);
1485
+ }
1486
+ }
1487
+ // Dependencies summary
1488
+ if (image.dependencies.length > 0) {
1489
+ const notable = filterNotableDeps(image.dependencies, image.language);
1490
+ if (notable.length > 0) {
1491
+ lines.push('');
1492
+ lines.push(`Dependencies: ${notable.join(', ')}`);
1493
+ }
1494
+ }
1495
+ lines.push('');
1496
+ const archLabel = {
1497
+ mvc: 'MVC',
1498
+ microservices: 'Microservices',
1499
+ monolith: 'Monolith',
1500
+ monorepo: 'Monorepo',
1501
+ layered: 'Layered',
1502
+ serverless: 'Serverless',
1503
+ unknown: 'Unknown',
1504
+ };
1505
+ lines.push(`Files: ${image.fileCount} total${image.testCount > 0 ? `, ${image.testCount} test files` : ''}`);
1506
+ lines.push(`Architecture: ${archLabel[image.architecture] || image.architecture}`);
1507
+ return lines.join('\n');
1508
+ }
1509
+ /**
1510
+ * Filter dependencies to only the notable ones (framework, db, auth, etc.)
1511
+ * to keep the output compact.
1512
+ */
1513
+ function filterNotableDeps(deps, language) {
1514
+ const notableNames = new Set([
1515
+ 'next', 'nuxt', 'react', 'react-dom', 'vue', 'angular',
1516
+ 'express', 'fastify', 'koa', 'nest', '@nestjs/core',
1517
+ 'prisma', '@prisma/client', 'typeorm', 'sequelize', 'mongoose', 'drizzle-orm',
1518
+ 'pg', 'mysql2', 'mongodb', 'redis', 'ioredis',
1519
+ 'jsonwebtoken', 'jose', 'next-auth', '@auth/core',
1520
+ 'zod', 'yup', 'joi',
1521
+ 'stripe', '@stripe/stripe-js',
1522
+ 'tailwindcss', '@tailwindcss/typography',
1523
+ 'jest', 'vitest', '@playwright/test', 'cypress',
1524
+ 'zustand', 'jotai', 'redux', '@reduxjs/toolkit',
1525
+ 'socket.io', 'ws',
1526
+ 'fastapi', 'django', 'flask',
1527
+ 'axum', 'actix-web', 'rocket',
1528
+ 'gin', 'echo', 'fiber',
1529
+ 'rails',
1530
+ ]);
1531
+ const result = [];
1532
+ for (const dep of deps) {
1533
+ const baseName = dep.name.replace(/^@/, '').split('/')[0];
1534
+ if (notableNames.has(dep.name) || notableNames.has(baseName)) {
1535
+ const ver = dep.version.replace(/^\^|~/, '');
1536
+ result.push(`${dep.name}@${ver}`);
1537
+ }
1538
+ }
1539
+ return result.slice(0, 15);
1540
+ }
1541
+ // ---------------------------------------------------------------------------
1542
+ // File watcher
1543
+ // ---------------------------------------------------------------------------
1544
+ /**
1545
+ * Watch a codebase directory for changes and re-scan automatically.
1546
+ *
1547
+ * Uses Node.js built-in `fs.watch` with a 500ms debounce. If chokidar is
1548
+ * available as an optional dependency, it will be preferred automatically
1549
+ * (more reliable cross-platform watching).
1550
+ *
1551
+ * @param rootPath - Absolute or relative path to the project root.
1552
+ * @param onChange - Callback invoked with the updated SchemaImage on changes.
1553
+ * @returns An object with a `stop()` method to stop watching.
1554
+ */
1555
+ export function watchCodebase(rootPath, onChange) {
1556
+ const resolvedPath = path.resolve(rootPath);
1557
+ let watcher = null;
1558
+ let debounceTimer;
1559
+ let stopped = false;
1560
+ const handleChange = () => {
1561
+ if (stopped)
1562
+ return;
1563
+ if (debounceTimer)
1564
+ clearTimeout(debounceTimer);
1565
+ debounceTimer = setTimeout(async () => {
1566
+ if (stopped)
1567
+ return;
1568
+ try {
1569
+ clearCache(); // invalidate cache to force fresh scan
1570
+ const image = await scanCodebase(resolvedPath);
1571
+ onChange(image);
1572
+ }
1573
+ catch {
1574
+ // Silently ignore scan errors during watch
1575
+ }
1576
+ }, 500);
1577
+ };
1578
+ // Try to require chokidar optionally
1579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1580
+ let chokidar;
1581
+ try {
1582
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1583
+ chokidar = require('chokidar');
1584
+ }
1585
+ catch {
1586
+ // chokidar not available, fall back to fs.watch
1587
+ }
1588
+ if (chokidar) {
1589
+ const cw = chokidar.watch(resolvedPath, {
1590
+ ignored: [
1591
+ /(^|[\/\\])\./,
1592
+ '**/node_modules/**',
1593
+ '**/.git/**',
1594
+ '**/dist/**',
1595
+ '**/build/**',
1596
+ '**/target/**',
1597
+ ],
1598
+ persistent: true,
1599
+ ignoreInitial: true,
1600
+ });
1601
+ cw.on('add', handleChange);
1602
+ cw.on('change', handleChange);
1603
+ cw.on('unlink', handleChange);
1604
+ cw.on('addDir', handleChange);
1605
+ cw.on('unlinkDir', handleChange);
1606
+ return {
1607
+ stop: () => {
1608
+ stopped = true;
1609
+ cw.close();
1610
+ if (debounceTimer)
1611
+ clearTimeout(debounceTimer);
1612
+ },
1613
+ };
1614
+ }
1615
+ // Fallback to fs.watch
1616
+ try {
1617
+ watcher = fsWatch(resolvedPath, { recursive: true }, (_eventType, _filename) => {
1618
+ handleChange();
1619
+ });
1620
+ }
1621
+ catch {
1622
+ // Some systems don't support recursive fs.watch
1623
+ try {
1624
+ watcher = fsWatch(resolvedPath, (_eventType, _filename) => {
1625
+ handleChange();
1626
+ });
1627
+ }
1628
+ catch {
1629
+ // Fallback: poll every 10 seconds
1630
+ const interval = setInterval(() => handleChange(), 10_000);
1631
+ return {
1632
+ stop: () => {
1633
+ stopped = true;
1634
+ clearInterval(interval);
1635
+ if (debounceTimer)
1636
+ clearTimeout(debounceTimer);
1637
+ },
1638
+ };
1639
+ }
1640
+ }
1641
+ return {
1642
+ stop: () => {
1643
+ stopped = true;
1644
+ if (watcher)
1645
+ watcher.close();
1646
+ if (debounceTimer)
1647
+ clearTimeout(debounceTimer);
1648
+ },
1649
+ };
1650
+ }
1651
+ // ---------------------------------------------------------------------------
1652
+ // Project size estimation
1653
+ // ---------------------------------------------------------------------------
1654
+ /**
1655
+ * Estimate the size of a project (file count and total lines of code).
1656
+ *
1657
+ * Attempts to use `cloc` if available on the system, otherwise falls back
1658
+ * to a manual line count via Node.js built-in `fs` operations. The manual
1659
+ * fallback caps at 10,000 files for performance.
1660
+ *
1661
+ * @param rootPath - Absolute or relative path to the project root.
1662
+ * @returns A ProjectSize with file count and approximate line count.
1663
+ */
1664
+ export async function estimateProjectSize(rootPath) {
1665
+ const resolvedPath = path.resolve(rootPath);
1666
+ // Try cloc first (fast, accurate)
1667
+ try {
1668
+ const output = execSync(`cloc --json "${resolvedPath}" 2>/dev/null`, {
1669
+ timeout: 30_000,
1670
+ encoding: 'utf-8',
1671
+ });
1672
+ const data = JSON.parse(output);
1673
+ if (data?.SUM) {
1674
+ return { files: data.SUM.nFiles || 0, lines: data.SUM.code || 0 };
1675
+ }
1676
+ }
1677
+ catch {
1678
+ // cloc not available, fall back to manual count
1679
+ }
1680
+ // Manual fallback
1681
+ const options = {
1682
+ maxDepth: 20,
1683
+ maxFileSize: MAX_FILE_SIZE,
1684
+ ignoreDirs: IGNORE_DIRS,
1685
+ };
1686
+ let ignorePatterns = [];
1687
+ const gitignore = await safeReadFile(path.join(resolvedPath, '.gitignore'));
1688
+ if (gitignore)
1689
+ ignorePatterns = parseGitignore(gitignore);
1690
+ let files = 0;
1691
+ let lines = 0;
1692
+ async function walk(dirPath, relPath, depth) {
1693
+ if (depth > 12 || files > 10_000)
1694
+ return;
1695
+ let entries;
1696
+ try {
1697
+ entries = await fsp.readdir(dirPath);
1698
+ }
1699
+ catch {
1700
+ return;
1701
+ }
1702
+ for (const entry of entries) {
1703
+ if (files > 10_000)
1704
+ return;
1705
+ const full = path.join(dirPath, entry);
1706
+ const rel = relPath ? path.join(relPath, entry) : entry;
1707
+ if (shouldIgnorePath(rel, ignorePatterns, options))
1708
+ continue;
1709
+ try {
1710
+ const stat = await fsp.stat(full);
1711
+ if (stat.isDirectory()) {
1712
+ await walk(full, rel, depth + 1);
1713
+ }
1714
+ else if (stat.isFile()) {
1715
+ files++;
1716
+ const content = await safeReadFile(full);
1717
+ if (content) {
1718
+ lines += content.split('\n').length;
1719
+ }
1720
+ }
1721
+ }
1722
+ catch {
1723
+ // skip
1724
+ }
1725
+ }
1726
+ }
1727
+ await walk(resolvedPath, '', 0);
1728
+ return { files, lines };
1729
+ }
1730
+ //# sourceMappingURL=codebase-scanner.js.map