@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.
- package/bin/aria.js +168 -0
- package/dist/aria-connector/src/auth-commands.d.ts +28 -0
- package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
- package/dist/aria-connector/src/auth-commands.js +129 -0
- package/dist/aria-connector/src/auth-commands.js.map +1 -0
- package/dist/aria-connector/src/auth.d.ts +12 -0
- package/dist/aria-connector/src/auth.d.ts.map +1 -0
- package/dist/aria-connector/src/auth.js +31 -0
- package/dist/aria-connector/src/auth.js.map +1 -0
- package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
- package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
- package/dist/aria-connector/src/auto-mcp.js +994 -0
- package/dist/aria-connector/src/auto-mcp.js.map +1 -0
- package/dist/aria-connector/src/chat.d.ts +21 -0
- package/dist/aria-connector/src/chat.d.ts.map +1 -0
- package/dist/aria-connector/src/chat.js +332 -0
- package/dist/aria-connector/src/chat.js.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.js +6 -0
- package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
- package/dist/aria-connector/src/cognition-log.d.ts +17 -0
- package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
- package/dist/aria-connector/src/cognition-log.js +19 -0
- package/dist/aria-connector/src/cognition-log.js.map +1 -0
- package/dist/aria-connector/src/config.d.ts +41 -0
- package/dist/aria-connector/src/config.d.ts.map +1 -0
- package/dist/aria-connector/src/config.js +50 -0
- package/dist/aria-connector/src/config.js.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.js +204 -0
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.js +63 -0
- package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.js +102 -0
- package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
- package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
- package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/shell.js +58 -0
- package/dist/aria-connector/src/connectors/shell.js.map +1 -0
- package/dist/aria-connector/src/garden-client.d.ts +19 -0
- package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-client.js +85 -0
- package/dist/aria-connector/src/garden-client.js.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.js +43 -0
- package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
- package/dist/aria-connector/src/harness-client.d.ts +166 -0
- package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
- package/dist/aria-connector/src/harness-client.js +344 -0
- package/dist/aria-connector/src/harness-client.js.map +1 -0
- package/dist/aria-connector/src/hive-client.d.ts +32 -0
- package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
- package/dist/aria-connector/src/hive-client.js +69 -0
- package/dist/aria-connector/src/hive-client.js.map +1 -0
- package/dist/aria-connector/src/index.d.ts +19 -0
- package/dist/aria-connector/src/index.d.ts.map +1 -0
- package/dist/aria-connector/src/index.js +13 -0
- package/dist/aria-connector/src/index.js.map +1 -0
- package/dist/aria-connector/src/install-hooks.d.ts +18 -0
- package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
- package/dist/aria-connector/src/install-hooks.js +224 -0
- package/dist/aria-connector/src/install-hooks.js.map +1 -0
- package/dist/aria-connector/src/model-context.d.ts +8 -0
- package/dist/aria-connector/src/model-context.d.ts.map +1 -0
- package/dist/aria-connector/src/model-context.js +83 -0
- package/dist/aria-connector/src/model-context.js.map +1 -0
- package/dist/aria-connector/src/persona.d.ts +27 -0
- package/dist/aria-connector/src/persona.d.ts.map +1 -0
- package/dist/aria-connector/src/persona.js +86 -0
- package/dist/aria-connector/src/persona.js.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.js +92 -0
- package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.js +28 -0
- package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
- package/dist/aria-connector/src/providers/google.d.ts +3 -0
- package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/google.js +38 -0
- package/dist/aria-connector/src/providers/google.js.map +1 -0
- package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
- package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/ollama.js +28 -0
- package/dist/aria-connector/src/providers/ollama.js.map +1 -0
- package/dist/aria-connector/src/providers/openai.d.ts +4 -0
- package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openai.js +84 -0
- package/dist/aria-connector/src/providers/openai.js.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.js +30 -0
- package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
- package/dist/aria-connector/src/providers/types.d.ts +20 -0
- package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/types.js +2 -0
- package/dist/aria-connector/src/providers/types.js.map +1 -0
- package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
- package/dist/aria-connector/src/setup-wizard.js +140 -0
- package/dist/aria-connector/src/setup-wizard.js.map +1 -0
- package/dist/aria-connector/src/types.d.ts +30 -0
- package/dist/aria-connector/src/types.d.ts.map +1 -0
- package/dist/aria-connector/src/types.js +5 -0
- package/dist/aria-connector/src/types.js.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
- package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
- package/dist/cli-0.2.0.tgz +0 -0
- package/dist/install.sh +13 -0
- package/hooks/aria-harness-via-sdk.mjs +317 -0
- package/hooks/aria-pre-tool-gate.mjs +596 -0
- package/hooks/aria-preprompt-consult.mjs +175 -0
- package/hooks/aria-stop-gate.mjs +222 -0
- package/package.json +47 -0
- package/src/__tests__/auth-commands.test.ts +132 -0
- package/src/auth-commands.ts +175 -0
- package/src/auth.ts +33 -0
- package/src/auto-mcp.ts +1172 -0
- package/src/chat.ts +387 -0
- package/src/codebase-scanner.ts +18 -0
- package/src/cognition-log.ts +30 -0
- package/src/config.ts +94 -0
- package/src/connectors/claude-code.ts +213 -0
- package/src/connectors/cursor.ts +75 -0
- package/src/connectors/opencode.ts +115 -0
- package/src/connectors/shell.ts +72 -0
- package/src/garden-client.ts +98 -0
- package/src/garden-control-plane.ts +108 -0
- package/src/harness-client.ts +454 -0
- package/src/hive-client.ts +104 -0
- package/src/index.ts +26 -0
- package/src/install-hooks.ts +259 -0
- package/src/model-context.ts +88 -0
- package/src/persona.ts +113 -0
- package/src/providers/anthropic.ts +120 -0
- package/src/providers/deepseek.ts +40 -0
- package/src/providers/google.ts +57 -0
- package/src/providers/ollama.ts +43 -0
- package/src/providers/openai.ts +108 -0
- package/src/providers/openrouter.ts +42 -0
- package/src/providers/types.ts +35 -0
- package/src/setup-wizard.ts +177 -0
- 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
|