@theihtisham/agent-shadow-brain 1.2.0 → 3.0.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/README.md +837 -73
- package/dist/adapters/aider.d.ts +11 -0
- package/dist/adapters/aider.d.ts.map +1 -0
- package/dist/adapters/aider.js +149 -0
- package/dist/adapters/aider.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/roo-code.d.ts +14 -0
- package/dist/adapters/roo-code.d.ts.map +1 -0
- package/dist/adapters/roo-code.js +186 -0
- package/dist/adapters/roo-code.js.map +1 -0
- package/dist/brain/accessibility-checker.d.ts +10 -0
- package/dist/brain/accessibility-checker.d.ts.map +1 -0
- package/dist/brain/accessibility-checker.js +379 -0
- package/dist/brain/accessibility-checker.js.map +1 -0
- package/dist/brain/adr-engine.d.ts +58 -0
- package/dist/brain/adr-engine.d.ts.map +1 -0
- package/dist/brain/adr-engine.js +400 -0
- package/dist/brain/adr-engine.js.map +1 -0
- package/dist/brain/api-contract-analyzer.d.ts +19 -0
- package/dist/brain/api-contract-analyzer.d.ts.map +1 -0
- package/dist/brain/api-contract-analyzer.js +251 -0
- package/dist/brain/api-contract-analyzer.js.map +1 -0
- package/dist/brain/ast-analyzer.d.ts +23 -0
- package/dist/brain/ast-analyzer.d.ts.map +1 -0
- package/dist/brain/ast-analyzer.js +462 -0
- package/dist/brain/ast-analyzer.js.map +1 -0
- package/dist/brain/code-age-analyzer.d.ts +11 -0
- package/dist/brain/code-age-analyzer.d.ts.map +1 -0
- package/dist/brain/code-age-analyzer.js +152 -0
- package/dist/brain/code-age-analyzer.js.map +1 -0
- package/dist/brain/code-similarity.d.ts +43 -0
- package/dist/brain/code-similarity.d.ts.map +1 -0
- package/dist/brain/code-similarity.js +227 -0
- package/dist/brain/code-similarity.js.map +1 -0
- package/dist/brain/config-drift-detector.d.ts +13 -0
- package/dist/brain/config-drift-detector.d.ts.map +1 -0
- package/dist/brain/config-drift-detector.js +198 -0
- package/dist/brain/config-drift-detector.js.map +1 -0
- package/dist/brain/context-completion.d.ts +39 -0
- package/dist/brain/context-completion.d.ts.map +1 -0
- package/dist/brain/context-completion.js +851 -0
- package/dist/brain/context-completion.js.map +1 -0
- package/dist/brain/dead-code-eliminator.d.ts +16 -0
- package/dist/brain/dead-code-eliminator.d.ts.map +1 -0
- package/dist/brain/dead-code-eliminator.js +359 -0
- package/dist/brain/dead-code-eliminator.js.map +1 -0
- package/dist/brain/dependency-graph.d.ts +35 -0
- package/dist/brain/dependency-graph.d.ts.map +1 -0
- package/dist/brain/dependency-graph.js +310 -0
- package/dist/brain/dependency-graph.js.map +1 -0
- package/dist/brain/env-analyzer.d.ts +13 -0
- package/dist/brain/env-analyzer.d.ts.map +1 -0
- package/dist/brain/env-analyzer.js +277 -0
- package/dist/brain/env-analyzer.js.map +1 -0
- package/dist/brain/i18n-detector.d.ts +12 -0
- package/dist/brain/i18n-detector.d.ts.map +1 -0
- package/dist/brain/i18n-detector.js +242 -0
- package/dist/brain/i18n-detector.js.map +1 -0
- package/dist/brain/learning-engine.d.ts +54 -0
- package/dist/brain/learning-engine.d.ts.map +1 -0
- package/dist/brain/learning-engine.js +855 -0
- package/dist/brain/learning-engine.js.map +1 -0
- package/dist/brain/license-compliance.d.ts +13 -0
- package/dist/brain/license-compliance.d.ts.map +1 -0
- package/dist/brain/license-compliance.js +213 -0
- package/dist/brain/license-compliance.js.map +1 -0
- package/dist/brain/llm-client.d.ts.map +1 -1
- package/dist/brain/llm-client.js +3 -0
- package/dist/brain/llm-client.js.map +1 -1
- package/dist/brain/mcp-server.d.ts +30 -0
- package/dist/brain/mcp-server.d.ts.map +1 -0
- package/dist/brain/mcp-server.js +408 -0
- package/dist/brain/mcp-server.js.map +1 -0
- package/dist/brain/multi-project.d.ts +13 -0
- package/dist/brain/multi-project.d.ts.map +1 -0
- package/dist/brain/multi-project.js +163 -0
- package/dist/brain/multi-project.js.map +1 -0
- package/dist/brain/mutation-advisor.d.ts +11 -0
- package/dist/brain/mutation-advisor.d.ts.map +1 -0
- package/dist/brain/mutation-advisor.js +154 -0
- package/dist/brain/mutation-advisor.js.map +1 -0
- package/dist/brain/neural-mesh.d.ts +69 -0
- package/dist/brain/neural-mesh.d.ts.map +1 -0
- package/dist/brain/neural-mesh.js +677 -0
- package/dist/brain/neural-mesh.js.map +1 -0
- package/dist/brain/orchestrator.d.ts +159 -2
- package/dist/brain/orchestrator.d.ts.map +1 -1
- package/dist/brain/orchestrator.js +478 -0
- package/dist/brain/orchestrator.js.map +1 -1
- package/dist/brain/perf-profiler.d.ts +14 -0
- package/dist/brain/perf-profiler.d.ts.map +1 -0
- package/dist/brain/perf-profiler.js +289 -0
- package/dist/brain/perf-profiler.js.map +1 -0
- package/dist/brain/semantic-analyzer.d.ts +46 -0
- package/dist/brain/semantic-analyzer.d.ts.map +1 -0
- package/dist/brain/semantic-analyzer.js +496 -0
- package/dist/brain/semantic-analyzer.js.map +1 -0
- package/dist/brain/team-mode.d.ts +27 -0
- package/dist/brain/team-mode.d.ts.map +1 -0
- package/dist/brain/team-mode.js +262 -0
- package/dist/brain/team-mode.js.map +1 -0
- package/dist/brain/type-safety.d.ts +13 -0
- package/dist/brain/type-safety.d.ts.map +1 -0
- package/dist/brain/type-safety.js +217 -0
- package/dist/brain/type-safety.js.map +1 -0
- package/dist/cli.js +998 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +379 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
// src/brain/context-completion.ts — Context Completion Engine for Shadow Brain
|
|
2
|
+
// Analyzes project to build knowledge, identify gaps, and persist context.
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
/** Read directory entries safely, returning empty on failure. */
|
|
6
|
+
async function readDirSafe(dirPath) {
|
|
7
|
+
try {
|
|
8
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
9
|
+
return entries.map((e) => ({
|
|
10
|
+
name: e.name,
|
|
11
|
+
fullPath: path.join(dirPath, e.name),
|
|
12
|
+
isDir: e.isDirectory(),
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Read a text file safely, returning null on failure. */
|
|
20
|
+
async function readFileSafe(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Check whether a file exists. */
|
|
29
|
+
async function fileExists(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fs.stat(filePath);
|
|
32
|
+
return stat.isFile();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Check whether a directory exists. */
|
|
39
|
+
async function dirExists(dirPath) {
|
|
40
|
+
try {
|
|
41
|
+
const stat = await fs.stat(dirPath);
|
|
42
|
+
return stat.isDirectory();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// ── ContextCompletionEngine ──────────────────────────────────────────────────
|
|
49
|
+
export class ContextCompletionEngine {
|
|
50
|
+
constructor(projectDir) {
|
|
51
|
+
this.projectDir = projectDir;
|
|
52
|
+
}
|
|
53
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Analyze the project directory to build a comprehensive ProjectKnowledge
|
|
56
|
+
* object containing name, conventions, architecture, patterns, and deps.
|
|
57
|
+
*/
|
|
58
|
+
async buildKnowledge() {
|
|
59
|
+
const [name, conventions, archResult, patternsResult, deps] = await Promise.all([
|
|
60
|
+
this.detectProjectName(),
|
|
61
|
+
this.detectConventions(),
|
|
62
|
+
this.detectArchitecture(),
|
|
63
|
+
this.detectPatterns(),
|
|
64
|
+
this.detectDependencies(),
|
|
65
|
+
]);
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
conventions,
|
|
69
|
+
architecture: archResult.description,
|
|
70
|
+
commonPatterns: patternsResult.common,
|
|
71
|
+
avoidPatterns: patternsResult.avoid,
|
|
72
|
+
dependencies: deps,
|
|
73
|
+
lastUpdated: new Date(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Persist the knowledge object to `.shadow-brain/knowledge.json` inside
|
|
78
|
+
* the project directory.
|
|
79
|
+
*/
|
|
80
|
+
async saveKnowledge(knowledge) {
|
|
81
|
+
const brainDir = path.join(this.projectDir, '.shadow-brain');
|
|
82
|
+
await fs.mkdir(brainDir, { recursive: true });
|
|
83
|
+
const filePath = path.join(brainDir, 'knowledge.json');
|
|
84
|
+
const data = JSON.stringify(knowledge, null, 2);
|
|
85
|
+
await fs.writeFile(filePath, data, 'utf-8');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Identify missing project context items (README, .gitignore, tsconfig,
|
|
89
|
+
* CI/CD configs, etc.) and return them as prioritised BrainInsight array.
|
|
90
|
+
*/
|
|
91
|
+
async getContextGaps(knowledge) {
|
|
92
|
+
const gaps = [];
|
|
93
|
+
const now = new Date();
|
|
94
|
+
// 1. No README.md → critical
|
|
95
|
+
if (!(await this.hasFile('README.md')) && !(await this.hasFile('README'))) {
|
|
96
|
+
gaps.push({
|
|
97
|
+
type: 'warning',
|
|
98
|
+
priority: 'critical',
|
|
99
|
+
title: 'Missing README.md',
|
|
100
|
+
content: 'The project has no README.md file. A README is essential for onboarding, documentation, and discoverability. Add one with a project description, setup instructions, and usage examples.',
|
|
101
|
+
timestamp: now,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// 2. No .gitignore → critical
|
|
105
|
+
if (!(await this.hasFile('.gitignore'))) {
|
|
106
|
+
gaps.push({
|
|
107
|
+
type: 'warning',
|
|
108
|
+
priority: 'critical',
|
|
109
|
+
title: 'Missing .gitignore',
|
|
110
|
+
content: 'No .gitignore file found. Without it, build artifacts, node_modules, secrets, and editor configs may be committed accidentally. Create a .gitignore appropriate for this project type.',
|
|
111
|
+
timestamp: now,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// 3. No CONTRIBUTING.md in open-source projects → medium
|
|
115
|
+
if (!(await this.hasFile('CONTRIBUTING.md'))) {
|
|
116
|
+
// Heuristic: if there's a LICENSE file, assume open-source
|
|
117
|
+
const hasLicense = (await this.hasFile('LICENSE')) ||
|
|
118
|
+
(await this.hasFile('LICENSE.md')) ||
|
|
119
|
+
(await this.hasFile('LICENSE.txt'));
|
|
120
|
+
if (hasLicense) {
|
|
121
|
+
gaps.push({
|
|
122
|
+
type: 'suggestion',
|
|
123
|
+
priority: 'medium',
|
|
124
|
+
title: 'Missing CONTRIBUTING.md',
|
|
125
|
+
content: 'This project has a license (suggesting open-source) but no CONTRIBUTING.md. Adding contribution guidelines helps the community participate effectively.',
|
|
126
|
+
timestamp: now,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 4. No tsconfig.json for TS files → high
|
|
131
|
+
const hasTsFiles = await this.hasFileExtension('.ts');
|
|
132
|
+
if (hasTsFiles && !(await this.hasFile('tsconfig.json'))) {
|
|
133
|
+
gaps.push({
|
|
134
|
+
type: 'warning',
|
|
135
|
+
priority: 'high',
|
|
136
|
+
title: 'Missing tsconfig.json',
|
|
137
|
+
content: 'TypeScript files were found but no tsconfig.json exists. A tsconfig is required for proper type-checking, module resolution, and compiler options.',
|
|
138
|
+
timestamp: now,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// 5. No .env.example when .env exists → high (security)
|
|
142
|
+
if (await this.hasFile('.env')) {
|
|
143
|
+
if (!(await this.hasFile('.env.example')) &&
|
|
144
|
+
!(await this.hasFile('.env.sample')) &&
|
|
145
|
+
!(await this.hasFile('.env.template'))) {
|
|
146
|
+
gaps.push({
|
|
147
|
+
type: 'warning',
|
|
148
|
+
priority: 'high',
|
|
149
|
+
title: 'Missing .env.example (security risk)',
|
|
150
|
+
content: 'A .env file exists but there is no .env.example. Collaborators may not know which environment variables are required. Create .env.example with dummy values so the project is documented and secrets are not accidentally shared.',
|
|
151
|
+
timestamp: now,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 6. No LICENSE file → medium
|
|
156
|
+
if (!(await this.hasFile('LICENSE')) &&
|
|
157
|
+
!(await this.hasFile('LICENSE.md')) &&
|
|
158
|
+
!(await this.hasFile('LICENSE.txt'))) {
|
|
159
|
+
gaps.push({
|
|
160
|
+
type: 'suggestion',
|
|
161
|
+
priority: 'medium',
|
|
162
|
+
title: 'Missing LICENSE file',
|
|
163
|
+
content: 'No license file detected. Without a license, the project defaults to full copyright reservation, which may prevent others from using or contributing. Add a LICENSE file (e.g., MIT, Apache-2.0).',
|
|
164
|
+
timestamp: now,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// 7. No CI/CD config → low
|
|
168
|
+
const hasCI = (await this.hasFile('.github/workflows')) ||
|
|
169
|
+
(await this.hasFile('.gitlab-ci.yml')) ||
|
|
170
|
+
(await this.hasFile('.circleci')) ||
|
|
171
|
+
(await this.hasFile('Jenkinsfile')) ||
|
|
172
|
+
(await this.hasFile('azure-pipelines.yml'));
|
|
173
|
+
if (!hasCI) {
|
|
174
|
+
gaps.push({
|
|
175
|
+
type: 'suggestion',
|
|
176
|
+
priority: 'low',
|
|
177
|
+
title: 'No CI/CD configuration',
|
|
178
|
+
content: 'No CI/CD pipeline configuration found. Adding automated testing and deployment (e.g., GitHub Actions) catches regressions early and improves code quality.',
|
|
179
|
+
timestamp: now,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// 8. Missing type definitions (no @types packages for TS) → medium
|
|
183
|
+
if (hasTsFiles) {
|
|
184
|
+
const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
|
|
185
|
+
if (pkgContent) {
|
|
186
|
+
try {
|
|
187
|
+
const pkg = JSON.parse(pkgContent);
|
|
188
|
+
const allDeps = {
|
|
189
|
+
...(pkg.dependencies || {}),
|
|
190
|
+
...(pkg.devDependencies || {}),
|
|
191
|
+
};
|
|
192
|
+
// Check for common libraries that usually need @types
|
|
193
|
+
const typeCandidates = {
|
|
194
|
+
express: '@types/express',
|
|
195
|
+
node: '@types/node',
|
|
196
|
+
jest: '@types/jest',
|
|
197
|
+
react: '@types/react',
|
|
198
|
+
lodash: '@types/lodash',
|
|
199
|
+
mongoose: '@types/mongoose',
|
|
200
|
+
cors: '@types/cors',
|
|
201
|
+
};
|
|
202
|
+
const missingTypes = [];
|
|
203
|
+
for (const [dep, typesPkg] of Object.entries(typeCandidates)) {
|
|
204
|
+
if (allDeps[dep] && !allDeps[typesPkg]) {
|
|
205
|
+
missingTypes.push(typesPkg);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (missingTypes.length > 0) {
|
|
209
|
+
gaps.push({
|
|
210
|
+
type: 'suggestion',
|
|
211
|
+
priority: 'medium',
|
|
212
|
+
title: 'Missing @types packages',
|
|
213
|
+
content: `The following type definition packages are recommended but not installed: ${missingTypes.join(', ')}. Install them with: npm i -D ${missingTypes.join(' ')}`,
|
|
214
|
+
timestamp: now,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Invalid package.json, skip this check
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// 9. No test configuration → medium
|
|
224
|
+
const hasTestConfig = (await this.hasFile('jest.config.js')) ||
|
|
225
|
+
(await this.hasFile('jest.config.ts')) ||
|
|
226
|
+
(await this.hasFile('vitest.config.ts')) ||
|
|
227
|
+
(await this.hasFile('vitest.config.js')) ||
|
|
228
|
+
(await this.hasFile('mocha.opts')) ||
|
|
229
|
+
(await this.hasFile('.mocharc.yml')) ||
|
|
230
|
+
(await this.hasFile('.mocharc.json')) ||
|
|
231
|
+
(await this.hasFile('karma.conf.js')) ||
|
|
232
|
+
(await this.hasFile('pytest.ini')) ||
|
|
233
|
+
(await this.hasFile('pyproject.toml'));
|
|
234
|
+
const hasTestDir = (await dirExists(path.join(this.projectDir, 'test'))) ||
|
|
235
|
+
(await dirExists(path.join(this.projectDir, 'tests'))) ||
|
|
236
|
+
(await dirExists(path.join(this.projectDir, '__tests__'))) ||
|
|
237
|
+
(await dirExists(path.join(this.projectDir, 'spec')));
|
|
238
|
+
if (!hasTestConfig && !hasTestDir) {
|
|
239
|
+
gaps.push({
|
|
240
|
+
type: 'suggestion',
|
|
241
|
+
priority: 'medium',
|
|
242
|
+
title: 'No test configuration found',
|
|
243
|
+
content: 'No test framework configuration or test directories detected. Adding tests (unit, integration) is critical for maintainability and confidence in refactoring.',
|
|
244
|
+
timestamp: now,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// 10. Empty conventions → low
|
|
248
|
+
if (knowledge.conventions.length === 0) {
|
|
249
|
+
gaps.push({
|
|
250
|
+
type: 'suggestion',
|
|
251
|
+
priority: 'low',
|
|
252
|
+
title: 'No coding conventions detected',
|
|
253
|
+
content: 'No linting, formatting, or style configuration files were found. Consider adding ESLint, Prettier, or an EditorConfig to enforce consistent code style across the project.',
|
|
254
|
+
timestamp: now,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return gaps;
|
|
258
|
+
}
|
|
259
|
+
// ── Private: Name Detection ───────────────────────────────────────────────
|
|
260
|
+
async detectProjectName() {
|
|
261
|
+
// Try package.json first
|
|
262
|
+
const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
|
|
263
|
+
if (pkgContent) {
|
|
264
|
+
try {
|
|
265
|
+
const pkg = JSON.parse(pkgContent);
|
|
266
|
+
if (pkg.name && typeof pkg.name === 'string') {
|
|
267
|
+
// Strip scope prefix: @scope/name → name
|
|
268
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Fall through
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Fallback to directory name
|
|
276
|
+
return path.basename(this.projectDir);
|
|
277
|
+
}
|
|
278
|
+
// ── Private: Convention Detection ─────────────────────────────────────────
|
|
279
|
+
async detectConventions() {
|
|
280
|
+
const conventions = [];
|
|
281
|
+
// ESLint
|
|
282
|
+
const eslintFiles = [
|
|
283
|
+
'.eslintrc',
|
|
284
|
+
'.eslintrc.js',
|
|
285
|
+
'.eslintrc.json',
|
|
286
|
+
'.eslintrc.yml',
|
|
287
|
+
'.eslintrc.yaml',
|
|
288
|
+
'eslint.config.js',
|
|
289
|
+
'eslint.config.mjs',
|
|
290
|
+
'eslint.config.ts',
|
|
291
|
+
];
|
|
292
|
+
for (const f of eslintFiles) {
|
|
293
|
+
const content = await readFileSafe(path.join(this.projectDir, f));
|
|
294
|
+
if (content) {
|
|
295
|
+
conventions.push(this.summarizeEslint(content, f));
|
|
296
|
+
break; // Only one ESLint config matters
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Also check package.json for eslintConfig
|
|
300
|
+
const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
|
|
301
|
+
if (pkgContent && !conventions.some((c) => c.startsWith('ESLint'))) {
|
|
302
|
+
try {
|
|
303
|
+
const pkg = JSON.parse(pkgContent);
|
|
304
|
+
if (pkg.eslintConfig) {
|
|
305
|
+
conventions.push(this.summarizeEslint(JSON.stringify(pkg.eslintConfig), 'package.json#eslintConfig'));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// skip
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Prettier
|
|
313
|
+
const prettierFiles = ['.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yml', '.prettierrc.yaml'];
|
|
314
|
+
for (const f of prettierFiles) {
|
|
315
|
+
const content = await readFileSafe(path.join(this.projectDir, f));
|
|
316
|
+
if (content) {
|
|
317
|
+
conventions.push(this.summarizePrettier(content, f));
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Check package.json for prettier config
|
|
322
|
+
if (pkgContent && !conventions.some((c) => c.startsWith('Prettier'))) {
|
|
323
|
+
try {
|
|
324
|
+
const pkg = JSON.parse(pkgContent);
|
|
325
|
+
if (pkg.prettier) {
|
|
326
|
+
conventions.push(this.summarizePrettier(JSON.stringify(pkg.prettier), 'package.json#prettier'));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// skip
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// EditorConfig
|
|
334
|
+
if (await fileExists(path.join(this.projectDir, '.editorconfig'))) {
|
|
335
|
+
const content = await readFileSafe(path.join(this.projectDir, '.editorconfig'));
|
|
336
|
+
if (content) {
|
|
337
|
+
conventions.push(this.summarizeEditorConfig(content));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// tsconfig patterns
|
|
341
|
+
const tsconfigContent = await readFileSafe(path.join(this.projectDir, 'tsconfig.json'));
|
|
342
|
+
if (tsconfigContent) {
|
|
343
|
+
conventions.push(this.summarizeTsconfig(tsconfigContent));
|
|
344
|
+
}
|
|
345
|
+
return conventions;
|
|
346
|
+
}
|
|
347
|
+
summarizeEslint(content, fileName) {
|
|
348
|
+
const features = ['ESLint configured'];
|
|
349
|
+
try {
|
|
350
|
+
// Attempt to parse as JSON for structured configs
|
|
351
|
+
const cleaned = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
352
|
+
const parsed = JSON.parse(cleaned);
|
|
353
|
+
if (parsed.extends) {
|
|
354
|
+
const exts = Array.isArray(parsed.extends) ? parsed.extends : [parsed.extends];
|
|
355
|
+
for (const ext of exts) {
|
|
356
|
+
if (typeof ext === 'string') {
|
|
357
|
+
const base = path.basename(ext);
|
|
358
|
+
features.push(`extends ${base}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (parsed.parser) {
|
|
363
|
+
features.push(`parser: ${parsed.parser}`);
|
|
364
|
+
}
|
|
365
|
+
if (parsed.rules) {
|
|
366
|
+
const ruleNames = Object.keys(parsed.rules);
|
|
367
|
+
features.push(`${ruleNames.length} custom rules`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// JS/YAML config — summarize from text
|
|
372
|
+
if (/typescript/.test(content))
|
|
373
|
+
features.push('TypeScript support');
|
|
374
|
+
if (/react/.test(content))
|
|
375
|
+
features.push('React plugin');
|
|
376
|
+
if (/import\//.test(content))
|
|
377
|
+
features.push('import plugin');
|
|
378
|
+
}
|
|
379
|
+
return `${features.join(', ')} (${fileName})`;
|
|
380
|
+
}
|
|
381
|
+
summarizePrettier(content, fileName) {
|
|
382
|
+
const settings = ['Prettier configured'];
|
|
383
|
+
try {
|
|
384
|
+
const parsed = JSON.parse(content);
|
|
385
|
+
if (parsed.semi === false)
|
|
386
|
+
settings.push('no semicolons');
|
|
387
|
+
if (parsed.singleQuote)
|
|
388
|
+
settings.push('single quotes');
|
|
389
|
+
if (parsed.tabWidth)
|
|
390
|
+
settings.push(`tab width: ${parsed.tabWidth}`);
|
|
391
|
+
if (parsed.printWidth)
|
|
392
|
+
settings.push(`print width: ${parsed.printWidth}`);
|
|
393
|
+
if (parsed.trailingComma)
|
|
394
|
+
settings.push(`trailing comma: ${parsed.trailingComma}`);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// JS config — best effort text scan
|
|
398
|
+
if (/semi\s*:\s*false/.test(content))
|
|
399
|
+
settings.push('no semicolons');
|
|
400
|
+
if (/singleQuote\s*:\s*true/.test(content))
|
|
401
|
+
settings.push('single quotes');
|
|
402
|
+
}
|
|
403
|
+
return `${settings.join(', ')} (${fileName})`;
|
|
404
|
+
}
|
|
405
|
+
summarizeEditorConfig(content) {
|
|
406
|
+
const settings = ['EditorConfig'];
|
|
407
|
+
if (/indent_style\s*=\s*space/.test(content))
|
|
408
|
+
settings.push('indent: spaces');
|
|
409
|
+
if (/indent_style\s*=\s*tab/.test(content))
|
|
410
|
+
settings.push('indent: tabs');
|
|
411
|
+
if (/end_of_line\s*=\s*lf/.test(content))
|
|
412
|
+
settings.push('LF line endings');
|
|
413
|
+
if (/insert_final_newline\s*=\s*true/.test(content))
|
|
414
|
+
settings.push('final newline');
|
|
415
|
+
if (/charset\s*=\s*utf-8/.test(content))
|
|
416
|
+
settings.push('UTF-8');
|
|
417
|
+
return settings.join(', ');
|
|
418
|
+
}
|
|
419
|
+
summarizeTsconfig(content) {
|
|
420
|
+
const features = ['TypeScript'];
|
|
421
|
+
try {
|
|
422
|
+
const parsed = JSON.parse(content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''));
|
|
423
|
+
const co = parsed.compilerOptions || {};
|
|
424
|
+
if (co.strict)
|
|
425
|
+
features.push('strict mode');
|
|
426
|
+
if (co.esModuleInterop)
|
|
427
|
+
features.push('ESM interop');
|
|
428
|
+
if (co.moduleResolution === 'bundler' || co.moduleResolution === 'node') {
|
|
429
|
+
features.push(`${co.moduleResolution} module resolution`);
|
|
430
|
+
}
|
|
431
|
+
if (co.target)
|
|
432
|
+
features.push(`target: ${co.target}`);
|
|
433
|
+
if (co.module)
|
|
434
|
+
features.push(`module: ${co.module}`);
|
|
435
|
+
if (co.jsx)
|
|
436
|
+
features.push(`JSX: ${co.jsx}`);
|
|
437
|
+
if (co.declaration)
|
|
438
|
+
features.push('declarations enabled');
|
|
439
|
+
if (co.noUncheckedIndexedAccess)
|
|
440
|
+
features.push('unchecked indexed access check');
|
|
441
|
+
if (co.noImplicitReturns)
|
|
442
|
+
features.push('implicit returns check');
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Text-based heuristic
|
|
446
|
+
if (/strict/.test(content))
|
|
447
|
+
features.push('strict mode');
|
|
448
|
+
if (/esModuleInterop/.test(content))
|
|
449
|
+
features.push('ESM interop');
|
|
450
|
+
}
|
|
451
|
+
return features.join(', ');
|
|
452
|
+
}
|
|
453
|
+
// ── Private: Architecture Detection ───────────────────────────────────────
|
|
454
|
+
async detectArchitecture() {
|
|
455
|
+
const rootEntries = await readDirSafe(this.projectDir);
|
|
456
|
+
const dirNames = new Set(rootEntries.filter((e) => e.isDir).map((e) => e.name));
|
|
457
|
+
const fileNames = new Set(rootEntries.filter((e) => !e.isDir).map((e) => e.name));
|
|
458
|
+
// Read package.json for framework/library clues
|
|
459
|
+
const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
|
|
460
|
+
let pkg = {};
|
|
461
|
+
if (pkgContent) {
|
|
462
|
+
try {
|
|
463
|
+
pkg = JSON.parse(pkgContent);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// ignore
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
470
|
+
// Detect architecture patterns
|
|
471
|
+
const parts = [];
|
|
472
|
+
// Monorepo detection
|
|
473
|
+
if (dirNames.has('packages') || dirNames.has('apps')) {
|
|
474
|
+
parts.push('monorepo');
|
|
475
|
+
// Check for turborepo/nx
|
|
476
|
+
if (fileNames.has('turbo.json') || allDeps['turbo'])
|
|
477
|
+
parts.push('Turborepo');
|
|
478
|
+
if (dirNames.has('nx.json') || allDeps['nx'])
|
|
479
|
+
parts.push('Nx');
|
|
480
|
+
}
|
|
481
|
+
// Frontend frameworks
|
|
482
|
+
if (allDeps['next'])
|
|
483
|
+
parts.push('Next.js');
|
|
484
|
+
else if (allDeps['nuxt'] || allDeps['nuxt3'])
|
|
485
|
+
parts.push('Nuxt');
|
|
486
|
+
else if (allDeps['gatsby'])
|
|
487
|
+
parts.push('Gatsby');
|
|
488
|
+
else if (allDeps['react'])
|
|
489
|
+
parts.push('React');
|
|
490
|
+
else if (allDeps['vue'])
|
|
491
|
+
parts.push('Vue');
|
|
492
|
+
else if (allDeps['svelte'] || allDeps['@sveltejs/kit'])
|
|
493
|
+
parts.push('Svelte');
|
|
494
|
+
else if (allDeps['angular'] || allDeps['@angular/core'])
|
|
495
|
+
parts.push('Angular');
|
|
496
|
+
else if (allDeps['astro'])
|
|
497
|
+
parts.push('Astro');
|
|
498
|
+
// Backend frameworks
|
|
499
|
+
if (allDeps['express'])
|
|
500
|
+
parts.push('Express');
|
|
501
|
+
else if (allDeps['fastify'])
|
|
502
|
+
parts.push('Fastify');
|
|
503
|
+
else if (allDeps['koa'])
|
|
504
|
+
parts.push('Koa');
|
|
505
|
+
else if (allDeps['nestjs'] || allDeps['@nestjs/core'])
|
|
506
|
+
parts.push('NestJS');
|
|
507
|
+
else if (allDeps['hono'])
|
|
508
|
+
parts.push('Hono');
|
|
509
|
+
else if (allDeps['hapi'] || allDeps['@hapi/hapi'])
|
|
510
|
+
parts.push('Hapi');
|
|
511
|
+
// Full-stack
|
|
512
|
+
if (allDeps['@remix-run/react'])
|
|
513
|
+
parts.push('Remix');
|
|
514
|
+
// Databases / ORM
|
|
515
|
+
if (allDeps['prisma'] || allDeps['@prisma/client'])
|
|
516
|
+
parts.push('Prisma');
|
|
517
|
+
else if (allDeps['mongoose'])
|
|
518
|
+
parts.push('MongoDB/Mongoose');
|
|
519
|
+
else if (allDeps['typeorm'])
|
|
520
|
+
parts.push('TypeORM');
|
|
521
|
+
else if (allDeps['drizzle-orm'])
|
|
522
|
+
parts.push('Drizzle');
|
|
523
|
+
else if (allDeps['pg'])
|
|
524
|
+
parts.push('PostgreSQL');
|
|
525
|
+
else if (allDeps['mysql2'])
|
|
526
|
+
parts.push('MySQL');
|
|
527
|
+
// Testing
|
|
528
|
+
if (allDeps['jest'])
|
|
529
|
+
parts.push('Jest');
|
|
530
|
+
else if (allDeps['vitest'])
|
|
531
|
+
parts.push('Vitest');
|
|
532
|
+
else if (allDeps['mocha'])
|
|
533
|
+
parts.push('Mocha');
|
|
534
|
+
// Build tools
|
|
535
|
+
if (allDeps['vite'])
|
|
536
|
+
parts.push('Vite');
|
|
537
|
+
else if (allDeps['webpack'] || allDeps['webpack-cli'])
|
|
538
|
+
parts.push('Webpack');
|
|
539
|
+
else if (allDeps['esbuild'])
|
|
540
|
+
parts.push('esbuild');
|
|
541
|
+
else if (allDeps['rollup'])
|
|
542
|
+
parts.push('Rollup');
|
|
543
|
+
else if (allDeps['tsup'])
|
|
544
|
+
parts.push('tsup');
|
|
545
|
+
// Language
|
|
546
|
+
if (fileNames.has('tsconfig.json'))
|
|
547
|
+
parts.unshift('TypeScript');
|
|
548
|
+
else if (this.hasFileExtensionSync(fileNames, '.py'))
|
|
549
|
+
parts.unshift('Python');
|
|
550
|
+
else if (this.hasFileExtensionSync(fileNames, '.go'))
|
|
551
|
+
parts.unshift('Go');
|
|
552
|
+
else if (this.hasFileExtensionSync(fileNames, '.rs'))
|
|
553
|
+
parts.unshift('Rust');
|
|
554
|
+
// Directory structure hints
|
|
555
|
+
if (dirNames.has('src')) {
|
|
556
|
+
if (dirNames.has('src/routes') || dirNames.has('src/controllers')) {
|
|
557
|
+
parts.push('MVC pattern');
|
|
558
|
+
}
|
|
559
|
+
if (dirNames.has('src/components')) {
|
|
560
|
+
parts.push('component-based');
|
|
561
|
+
}
|
|
562
|
+
if (dirNames.has('src/services')) {
|
|
563
|
+
parts.push('service layer');
|
|
564
|
+
}
|
|
565
|
+
if (dirNames.has('src/lib') || dirNames.has('src/utils')) {
|
|
566
|
+
parts.push('utility module');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (dirNames.has('api') && dirNames.has('web')) {
|
|
570
|
+
parts.push('API + frontend split');
|
|
571
|
+
}
|
|
572
|
+
// Docker
|
|
573
|
+
if (fileNames.has('Dockerfile') || fileNames.has('docker-compose.yml') || fileNames.has('docker-compose.yaml')) {
|
|
574
|
+
parts.push('Docker');
|
|
575
|
+
}
|
|
576
|
+
const description = parts.length > 0 ? parts.join(' + ') : 'Unknown project structure';
|
|
577
|
+
return { description };
|
|
578
|
+
}
|
|
579
|
+
hasFileExtensionSync(fileNames, ext) {
|
|
580
|
+
return Array.from(fileNames).some((f) => f.endsWith(ext));
|
|
581
|
+
}
|
|
582
|
+
// ── Private: Pattern Detection ────────────────────────────────────────────
|
|
583
|
+
async detectPatterns() {
|
|
584
|
+
const common = [];
|
|
585
|
+
const avoid = [];
|
|
586
|
+
const srcDir = path.join(this.projectDir, 'src');
|
|
587
|
+
const scanDir = (await dirExists(srcDir)) ? srcDir : this.projectDir;
|
|
588
|
+
// Collect source files (limit to 200 for performance)
|
|
589
|
+
const sourceFiles = await this.collectSourceFiles(scanDir, 200);
|
|
590
|
+
if (sourceFiles.length === 0) {
|
|
591
|
+
return { common, avoid };
|
|
592
|
+
}
|
|
593
|
+
// Sample up to 30 files for content analysis
|
|
594
|
+
const sampleFiles = sourceFiles.slice(0, 30);
|
|
595
|
+
const contents = new Map();
|
|
596
|
+
for (const filePath of sampleFiles) {
|
|
597
|
+
const content = await readFileSafe(filePath);
|
|
598
|
+
if (content) {
|
|
599
|
+
contents.set(filePath, content);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Analyze import patterns
|
|
603
|
+
const importPatterns = new Map();
|
|
604
|
+
for (const [, content] of Array.from(contents.entries())) {
|
|
605
|
+
const importMatches = Array.from(content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g));
|
|
606
|
+
for (const match of importMatches) {
|
|
607
|
+
const mod = match[1];
|
|
608
|
+
// Normalize relative imports to pattern
|
|
609
|
+
const pattern = mod.startsWith('.')
|
|
610
|
+
? 'relative-import'
|
|
611
|
+
: mod.startsWith('@')
|
|
612
|
+
? `scoped-import:${mod.split('/').slice(0, 2).join('/')}`
|
|
613
|
+
: `external-import:${mod.split('/')[0]}`;
|
|
614
|
+
importPatterns.set(pattern, (importPatterns.get(pattern) || 0) + 1);
|
|
615
|
+
}
|
|
616
|
+
// CommonJS require
|
|
617
|
+
const requireMatches = Array.from(content.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g));
|
|
618
|
+
for (const match of requireMatches) {
|
|
619
|
+
const mod = match[1];
|
|
620
|
+
const pattern = mod.startsWith('.')
|
|
621
|
+
? 'relative-require'
|
|
622
|
+
: `external-require:${mod.split('/')[0]}`;
|
|
623
|
+
importPatterns.set(pattern, (importPatterns.get(pattern) || 0) + 1);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// Report frequent import patterns
|
|
627
|
+
for (const [pattern, count] of Array.from(importPatterns.entries())) {
|
|
628
|
+
if (count >= 3) {
|
|
629
|
+
if (pattern === 'relative-import') {
|
|
630
|
+
common.push('ES module relative imports');
|
|
631
|
+
}
|
|
632
|
+
else if (pattern === 'relative-require') {
|
|
633
|
+
common.push('CommonJS relative requires');
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
common.push(`Uses ${pattern.replace(/-/g, ' ')} (${count} occurrences)`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Code style patterns
|
|
641
|
+
let arrowFnCount = 0;
|
|
642
|
+
let asyncAwaitCount = 0;
|
|
643
|
+
let classCount = 0;
|
|
644
|
+
let exportDefaultCount = 0;
|
|
645
|
+
let namedExportCount = 0;
|
|
646
|
+
let typeOnlyImportCount = 0;
|
|
647
|
+
let consoleLogCount = 0;
|
|
648
|
+
let anyTypeCount = 0;
|
|
649
|
+
let evalCount = 0;
|
|
650
|
+
let varCount = 0;
|
|
651
|
+
for (const [, content] of Array.from(contents.entries())) {
|
|
652
|
+
if (/=>\s*\{/.test(content) || /=>\s*[^\{]/.test(content))
|
|
653
|
+
arrowFnCount++;
|
|
654
|
+
if (/async\s+/.test(content) && /await\s+/.test(content))
|
|
655
|
+
asyncAwaitCount++;
|
|
656
|
+
if (/class\s+\w+/.test(content))
|
|
657
|
+
classCount++;
|
|
658
|
+
if (/export\s+default\s+/.test(content))
|
|
659
|
+
exportDefaultCount++;
|
|
660
|
+
if (/export\s+(const|function|class|interface|type)\s/.test(content))
|
|
661
|
+
namedExportCount++;
|
|
662
|
+
if (/import\s+type\s+/.test(content) || /type\s+.*\s+from/.test(content))
|
|
663
|
+
typeOnlyImportCount++;
|
|
664
|
+
if (/console\.log/.test(content))
|
|
665
|
+
consoleLogCount++;
|
|
666
|
+
if (/:\s*any\b/.test(content))
|
|
667
|
+
anyTypeCount++;
|
|
668
|
+
if (/\beval\s*\(/.test(content))
|
|
669
|
+
evalCount++;
|
|
670
|
+
if (/\bvar\s+\w/.test(content))
|
|
671
|
+
varCount++;
|
|
672
|
+
}
|
|
673
|
+
const fileCount = contents.size || 1;
|
|
674
|
+
if (arrowFnCount > fileCount * 0.3)
|
|
675
|
+
common.push('arrow functions preferred');
|
|
676
|
+
if (asyncAwaitCount > fileCount * 0.2)
|
|
677
|
+
common.push('async/await pattern');
|
|
678
|
+
if (classCount > fileCount * 0.2)
|
|
679
|
+
common.push('class-based modules');
|
|
680
|
+
if (exportDefaultCount > namedExportCount)
|
|
681
|
+
common.push('default exports');
|
|
682
|
+
else if (namedExportCount > exportDefaultCount)
|
|
683
|
+
common.push('named exports');
|
|
684
|
+
if (typeOnlyImportCount > 0)
|
|
685
|
+
common.push('type-only imports (isolatedModules compatible)');
|
|
686
|
+
// Anti-patterns
|
|
687
|
+
if (consoleLogCount > fileCount * 0.5) {
|
|
688
|
+
avoid.push('excessive console.log — consider a proper logging library');
|
|
689
|
+
}
|
|
690
|
+
if (anyTypeCount > fileCount * 0.3) {
|
|
691
|
+
avoid.push('excessive use of `any` type — use specific types for safety');
|
|
692
|
+
}
|
|
693
|
+
if (evalCount > 0) {
|
|
694
|
+
avoid.push('use of eval() — security and performance risk');
|
|
695
|
+
}
|
|
696
|
+
if (varCount > fileCount * 0.2) {
|
|
697
|
+
avoid.push('use of var — prefer const or let');
|
|
698
|
+
}
|
|
699
|
+
// Detect naming conventions from filenames
|
|
700
|
+
const fileBasenames = sourceFiles.map((f) => path.basename(f, path.extname(f)));
|
|
701
|
+
const kebabCount = fileBasenames.filter((n) => /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(n)).length;
|
|
702
|
+
const camelCount = fileBasenames.filter((n) => /^[a-z][a-zA-Z0-9]+$/.test(n)).length;
|
|
703
|
+
const pascalCount = fileBasenames.filter((n) => /^[A-Z][a-zA-Z0-9]+$/.test(n)).length;
|
|
704
|
+
const snakeCount = fileBasenames.filter((n) => /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(n)).length;
|
|
705
|
+
if (kebabCount > fileBasenames.length * 0.5)
|
|
706
|
+
common.push('kebab-case file naming');
|
|
707
|
+
else if (camelCount > fileBasenames.length * 0.5)
|
|
708
|
+
common.push('camelCase file naming');
|
|
709
|
+
else if (pascalCount > fileBasenames.length * 0.5)
|
|
710
|
+
common.push('PascalCase file naming');
|
|
711
|
+
else if (snakeCount > fileBasenames.length * 0.5)
|
|
712
|
+
common.push('snake_case file naming');
|
|
713
|
+
return { common, avoid };
|
|
714
|
+
}
|
|
715
|
+
// ── Private: Dependency Detection ─────────────────────────────────────────
|
|
716
|
+
async detectDependencies() {
|
|
717
|
+
const pkgContent = await readFileSafe(path.join(this.projectDir, 'package.json'));
|
|
718
|
+
if (!pkgContent) {
|
|
719
|
+
// Try other package managers
|
|
720
|
+
const cargoContent = await readFileSafe(path.join(this.projectDir, 'Cargo.toml'));
|
|
721
|
+
if (cargoContent) {
|
|
722
|
+
return this.parseCargoDeps(cargoContent);
|
|
723
|
+
}
|
|
724
|
+
const requirementsContent = await readFileSafe(path.join(this.projectDir, 'requirements.txt'));
|
|
725
|
+
if (requirementsContent) {
|
|
726
|
+
return requirementsContent
|
|
727
|
+
.split('\n')
|
|
728
|
+
.map((l) => l.split('==')[0].split('>=')[0].split('~=')[0].trim())
|
|
729
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
730
|
+
}
|
|
731
|
+
const goModContent = await readFileSafe(path.join(this.projectDir, 'go.mod'));
|
|
732
|
+
if (goModContent) {
|
|
733
|
+
return goModContent
|
|
734
|
+
.split('\n')
|
|
735
|
+
.filter((l) => l.includes('/'))
|
|
736
|
+
.map((l) => l.trim().split(' ')[0])
|
|
737
|
+
.filter((l) => l && !l.startsWith('module') && !l.startsWith('go '));
|
|
738
|
+
}
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const pkg = JSON.parse(pkgContent);
|
|
743
|
+
const deps = {
|
|
744
|
+
...(pkg.dependencies || {}),
|
|
745
|
+
...(pkg.devDependencies || {}),
|
|
746
|
+
};
|
|
747
|
+
return Object.keys(deps).sort();
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
parseCargoDeps(content) {
|
|
754
|
+
const deps = [];
|
|
755
|
+
let inDeps = false;
|
|
756
|
+
for (const line of content.split('\n')) {
|
|
757
|
+
const trimmed = line.trim();
|
|
758
|
+
if (trimmed === '[dependencies]') {
|
|
759
|
+
inDeps = true;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (trimmed.startsWith('[')) {
|
|
763
|
+
inDeps = false;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (inDeps && trimmed.includes('=')) {
|
|
767
|
+
const name = trimmed.split('=')[0].trim();
|
|
768
|
+
if (name)
|
|
769
|
+
deps.push(name);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return deps;
|
|
773
|
+
}
|
|
774
|
+
// ── Private: File Utilities ───────────────────────────────────────────────
|
|
775
|
+
async hasFile(name) {
|
|
776
|
+
return fileExists(path.join(this.projectDir, name));
|
|
777
|
+
}
|
|
778
|
+
async hasFileExtension(ext) {
|
|
779
|
+
// Check top-level src directory
|
|
780
|
+
const candidates = [this.projectDir];
|
|
781
|
+
const srcDir = path.join(this.projectDir, 'src');
|
|
782
|
+
if (await dirExists(srcDir)) {
|
|
783
|
+
candidates.push(srcDir);
|
|
784
|
+
}
|
|
785
|
+
for (const dir of candidates) {
|
|
786
|
+
const entries = await readDirSafe(dir);
|
|
787
|
+
for (const entry of entries) {
|
|
788
|
+
if (!entry.isDir && entry.name.endsWith(ext)) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
// Check one level deep
|
|
792
|
+
if (entry.isDir && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
793
|
+
const subEntries = await readDirSafe(entry.fullPath);
|
|
794
|
+
for (const sub of subEntries) {
|
|
795
|
+
if (!sub.isDir && sub.name.endsWith(ext)) {
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Recursively collect source files up to a limit.
|
|
806
|
+
* Skips node_modules, .git, dist, build, coverage, and hidden directories.
|
|
807
|
+
*/
|
|
808
|
+
async collectSourceFiles(dir, limit) {
|
|
809
|
+
const results = [];
|
|
810
|
+
const skipDirs = new Set([
|
|
811
|
+
'node_modules',
|
|
812
|
+
'.git',
|
|
813
|
+
'dist',
|
|
814
|
+
'build',
|
|
815
|
+
'coverage',
|
|
816
|
+
'.next',
|
|
817
|
+
'.nuxt',
|
|
818
|
+
'.cache',
|
|
819
|
+
'.turbo',
|
|
820
|
+
'__pycache__',
|
|
821
|
+
'target',
|
|
822
|
+
'vendor',
|
|
823
|
+
]);
|
|
824
|
+
const sourceExtensions = new Set([
|
|
825
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
826
|
+
'.py', '.go', '.rs', '.java', '.rb',
|
|
827
|
+
]);
|
|
828
|
+
const queue = [dir];
|
|
829
|
+
while (queue.length > 0 && results.length < limit) {
|
|
830
|
+
const current = queue.shift();
|
|
831
|
+
const entries = await readDirSafe(current);
|
|
832
|
+
for (const entry of entries) {
|
|
833
|
+
if (results.length >= limit)
|
|
834
|
+
break;
|
|
835
|
+
if (entry.isDir) {
|
|
836
|
+
if (!entry.name.startsWith('.') && !skipDirs.has(entry.name)) {
|
|
837
|
+
queue.push(entry.fullPath);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
const ext = path.extname(entry.name);
|
|
842
|
+
if (sourceExtensions.has(ext)) {
|
|
843
|
+
results.push(entry.fullPath);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return results;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
//# sourceMappingURL=context-completion.js.map
|