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.
@@ -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();