readme-gen-analyzer 1.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/.turbo/turbo-build.log +4 -0
- package/README.md +77 -0
- package/dist/analyzers/ast-feature.detector.d.ts +10 -0
- package/dist/analyzers/ast-feature.detector.js +151 -0
- package/dist/analyzers/definition.extractor.d.ts +9 -0
- package/dist/analyzers/definition.extractor.js +141 -0
- package/dist/analyzers/dependency.analyzer.d.ts +14 -0
- package/dist/analyzers/dependency.analyzer.js +30 -0
- package/dist/analyzers/devops.analyzer.d.ts +3 -0
- package/dist/analyzers/devops.analyzer.js +42 -0
- package/dist/analyzers/env.extractor.d.ts +7 -0
- package/dist/analyzers/env.extractor.js +46 -0
- package/dist/analyzers/example.analyzer.d.ts +6 -0
- package/dist/analyzers/example.analyzer.js +84 -0
- package/dist/analyzers/feature.detector.d.ts +4 -0
- package/dist/analyzers/feature.detector.js +68 -0
- package/dist/analyzers/package.parser.d.ts +28 -0
- package/dist/analyzers/package.parser.js +341 -0
- package/dist/analyzers/polyglot.extractors.d.ts +18 -0
- package/dist/analyzers/polyglot.extractors.js +153 -0
- package/dist/analyzers/route.extractor.d.ts +10 -0
- package/dist/analyzers/route.extractor.js +41 -0
- package/dist/analyzers/schema.analyzer.d.ts +3 -0
- package/dist/analyzers/schema.analyzer.js +48 -0
- package/dist/analyzers/semantic.refiner.d.ts +16 -0
- package/dist/analyzers/semantic.refiner.js +154 -0
- package/dist/analyzers/structure.analyzer.d.ts +18 -0
- package/dist/analyzers/structure.analyzer.js +150 -0
- package/dist/analyzers/trace.analyzer.d.ts +10 -0
- package/dist/analyzers/trace.analyzer.js +75 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +44 -0
- package/dist/internal/analysis/chunker.d.ts +25 -0
- package/dist/internal/analysis/chunker.js +78 -0
- package/dist/internal/analysis/evidence.d.ts +17 -0
- package/dist/internal/analysis/evidence.js +130 -0
- package/dist/internal/analysis/techStack.d.ts +6 -0
- package/dist/internal/analysis/techStack.js +67 -0
- package/dist/internal/llm/llmClient.d.ts +69 -0
- package/dist/internal/llm/llmClient.js +204 -0
- package/dist/internal/pipeline/merge.d.ts +14 -0
- package/dist/internal/pipeline/merge.js +53 -0
- package/dist/internal/pipeline/persona.d.ts +7 -0
- package/dist/internal/pipeline/persona.js +28 -0
- package/dist/internal/pipeline/quality.d.ts +9 -0
- package/dist/internal/pipeline/quality.js +52 -0
- package/dist/internal/pipeline/readme.d.ts +3 -0
- package/dist/internal/pipeline/readme.js +80 -0
- package/dist/internal/pipeline/runPipeline.d.ts +57 -0
- package/dist/internal/pipeline/runPipeline.js +101 -0
- package/dist/internal/pipeline/stages.d.ts +5 -0
- package/dist/internal/pipeline/stages.js +85 -0
- package/dist/internal/pipeline/types.d.ts +98 -0
- package/dist/internal/pipeline/types.js +2 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +2 -0
- package/dist/utils/scanner.d.ts +14 -0
- package/dist/utils/scanner.js +81 -0
- package/dist/utils/scriptsMarkdown.d.ts +9 -0
- package/dist/utils/scriptsMarkdown.js +131 -0
- package/package.json +19 -0
- package/src/analyzers/ast-feature.detector.ts +173 -0
- package/src/analyzers/definition.extractor.ts +156 -0
- package/src/analyzers/dependency.analyzer.ts +32 -0
- package/src/analyzers/devops.analyzer.ts +44 -0
- package/src/analyzers/env.extractor.ts +58 -0
- package/src/analyzers/example.analyzer.ts +96 -0
- package/src/analyzers/feature.detector.ts +65 -0
- package/src/analyzers/package.parser.ts +364 -0
- package/src/analyzers/polyglot.extractors.ts +169 -0
- package/src/analyzers/route.extractor.ts +54 -0
- package/src/analyzers/schema.analyzer.ts +50 -0
- package/src/analyzers/semantic.refiner.ts +163 -0
- package/src/analyzers/structure.analyzer.ts +156 -0
- package/src/analyzers/trace.analyzer.ts +75 -0
- package/src/index.ts +29 -0
- package/src/internal/analysis/chunker.ts +103 -0
- package/src/internal/analysis/evidence.ts +152 -0
- package/src/internal/analysis/techStack.ts +71 -0
- package/src/internal/llm/llmClient.ts +261 -0
- package/src/internal/pipeline/merge.ts +63 -0
- package/src/internal/pipeline/persona.ts +27 -0
- package/src/internal/pipeline/quality.ts +47 -0
- package/src/internal/pipeline/readme.ts +98 -0
- package/src/internal/pipeline/runPipeline.ts +153 -0
- package/src/internal/pipeline/stages.ts +89 -0
- package/src/internal/pipeline/types.ts +102 -0
- package/src/types.ts +100 -0
- package/src/utils/scanner.ts +48 -0
- package/src/utils/scriptsMarkdown.ts +140 -0
- package/test-local.ts +16 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildScriptsMarkdown = buildScriptsMarkdown;
|
|
4
|
+
exports.formatWorkspaceScriptsForPrompt = formatWorkspaceScriptsForPrompt;
|
|
5
|
+
/** Inline code content: escape backticks so markdown stays valid */
|
|
6
|
+
function escapeInlineCode(s) {
|
|
7
|
+
return String(s).replace(/`/g, '\\`');
|
|
8
|
+
}
|
|
9
|
+
function packageManagerRun(pm) {
|
|
10
|
+
const p = (pm || 'npm').toLowerCase();
|
|
11
|
+
if (p === 'go mod' || p === 'go')
|
|
12
|
+
return { label: 'go', runWord: 'go run' };
|
|
13
|
+
if (p === 'poetry')
|
|
14
|
+
return { label: 'poetry', runWord: 'poetry run' };
|
|
15
|
+
if (p === 'uv')
|
|
16
|
+
return { label: 'uv', runWord: 'uv run' };
|
|
17
|
+
if (p === 'pdm')
|
|
18
|
+
return { label: 'pdm', runWord: 'pdm run' };
|
|
19
|
+
if (p === 'hatch')
|
|
20
|
+
return { label: 'hatch', runWord: 'hatch run' };
|
|
21
|
+
if (p === 'pip')
|
|
22
|
+
return { label: 'pip', runWord: 'python -m' };
|
|
23
|
+
if (p === 'pnpm')
|
|
24
|
+
return { label: 'pnpm', runWord: 'pnpm run' };
|
|
25
|
+
if (p === 'yarn')
|
|
26
|
+
return { label: 'yarn', runWord: 'yarn' };
|
|
27
|
+
if (p === 'bun')
|
|
28
|
+
return { label: 'bun', runWord: 'bun run' };
|
|
29
|
+
return { label: 'npm', runWord: 'npm run' };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Workspace script keys from PackageParser: "apps/api:dev" → dir apps/api, script dev.
|
|
33
|
+
* Plain keys: "build", "test" (no colon, or colon without path segments).
|
|
34
|
+
*/
|
|
35
|
+
function parseScriptKey(key) {
|
|
36
|
+
const i = key.indexOf(':');
|
|
37
|
+
if (i <= 0)
|
|
38
|
+
return { workspaceDir: null, scriptName: key };
|
|
39
|
+
const maybePath = key.slice(0, i);
|
|
40
|
+
const scriptName = key.slice(i + 1);
|
|
41
|
+
if (maybePath.includes('/') || maybePath.startsWith('packages\\') || maybePath.includes('\\')) {
|
|
42
|
+
const normalized = maybePath.replace(/\\/g, '/');
|
|
43
|
+
return { workspaceDir: normalized, scriptName };
|
|
44
|
+
}
|
|
45
|
+
// e.g. "npm:publish" — treat as single script name with colon
|
|
46
|
+
return { workspaceDir: null, scriptName: key };
|
|
47
|
+
}
|
|
48
|
+
function howToRun(pm, scriptKey, rawValue) {
|
|
49
|
+
const { runWord } = packageManagerRun(pm);
|
|
50
|
+
const { workspaceDir, scriptName } = parseScriptKey(scriptKey);
|
|
51
|
+
const value = rawValue.trim();
|
|
52
|
+
if (/^(go|python|pytest|uv|poetry|pdm|ruff|black|hatch)\s/i.test(value)) {
|
|
53
|
+
return { command: value };
|
|
54
|
+
}
|
|
55
|
+
if (workspaceDir) {
|
|
56
|
+
const cmd = `cd ${workspaceDir} && ${runWord} ${scriptName}`;
|
|
57
|
+
return {
|
|
58
|
+
command: cmd,
|
|
59
|
+
notes: value.startsWith('turbo ') ? 'turbo task' : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { command: `${runWord} ${scriptName}` };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Markdown block for README / evidence: table + package manager line.
|
|
66
|
+
*/
|
|
67
|
+
function buildScriptsMarkdown(summary) {
|
|
68
|
+
const scripts = summary.scripts || {};
|
|
69
|
+
const entries = Object.entries(scripts);
|
|
70
|
+
if (entries.length === 0) {
|
|
71
|
+
const pm = (summary.packageManager || '').toLowerCase();
|
|
72
|
+
if (pm === 'go mod' || pm === 'go') {
|
|
73
|
+
return [
|
|
74
|
+
'_Detected: **Go modules**._',
|
|
75
|
+
'',
|
|
76
|
+
'| Task | Command |',
|
|
77
|
+
'| --- | --- |',
|
|
78
|
+
'| build | `go build ./...` |',
|
|
79
|
+
'| test | `go test ./...` |',
|
|
80
|
+
'| vet | `go vet ./...` |',
|
|
81
|
+
'| tidy | `go mod tidy` |',
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
if (['pip', 'poetry', 'uv', 'pdm', 'hatch'].includes(pm)) {
|
|
85
|
+
const install = pm === 'poetry'
|
|
86
|
+
? 'poetry install'
|
|
87
|
+
: pm === 'uv'
|
|
88
|
+
? 'uv sync'
|
|
89
|
+
: pm === 'pdm'
|
|
90
|
+
? 'pdm install'
|
|
91
|
+
: pm === 'hatch'
|
|
92
|
+
? 'hatch env create'
|
|
93
|
+
: 'pip install -r requirements.txt';
|
|
94
|
+
return [
|
|
95
|
+
`_Detected: **${pm}**._`,
|
|
96
|
+
'',
|
|
97
|
+
'| Task | Command |',
|
|
98
|
+
'| --- | --- |',
|
|
99
|
+
'| install | `' + install + '` |',
|
|
100
|
+
'| tests | `pytest` |',
|
|
101
|
+
'| types | `mypy .` _if configured_ |',
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const { label } = packageManagerRun(summary.packageManager);
|
|
107
|
+
const lines = [
|
|
108
|
+
`_Detected package manager: **${label}**._`,
|
|
109
|
+
'',
|
|
110
|
+
'| Script | Definition | Run (from repo root) |',
|
|
111
|
+
'| --- | --- | --- |',
|
|
112
|
+
];
|
|
113
|
+
for (const [key, rawVal] of entries) {
|
|
114
|
+
const def = escapeInlineCode(String(rawVal));
|
|
115
|
+
const { command, notes } = howToRun(summary.packageManager, key, String(rawVal));
|
|
116
|
+
const runCol = notes ? `\`${escapeInlineCode(command)}\` _(${notes})_` : `\`${escapeInlineCode(command)}\``;
|
|
117
|
+
lines.push(`| \`${escapeInlineCode(key)}\` | \`${def}\` | ${runCol} |`);
|
|
118
|
+
}
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Compact one-liner list for nested README prompts (per workspace).
|
|
123
|
+
*/
|
|
124
|
+
function formatWorkspaceScriptsForPrompt(scripts, workspaceDir, packageManager) {
|
|
125
|
+
const { runWord } = packageManagerRun(packageManager);
|
|
126
|
+
const rows = Object.entries(scripts).map(([name, val]) => {
|
|
127
|
+
const cmd = `cd ${workspaceDir} && ${runWord} ${name}`;
|
|
128
|
+
return `- **${name}**: \`${escapeInlineCode(String(val))}\` → \`${escapeInlineCode(cmd)}\``;
|
|
129
|
+
});
|
|
130
|
+
return rows.join('\n');
|
|
131
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "readme-gen-analyzer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared code analysis logic for readme-gen",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc -w"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"ignore": "^7.0.5",
|
|
13
|
+
"ts-morph": "^27.0.2"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.12.7",
|
|
17
|
+
"typescript": "^5.4.5"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Project, SyntaxKind, CallExpression, PropertyAccessExpression } from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
export interface AstFeature {
|
|
4
|
+
name: string;
|
|
5
|
+
evidence: {
|
|
6
|
+
snippet: string;
|
|
7
|
+
file: string;
|
|
8
|
+
}[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class AstFeatureDetector {
|
|
12
|
+
public static detect(files: Record<string, string>): AstFeature[] {
|
|
13
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
14
|
+
const features: Map<string, AstFeature> = new Map();
|
|
15
|
+
|
|
16
|
+
const addEvidence = (featureName: string, snippet: string, file: string) => {
|
|
17
|
+
if (!features.has(featureName)) {
|
|
18
|
+
features.set(featureName, { name: featureName, evidence: [] });
|
|
19
|
+
}
|
|
20
|
+
const feat = features.get(featureName)!;
|
|
21
|
+
if (feat.evidence.length < 5) { // Limit evidence to 5 snippets per feature
|
|
22
|
+
feat.evidence.push({ snippet, file });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
27
|
+
if (!filePath.match(/\.(ts|js|tsx|jsx)$/)) continue;
|
|
28
|
+
|
|
29
|
+
const sourceFile = project.createSourceFile(filePath, content);
|
|
30
|
+
|
|
31
|
+
// 1. Scan for Call Expressions (Mainly for app.get, mongoose.connect, etc.)
|
|
32
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
33
|
+
|
|
34
|
+
for (const call of callExpressions) {
|
|
35
|
+
const expression = call.getExpression();
|
|
36
|
+
const text = call.getText();
|
|
37
|
+
|
|
38
|
+
// Pattern: app.get, router.post, etc.
|
|
39
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
40
|
+
const pae = expression as PropertyAccessExpression;
|
|
41
|
+
const name = pae.getName().toLowerCase();
|
|
42
|
+
const base = pae.getExpression().getText().toLowerCase();
|
|
43
|
+
|
|
44
|
+
// API Routes
|
|
45
|
+
if (['get', 'post', 'put', 'delete', 'patch', 'use'].includes(name)) {
|
|
46
|
+
if (['app', 'router', 'server', 'express'].includes(base)) {
|
|
47
|
+
addEvidence('API Endpoints', text.substring(0, 150), filePath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Database Integration (Mongoose, pg, MySQL)
|
|
52
|
+
if (name === 'connect' && (base === 'mongoose' || base === 'db')) {
|
|
53
|
+
addEvidence('Database Integration', text.substring(0, 150), filePath);
|
|
54
|
+
}
|
|
55
|
+
if (name === 'model' && base === 'mongoose') {
|
|
56
|
+
addEvidence('Database Integration', text.substring(0, 150), filePath);
|
|
57
|
+
}
|
|
58
|
+
if (name === 'createconnection' && (base === 'mysql' || base === 'db')) {
|
|
59
|
+
addEvidence('Database Integration', text.substring(0, 150), filePath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Authentication (JWT, bcrypt, passport)
|
|
63
|
+
if (['sign', 'verify'].includes(name) && base === 'jwt') {
|
|
64
|
+
addEvidence('Authentication', text.substring(0, 150), filePath);
|
|
65
|
+
}
|
|
66
|
+
if (['hash', 'compare'].includes(name) && base === 'bcrypt') {
|
|
67
|
+
addEvidence('Authentication', text.substring(0, 150), filePath);
|
|
68
|
+
}
|
|
69
|
+
if (['use', 'authenticate'].includes(name) && base === 'passport') {
|
|
70
|
+
addEvidence('Authentication', text.substring(0, 150), filePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Environment Configuration
|
|
74
|
+
if (name === 'config' && base === 'dotenv') {
|
|
75
|
+
addEvidence('Environment Configuration', text.substring(0, 150), filePath);
|
|
76
|
+
}
|
|
77
|
+
if (name === 'get' && base === 'config') {
|
|
78
|
+
addEvidence('Environment Configuration', text.substring(0, 150), filePath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// CLI Tool (Commander)
|
|
82
|
+
if (['command', 'option', 'parse', 'version'].includes(name) && ['program', 'cmd'].includes(base)) {
|
|
83
|
+
addEvidence('CLI Tool', text.substring(0, 150), filePath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Scan for New Expressions (Prisma, pg Pool)
|
|
88
|
+
const newExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression);
|
|
89
|
+
for (const newExpr of newExpressions) {
|
|
90
|
+
const newText = newExpr.getText().toLowerCase();
|
|
91
|
+
if (newText.includes('prismaclient')) {
|
|
92
|
+
addEvidence('Database Integration', newExpr.getText().substring(0, 150), filePath);
|
|
93
|
+
}
|
|
94
|
+
if (newText.includes('pool') || newText.includes('client')) {
|
|
95
|
+
addEvidence('Database Integration', newExpr.getText().substring(0, 150), filePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Scan for Property Access (process.env)
|
|
100
|
+
const propAccess = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression);
|
|
101
|
+
for (const pae of propAccess) {
|
|
102
|
+
if (pae.getText().startsWith('process.env.')) {
|
|
103
|
+
addEvidence('Environment Configuration', pae.getText(), filePath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 4. Scan for Decorators (NestJS)
|
|
108
|
+
const decorators = sourceFile.getDescendantsOfKind(SyntaxKind.Decorator);
|
|
109
|
+
for (const dec of decorators) {
|
|
110
|
+
const decName = dec.getName().toLowerCase();
|
|
111
|
+
if (['get', 'post', 'put', 'delete', 'patch', 'controller'].includes(decName)) {
|
|
112
|
+
addEvidence('API Endpoints', dec.getText(), filePath);
|
|
113
|
+
}
|
|
114
|
+
if (['injectable'].includes(decName)) {
|
|
115
|
+
// Marker for service oriented architecture
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 5. File-path based API Route Detection (Next.js, etc.)
|
|
121
|
+
if (filePath.includes('pages/api/') || filePath.includes('app/api/')) {
|
|
122
|
+
addEvidence('API Endpoints', `File-based route: ${filePath}`, filePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 6. Direct variable usage for CLI
|
|
126
|
+
if (content.includes('process.argv')) {
|
|
127
|
+
addEvidence('CLI Tool', 'process.argv', filePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
132
|
+
if (filePath.endsWith('.py')) {
|
|
133
|
+
if (/@(?:app|router)\.(get|post|put|delete|patch)\s*\(/i.test(content)) {
|
|
134
|
+
addEvidence('API Endpoints', 'FastAPI/Starlette-style route decorators', filePath);
|
|
135
|
+
}
|
|
136
|
+
if (/flask|Flask\(__name__\)/.test(content) || /@app\.route\s*\(/i.test(content)) {
|
|
137
|
+
addEvidence('API Endpoints', 'Flask routes', filePath);
|
|
138
|
+
}
|
|
139
|
+
if (/django\.|from\s+django/.test(content)) {
|
|
140
|
+
addEvidence('Django application', 'Django imports', filePath);
|
|
141
|
+
}
|
|
142
|
+
if (/sqlalchemy|SQLAlchemy/.test(content)) {
|
|
143
|
+
addEvidence('Database Integration', 'SQLAlchemy', filePath);
|
|
144
|
+
}
|
|
145
|
+
if (/os\.getenv|os\.environ/.test(content)) {
|
|
146
|
+
addEvidence('Environment Configuration', 'os.getenv / os.environ', filePath);
|
|
147
|
+
}
|
|
148
|
+
if (/celery|Celery/.test(content)) {
|
|
149
|
+
addEvidence('Task queue', 'Celery', filePath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (filePath.endsWith('.go')) {
|
|
153
|
+
if (/gin-gonic\/gin|\.GET\s*\(|\.POST\s*\(/.test(content)) {
|
|
154
|
+
addEvidence('API Endpoints', 'Go HTTP handlers (gin / stdlib style)', filePath);
|
|
155
|
+
}
|
|
156
|
+
if (/gorm\.io|database\/sql|jackc\/pgx/.test(content)) {
|
|
157
|
+
addEvidence('Database Integration', 'Go database client', filePath);
|
|
158
|
+
}
|
|
159
|
+
if (/os\.Getenv/.test(content)) {
|
|
160
|
+
addEvidence('Environment Configuration', 'os.Getenv', filePath);
|
|
161
|
+
}
|
|
162
|
+
if (/spf13\/cobra/.test(content)) {
|
|
163
|
+
addEvidence('CLI Tool', 'Cobra CLI', filePath);
|
|
164
|
+
}
|
|
165
|
+
if (/grpc\.|google\.golang\.org\/grpc/.test(content)) {
|
|
166
|
+
addEvidence('gRPC', 'gRPC imports', filePath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Array.from(features.values());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Project,
|
|
3
|
+
SyntaxKind,
|
|
4
|
+
ClassDeclaration,
|
|
5
|
+
FunctionDeclaration,
|
|
6
|
+
InterfaceDeclaration,
|
|
7
|
+
TypeAliasDeclaration,
|
|
8
|
+
ParameterDeclaration,
|
|
9
|
+
MethodDeclaration,
|
|
10
|
+
ArrowFunction,
|
|
11
|
+
Node
|
|
12
|
+
} from 'ts-morph';
|
|
13
|
+
import { PolyglotExtractors } from './polyglot.extractors';
|
|
14
|
+
|
|
15
|
+
export class DefinitionExtractor {
|
|
16
|
+
public static extract(files: Record<string, string>): Record<string, string[]> {
|
|
17
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
18
|
+
const result: Record<string, string[]> = {};
|
|
19
|
+
|
|
20
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
21
|
+
if (!filePath.match(/\.(ts|js|tsx|jsx)$/)) continue;
|
|
22
|
+
console.log(`[Analyzer] Parsing file for definitions: ${filePath}`);
|
|
23
|
+
project.createSourceFile(filePath, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
project.getSourceFiles().forEach(sourceFile => {
|
|
27
|
+
const filePath = sourceFile.getFilePath();
|
|
28
|
+
const definitions: string[] = [];
|
|
29
|
+
|
|
30
|
+
// Extract Imports
|
|
31
|
+
sourceFile.getImportDeclarations().forEach(imp => {
|
|
32
|
+
const module = imp.getModuleSpecifierValue();
|
|
33
|
+
const names = imp.getNamedImports().map(n => n.getName()).join(', ');
|
|
34
|
+
if (names) definitions.push(`Import: { ${names} } from "${module}"`);
|
|
35
|
+
else if (imp.getDefaultImport()) definitions.push(`Import Default: ${imp.getDefaultImport()?.getText()} from "${module}"`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Recursive Extraction from all top-level nodes
|
|
39
|
+
sourceFile.forEachChild(node => {
|
|
40
|
+
this.walkNode(node, definitions);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Interfaces/Types (Stay top-level mostly)
|
|
44
|
+
sourceFile.getInterfaces().forEach(intf => {
|
|
45
|
+
const name = intf.getName();
|
|
46
|
+
const properties = intf.getProperties().map(p => `${p.getName()}: ${p.getType().getText()}`).join(', ');
|
|
47
|
+
definitions.push(`Interface: ${name} { ${properties.substring(0, 200)} }`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
sourceFile.getTypeAliases().forEach(type => {
|
|
51
|
+
definitions.push(`Type Alias: ${type.getName()} = ${type.getType().getText().substring(0, 200)}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (definitions.length > 0) {
|
|
55
|
+
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
56
|
+
result[cleanPath] = definitions;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const polyglot = PolyglotExtractors.extractDefinitions(files);
|
|
61
|
+
for (const [p, defs] of Object.entries(polyglot)) {
|
|
62
|
+
if (!result[p]) result[p] = defs;
|
|
63
|
+
else result[p] = [...result[p]!, ...defs];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static walkNode(node: Node, definitions: string[]) {
|
|
70
|
+
// Classes
|
|
71
|
+
if (Node.isClassDeclaration(node)) {
|
|
72
|
+
const name = node.getName() || "AnonymousClass";
|
|
73
|
+
definitions.push(`Class: ${name}`);
|
|
74
|
+
node.getMethods().forEach(m => definitions.push(` ${this.formatSignature(m, 'Method')}`));
|
|
75
|
+
node.getConstructors().forEach(c => definitions.push(` ${this.formatSignature(c, 'Constructor')}`));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Functions
|
|
79
|
+
if (Node.isFunctionDeclaration(node)) {
|
|
80
|
+
const signature = this.formatSignature(node, 'Function');
|
|
81
|
+
console.log(` [Extractor] Found Function: ${signature}`);
|
|
82
|
+
definitions.push(signature);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Variable-based Functions (Arrow or Function Expressions)
|
|
86
|
+
if (Node.isVariableDeclaration(node)) {
|
|
87
|
+
const initializer = node.getInitializer();
|
|
88
|
+
if (initializer) {
|
|
89
|
+
if (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) {
|
|
90
|
+
const name = node.getName();
|
|
91
|
+
const params = this.formatSignature(initializer, 'Function (assigned)');
|
|
92
|
+
console.log(` [Extractor] Found Variable Function: ${name} ${params}`);
|
|
93
|
+
definitions.push(params);
|
|
94
|
+
} else if (Node.isObjectLiteralExpression(initializer)) {
|
|
95
|
+
// If it's a small object literal with methods, extract them
|
|
96
|
+
initializer.getProperties().forEach(prop => {
|
|
97
|
+
if (Node.isMethodDeclaration(prop) || Node.isPropertyAssignment(prop)) {
|
|
98
|
+
const propInit = Node.isPropertyAssignment(prop) ? prop.getInitializer() : null;
|
|
99
|
+
if (Node.isMethodDeclaration(prop) || (propInit && (Node.isArrowFunction(propInit) || Node.isFunctionExpression(propInit)))) {
|
|
100
|
+
const name = Node.isPropertyAssignment(prop) ? prop.getName() : (prop as any).getName();
|
|
101
|
+
const fn = Node.isPropertyAssignment(prop) ? (propInit as any) : prop;
|
|
102
|
+
definitions.push(` Method (obj-prop): ${name}${this.formatParamsSnippet(fn)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Export Assignments
|
|
111
|
+
if (Node.isExportAssignment(node)) {
|
|
112
|
+
const expression = node.getExpression();
|
|
113
|
+
if (Node.isArrowFunction(expression) || Node.isFunctionExpression(expression)) {
|
|
114
|
+
definitions.push(`Export Default Function: ${this.formatSignature(expression, '')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Recurse into children to find nested functions (but not too deep into implementations)
|
|
119
|
+
if (!Node.isClassDeclaration(node) && !Node.isFunctionDeclaration(node) && !Node.isMethodDeclaration(node)) {
|
|
120
|
+
node.forEachChild(child => this.walkNode(child, definitions));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static formatSignature(node: any, kind: string): string {
|
|
125
|
+
const name = node.getName?.() || "";
|
|
126
|
+
const params = this.formatParamsSnippet(node);
|
|
127
|
+
const asyncStr = node.isAsync?.() ? 'async ' : '';
|
|
128
|
+
return `${asyncStr}${kind}${name ? ': ' + name : ''}${params}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static formatParamsSnippet(node: any): string {
|
|
132
|
+
const params = this.formatParams(node.getParameters?.() || []);
|
|
133
|
+
const returnType = this.safeGetReturnType(node);
|
|
134
|
+
return `(${params}) -> ${returnType}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static formatParams(params: ParameterDeclaration[]): string {
|
|
138
|
+
return params.map(p => {
|
|
139
|
+
const name = p.getName();
|
|
140
|
+
const type = this.safeGetParamType(p);
|
|
141
|
+
return `${name}${p.isOptional() ? '?' : ''}: ${type}`;
|
|
142
|
+
}).join(', ');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private static safeGetParamType(p: ParameterDeclaration): string {
|
|
146
|
+
try {
|
|
147
|
+
return p.getTypeNode()?.getText() || p.getType().getText() || 'any';
|
|
148
|
+
} catch { return 'any'; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static safeGetReturnType(fn: any): string {
|
|
152
|
+
try {
|
|
153
|
+
return fn.getReturnTypeNode()?.getText() || fn.getReturnType().getText() || 'void';
|
|
154
|
+
} catch { return 'void'; }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface DependencyGroups {
|
|
2
|
+
core: string[];
|
|
3
|
+
database: string[];
|
|
4
|
+
testing: string[];
|
|
5
|
+
deployment: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class DependencyAnalyzer {
|
|
9
|
+
private static CORE = ['express', 'fastapi', 'flask', 'django', 'react', 'next', 'vue', 'nest', 'koa'];
|
|
10
|
+
private static DATABASE = ['mongoose', 'prisma', 'pg', 'mysql2', 'redis', 'sequelize', 'mongodb', 'psycopg2', 'sqlalchemy'];
|
|
11
|
+
private static TESTING = ['jest', 'mocha', 'chai', 'cypress', 'pytest', 'vitest', 'playwright'];
|
|
12
|
+
private static DEPLOYMENT = ['docker', 'aws-sdk', 'firebase', 'vercel', 'netlify', 'terraform'];
|
|
13
|
+
|
|
14
|
+
public static analyze(dependencies: string[]): DependencyGroups {
|
|
15
|
+
const groups: DependencyGroups = {
|
|
16
|
+
core: [],
|
|
17
|
+
database: [],
|
|
18
|
+
testing: [],
|
|
19
|
+
deployment: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
dependencies.forEach(dep => {
|
|
23
|
+
const lower = dep.toLowerCase();
|
|
24
|
+
if (this.CORE.some(c => lower.includes(c))) groups.core.push(dep);
|
|
25
|
+
if (this.DATABASE.some(d => lower.includes(d))) groups.database.push(dep);
|
|
26
|
+
if (this.TESTING.some(t => lower.includes(t))) groups.testing.push(dep);
|
|
27
|
+
if (this.DEPLOYMENT.some(d => lower.includes(d))) groups.deployment.push(dep);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return groups;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class DevOpsAnalyzer {
|
|
2
|
+
public static analyze(files: Record<string, string>) {
|
|
3
|
+
const devOps: any = {};
|
|
4
|
+
|
|
5
|
+
// 1. Dockerfile
|
|
6
|
+
const dockerPath = Object.keys(files).find(f => f.toLowerCase().endsWith('dockerfile'));
|
|
7
|
+
if (dockerPath) {
|
|
8
|
+
const content = files[dockerPath];
|
|
9
|
+
devOps.docker = {
|
|
10
|
+
baseImage: content.match(/FROM\s+([^\s\n]+)/i)?.[1],
|
|
11
|
+
ports: [...content.matchAll(/EXPOSE\s+(\d+)/gi)].map(m => m[1]),
|
|
12
|
+
command: content.match(/CMD\s+\[?([^\]\n]+)\]?/i)?.[1]?.replace(/"/g, '')
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 2. Docker Compose
|
|
17
|
+
const composePath = Object.keys(files).find(f => f.toLowerCase().includes('docker-compose') && f.endsWith('.yml'));
|
|
18
|
+
if (composePath) {
|
|
19
|
+
const content = files[composePath];
|
|
20
|
+
const services = [...content.matchAll(/^\s+([a-z0-9_-]+):/gm)]
|
|
21
|
+
.map(m => m[1])
|
|
22
|
+
.filter(s => !['services', 'networks', 'volumes', 'version'].includes(s));
|
|
23
|
+
const networks = [...content.matchAll(/^\s+networks:\s*\n(\s+- [a-z0-9_-]+\n)+/gi)].length > 0 ? ['Enabled'] : [];
|
|
24
|
+
|
|
25
|
+
devOps.compose = { services, networks };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 3. GitHub Actions
|
|
29
|
+
const workflowPath = Object.keys(files).find(f => f.includes('.github/workflows') && f.endsWith('.yml'));
|
|
30
|
+
if (workflowPath) {
|
|
31
|
+
const content = files[workflowPath];
|
|
32
|
+
const jobs = [...content.matchAll(/^\s+([a-z0-9_-]+):/gm)]
|
|
33
|
+
.map(m => m[1])
|
|
34
|
+
.filter(j => !['jobs', 'on', 'workflow_dispatch', 'name'].includes(j));
|
|
35
|
+
|
|
36
|
+
devOps.pipeline = {
|
|
37
|
+
provider: 'GitHub Actions',
|
|
38
|
+
jobs
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Object.keys(devOps).length > 0 ? devOps : undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface EnvVar {
|
|
2
|
+
name: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class EnvExtractor {
|
|
6
|
+
private static ENV_REGEX = /process\.env\.([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
7
|
+
|
|
8
|
+
public static extract(files: Record<string, string>): string[] {
|
|
9
|
+
const envVars = new Set<string>();
|
|
10
|
+
|
|
11
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
12
|
+
// 1. From .env or .env.example
|
|
13
|
+
if (filePath.endsWith('.env') || filePath.endsWith('.env.example')) {
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
18
|
+
const match = trimmed.match(/^([^=]+)=/);
|
|
19
|
+
if (match) {
|
|
20
|
+
envVars.add(match[1].trim());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. From code usage (process.env.VAR)
|
|
27
|
+
if (filePath.endsWith('.ts') || filePath.endsWith('.js')) {
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = this.ENV_REGEX.exec(content)) !== null) {
|
|
30
|
+
envVars.add(match[1]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (filePath.endsWith('.py')) {
|
|
35
|
+
for (const m of content.matchAll(
|
|
36
|
+
/os\.(?:getenv|environ\.get)\(\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']/g,
|
|
37
|
+
)) {
|
|
38
|
+
envVars.add(m[1]!);
|
|
39
|
+
}
|
|
40
|
+
for (const m of content.matchAll(
|
|
41
|
+
/os\.environ\[\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']\s*\]/g,
|
|
42
|
+
)) {
|
|
43
|
+
envVars.add(m[1]!);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (filePath.endsWith('.go')) {
|
|
48
|
+
for (const m of content.matchAll(
|
|
49
|
+
/os\.Getenv\(\s*["']([a-zA-Z_][a-zA-Z0-9_]*)["']\s*\)/g,
|
|
50
|
+
)) {
|
|
51
|
+
envVars.add(m[1]!);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.from(envVars);
|
|
57
|
+
}
|
|
58
|
+
}
|