projectify-cli 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/README.md +94 -0
- package/dist/ai/index.js +100 -0
- package/dist/ai/providers/base.js +2 -0
- package/dist/ai/providers/gemini.js +56 -0
- package/dist/ai/providers/ollama.js +50 -0
- package/dist/ai/providers/openai.js +72 -0
- package/dist/analyzer/index.js +34 -0
- package/dist/analyzer/parsers/javascript.js +137 -0
- package/dist/analyzer/parsers/python.js +44 -0
- package/dist/graph/index.js +126 -0
- package/dist/index.js +136 -0
- package/dist/report/htmlGenerator.js +582 -0
- package/dist/scanner/index.js +29 -0
- package/dist/utils/fileUtils.js +24 -0
- package/dist/utils/gitUtils.js +47 -0
- package/functions.png +0 -0
- package/graph.png +0 -0
- package/package.json +57 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DependencyGraph = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
class DependencyGraph {
|
|
9
|
+
constructor(analysis) {
|
|
10
|
+
this.analysis = analysis;
|
|
11
|
+
this.nodes = new Map();
|
|
12
|
+
this.edges = new Map(); // From -> Set<To>
|
|
13
|
+
this.reverseEdges = new Map(); // To -> Set<From>
|
|
14
|
+
this.buildGraph();
|
|
15
|
+
this.calculateMetrics();
|
|
16
|
+
}
|
|
17
|
+
buildGraph() {
|
|
18
|
+
// strict node handling
|
|
19
|
+
Object.keys(this.analysis.files).forEach(file => {
|
|
20
|
+
this.nodes.set(file, {
|
|
21
|
+
id: file,
|
|
22
|
+
inDegree: 0,
|
|
23
|
+
outDegree: 0,
|
|
24
|
+
blastRadius: 0,
|
|
25
|
+
affectedFiles: 0
|
|
26
|
+
});
|
|
27
|
+
this.edges.set(file, new Set());
|
|
28
|
+
this.reverseEdges.set(file, new Set());
|
|
29
|
+
});
|
|
30
|
+
Object.entries(this.analysis.files).forEach(([filePath, fileData]) => {
|
|
31
|
+
fileData.imports.forEach(importPath => {
|
|
32
|
+
const resolvedPath = this.resolveImport(filePath, importPath);
|
|
33
|
+
if (resolvedPath && this.nodes.has(resolvedPath)) {
|
|
34
|
+
this.addEdge(filePath, resolvedPath);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
resolveImport(sourceFile, importPath) {
|
|
40
|
+
const dir = path_1.default.dirname(sourceFile);
|
|
41
|
+
const possibleExtensions = ['', '.js', '.ts', '.jsx', '.tsx', '.py'];
|
|
42
|
+
// 1. Check relative imports (starts with .)
|
|
43
|
+
if (importPath.startsWith('.')) {
|
|
44
|
+
for (const ext of possibleExtensions) {
|
|
45
|
+
const resolved = path_1.default.resolve(dir, importPath + ext);
|
|
46
|
+
if (this.nodes.has(resolved))
|
|
47
|
+
return resolved;
|
|
48
|
+
const indexResolved = path_1.default.resolve(dir, importPath, 'index' + ext);
|
|
49
|
+
if (this.nodes.has(indexResolved))
|
|
50
|
+
return indexResolved;
|
|
51
|
+
// Python __init__
|
|
52
|
+
const initResolved = path_1.default.resolve(dir, importPath, '__init__.py');
|
|
53
|
+
if (this.nodes.has(initResolved))
|
|
54
|
+
return initResolved;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 2. Check Python module imports (e.g. 'analyzer.scanner' -> 'analyzer/scanner.py')
|
|
58
|
+
else if (!importPath.startsWith('/') && !importPath.startsWith('@')) {
|
|
59
|
+
const pyPath = importPath.replace(/\./g, '/');
|
|
60
|
+
// Try resolving from root (simplified assumption for now, ideally scan PYTHONPATH)
|
|
61
|
+
// We'll try relative first for simple cases, then "absolute" from project root
|
|
62
|
+
// Try relative to current file (Python often allows this implicitly in packages)
|
|
63
|
+
for (const ext of possibleExtensions) {
|
|
64
|
+
const resolved = path_1.default.resolve(dir, pyPath + ext);
|
|
65
|
+
if (this.nodes.has(resolved))
|
|
66
|
+
return resolved;
|
|
67
|
+
const initResolved = path_1.default.resolve(dir, pyPath, '__init__.py');
|
|
68
|
+
if (this.nodes.has(initResolved))
|
|
69
|
+
return initResolved;
|
|
70
|
+
}
|
|
71
|
+
// Try from project root (we don't easily know project root here, but we can guess it's where package.json is,
|
|
72
|
+
// OR we can iterate all nodes to find a match - expensive but correct for "Project" analyzer)
|
|
73
|
+
// A faster way is to map "fileName" -> fullPath in a separate index.
|
|
74
|
+
// For now, let's skip complex root resolution to keep it simple.
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
addEdge(from, to) {
|
|
79
|
+
if (!this.edges.get(from)?.has(to)) {
|
|
80
|
+
this.edges.get(from)?.add(to);
|
|
81
|
+
this.reverseEdges.get(to)?.add(from);
|
|
82
|
+
const fromNode = this.nodes.get(from);
|
|
83
|
+
const toNode = this.nodes.get(to);
|
|
84
|
+
fromNode.outDegree++;
|
|
85
|
+
toNode.inDegree++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
calculateMetrics() {
|
|
89
|
+
this.nodes.forEach(node => {
|
|
90
|
+
const dependents = this.getAllDependents(node.id);
|
|
91
|
+
node.affectedFiles = dependents.size;
|
|
92
|
+
node.blastRadius = (dependents.size / this.nodes.size) * 100;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
getAllDependents(nodeId) {
|
|
96
|
+
const dependents = new Set();
|
|
97
|
+
const queue = [nodeId];
|
|
98
|
+
const visited = new Set([nodeId]);
|
|
99
|
+
while (queue.length > 0) {
|
|
100
|
+
const current = queue.shift();
|
|
101
|
+
const incoming = this.reverseEdges.get(current);
|
|
102
|
+
if (incoming) {
|
|
103
|
+
incoming.forEach(src => {
|
|
104
|
+
if (!visited.has(src)) {
|
|
105
|
+
visited.add(src);
|
|
106
|
+
dependents.add(src);
|
|
107
|
+
queue.push(src);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return dependents;
|
|
113
|
+
}
|
|
114
|
+
getTopBlastRadius(limit = 5) {
|
|
115
|
+
return Array.from(this.nodes.values())
|
|
116
|
+
.sort((a, b) => b.blastRadius - a.blastRadius)
|
|
117
|
+
.slice(0, limit);
|
|
118
|
+
}
|
|
119
|
+
getNodes() {
|
|
120
|
+
return this.nodes;
|
|
121
|
+
}
|
|
122
|
+
getEdges() {
|
|
123
|
+
return this.edges;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.DependencyGraph = DependencyGraph;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const scanner_1 = require("./scanner");
|
|
12
|
+
const analyzer_1 = require("./analyzer");
|
|
13
|
+
const graph_1 = require("./graph");
|
|
14
|
+
const ai_1 = require("./ai");
|
|
15
|
+
const gitUtils_1 = require("./utils/gitUtils");
|
|
16
|
+
const program = new commander_1.Command();
|
|
17
|
+
program
|
|
18
|
+
.name('projectify')
|
|
19
|
+
.description('Projectify - Autonomous Code Analysis & Visualization')
|
|
20
|
+
.version('2.0.0');
|
|
21
|
+
program
|
|
22
|
+
.argument('[path]', 'Project path to analyze', '.')
|
|
23
|
+
.option('--no-ai', 'Skip AI analysis')
|
|
24
|
+
.option('--summary', 'Generate full project summary')
|
|
25
|
+
.option('--provider <type>', 'AI Provider (openai, gemini, ollama)', 'openai')
|
|
26
|
+
.option('--model <name>', 'Model name (optional)')
|
|
27
|
+
.action(async (projectPath, options) => {
|
|
28
|
+
try {
|
|
29
|
+
console.log(chalk_1.default.blue(`🚀 Starting analysis for: ${projectPath}`));
|
|
30
|
+
// 1. Scan
|
|
31
|
+
console.log(chalk_1.default.yellow('scanning files...'));
|
|
32
|
+
const files = await (0, scanner_1.scanProject)({ path: projectPath });
|
|
33
|
+
console.log(chalk_1.default.green(`found ${files.length} files.`));
|
|
34
|
+
// 2. Analyze
|
|
35
|
+
console.log(chalk_1.default.yellow('parsing codebase...'));
|
|
36
|
+
const analysis = await (0, analyzer_1.analyzeFiles)(files);
|
|
37
|
+
// 3. Git Analysis
|
|
38
|
+
console.log(chalk_1.default.yellow('analyzing git history...'));
|
|
39
|
+
const gitService = new gitUtils_1.GitService(projectPath);
|
|
40
|
+
const gitStats = await gitService.getAnalysis();
|
|
41
|
+
if (gitStats) {
|
|
42
|
+
console.log(chalk_1.default.green(`git history found: ${gitStats.totalCommits} commits, ${gitStats.authroStats.length} authors.`));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log(chalk_1.default.gray('no git repository found or git error.'));
|
|
46
|
+
}
|
|
47
|
+
// 4. Build Graph
|
|
48
|
+
console.log(chalk_1.default.yellow('building dependency graph...'));
|
|
49
|
+
const graph = new graph_1.DependencyGraph(analysis);
|
|
50
|
+
const topRisks = graph.getTopBlastRadius(5);
|
|
51
|
+
console.log(chalk_1.default.bold.underline('\n🔥 Top Blast Radius Risks:'));
|
|
52
|
+
topRisks.forEach(node => {
|
|
53
|
+
console.log(`${chalk_1.default.cyan(path_1.default.basename(node.id))} : ${chalk_1.default.red(node.blastRadius.toFixed(1) + '%')} impact (${node.affectedFiles} files)`);
|
|
54
|
+
});
|
|
55
|
+
// 5. AI Analysis
|
|
56
|
+
let ai = null;
|
|
57
|
+
let projectSummary = '';
|
|
58
|
+
let gitInsight = '';
|
|
59
|
+
if (options.ai) {
|
|
60
|
+
const providerType = options.provider;
|
|
61
|
+
let apiKey = '';
|
|
62
|
+
if (providerType === 'openai') {
|
|
63
|
+
apiKey = process.env.OPENAI_API_KEY || '';
|
|
64
|
+
if (!apiKey)
|
|
65
|
+
console.log(chalk_1.default.red('\n⚠️ OPENAI_API_KEY not found.'));
|
|
66
|
+
}
|
|
67
|
+
else if (providerType === 'gemini') {
|
|
68
|
+
apiKey = process.env.GEMINI_API_KEY || '';
|
|
69
|
+
if (!apiKey)
|
|
70
|
+
console.log(chalk_1.default.red('\n⚠️ GEMINI_API_KEY not found.'));
|
|
71
|
+
}
|
|
72
|
+
if ((providerType === 'ollama') || apiKey) {
|
|
73
|
+
try {
|
|
74
|
+
console.log(chalk_1.default.blue(`\n🧠 Initializing AI (${providerType})...`));
|
|
75
|
+
ai = new ai_1.CodeIntelligence(providerType, apiKey, options.model);
|
|
76
|
+
// Analyze the highest risk file
|
|
77
|
+
if (topRisks.length > 0) {
|
|
78
|
+
const riskiest = topRisks[0];
|
|
79
|
+
console.log(chalk_1.default.gray(`Analyzing ${path_1.default.basename(riskiest.id)}...`));
|
|
80
|
+
const insight = await ai.analyzeBlastRadius(riskiest);
|
|
81
|
+
console.log(chalk_1.default.white(insight));
|
|
82
|
+
}
|
|
83
|
+
// Detailed Project Summary
|
|
84
|
+
if (options.summary) {
|
|
85
|
+
console.log(chalk_1.default.blue('\n🧠 Generating Project Summary...'));
|
|
86
|
+
const fileList = Object.keys(analysis.files);
|
|
87
|
+
projectSummary = await ai.generateProjectSummary(analysis.fileCount, topRisks, fileList);
|
|
88
|
+
console.log(chalk_1.default.white(chalk_1.default.bold('\nProject Overview:\n') + projectSummary));
|
|
89
|
+
}
|
|
90
|
+
// Git Evolution Insight
|
|
91
|
+
if (gitStats) {
|
|
92
|
+
console.log(chalk_1.default.blue('\n🧠 Analyzing Project Evolution...'));
|
|
93
|
+
gitInsight = await ai.analyzeGitHistory(gitStats);
|
|
94
|
+
console.log(chalk_1.default.white(chalk_1.default.bold('\nGit Insights:\n') + gitInsight));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error(chalk_1.default.red(`AI Initialization failed: ${e.message}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 6. Save Report
|
|
103
|
+
const reportPath = path_1.default.resolve('analysis-report.json');
|
|
104
|
+
await fs_extra_1.default.writeJSON(reportPath, {
|
|
105
|
+
timestamp: new Date(),
|
|
106
|
+
files: analysis.fileCount,
|
|
107
|
+
topRisks,
|
|
108
|
+
gitAnalysis: gitStats,
|
|
109
|
+
aiInsights: {
|
|
110
|
+
projectSummary,
|
|
111
|
+
gitInsight
|
|
112
|
+
},
|
|
113
|
+
fullAnalysis: analysis
|
|
114
|
+
}, { spaces: 2 });
|
|
115
|
+
console.log(chalk_1.default.green(`\n✅ JSON Report saved to ${reportPath}`));
|
|
116
|
+
const htmlPath = path_1.default.resolve('analysis-report.html');
|
|
117
|
+
const { generateHtmlReport } = require('./report/htmlGenerator');
|
|
118
|
+
// Check if we need to pass new data to html generator.
|
|
119
|
+
// For now, we just pass the same args, assuming HTML generator might need updates later
|
|
120
|
+
// but the JSON report is the source of truth for raw data.
|
|
121
|
+
await generateHtmlReport(projectPath, analysis, graph, htmlPath, gitStats);
|
|
122
|
+
console.log(chalk_1.default.green(`✅ HTML Graph saved to ${htmlPath}`));
|
|
123
|
+
// Save Summary MD
|
|
124
|
+
if (projectSummary) {
|
|
125
|
+
const summaryPath = path_1.default.resolve('project-summary.md');
|
|
126
|
+
const content = `# Project Summary\n\n${projectSummary}\n\n## Evolution Insights\n\n${gitInsight}`;
|
|
127
|
+
await fs_extra_1.default.writeFile(summaryPath, content);
|
|
128
|
+
console.log(chalk_1.default.green(`✅ Summary saved to ${summaryPath}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error(chalk_1.default.red('Analysis failed:'), error);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
program.parse();
|