@madnessengineering/uml-generator 0.1.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/EXAMPLES.md +249 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/UML-GENERATOR-README.md +165 -0
- package/package.json +63 -0
- package/tui.js +490 -0
- package/uml-generator.js +614 -0
package/uml-generator.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ๐โก SWARMDESK UML GENERATOR
|
|
4
|
+
* Standalone UML generator for any codebase - visualize any repo in 3D!
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Analyze local Git repositories
|
|
8
|
+
* - Clone and analyze GitHub repositories
|
|
9
|
+
* - Generate UML JSON for SwarmDesk 3D visualization
|
|
10
|
+
* - Support for JavaScript/TypeScript/React codebases
|
|
11
|
+
* - Git metrics and dependency analysis
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node uml-generator.js /path/to/repo
|
|
15
|
+
* node uml-generator.js https://github.com/user/repo
|
|
16
|
+
* node uml-generator.js . --output my-project.json
|
|
17
|
+
* node uml-generator.js /path/to/repo --include "src,lib" --exclude "test,dist"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
const { parse: parseComments } = require('comment-parser');
|
|
24
|
+
const ts = require('typescript');
|
|
25
|
+
|
|
26
|
+
// Configuration from command line
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
|
|
29
|
+
// Check for help flag
|
|
30
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
31
|
+
console.log(`
|
|
32
|
+
๐โก SWARMDESK UML GENERATOR
|
|
33
|
+
|
|
34
|
+
USAGE:
|
|
35
|
+
node uml-generator.js Launch interactive TUI mode
|
|
36
|
+
node uml-generator.js [path] Analyze local directory
|
|
37
|
+
node uml-generator.js [github-url] Clone and analyze GitHub repo
|
|
38
|
+
node uml-generator.js [path] [options] Analyze with options
|
|
39
|
+
|
|
40
|
+
OPTIONS:
|
|
41
|
+
--output <file> Output JSON file path
|
|
42
|
+
--include <patterns> Comma-separated directories to include
|
|
43
|
+
--exclude <patterns> Comma-separated patterns to exclude
|
|
44
|
+
--help, -h Show this help message
|
|
45
|
+
|
|
46
|
+
EXAMPLES:
|
|
47
|
+
node uml-generator.js # Interactive TUI
|
|
48
|
+
node uml-generator.js . # Analyze current dir
|
|
49
|
+
node uml-generator.js /path/to/project # Analyze specific dir
|
|
50
|
+
node uml-generator.js https://github.com/user/repo # Analyze GitHub repo
|
|
51
|
+
node uml-generator.js . --output my-uml.json # Custom output
|
|
52
|
+
node uml-generator.js . --include "src,lib" # Custom patterns
|
|
53
|
+
|
|
54
|
+
๐งโโ๏ธ From the Mad Laboratory
|
|
55
|
+
`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let targetPath = args[0] || '.';
|
|
60
|
+
let outputFile = null;
|
|
61
|
+
let includePatterns = ['src', 'lib', 'components', 'pages', 'utils', 'hooks', 'services'];
|
|
62
|
+
let excludePatterns = ['node_modules', 'dist', 'build', '.git', 'coverage', 'test', '__tests__'];
|
|
63
|
+
|
|
64
|
+
// Parse command line arguments
|
|
65
|
+
for (let i = 1; i < args.length; i++) {
|
|
66
|
+
if (args[i] === '--output' && args[i + 1]) {
|
|
67
|
+
outputFile = args[i + 1];
|
|
68
|
+
i++;
|
|
69
|
+
} else if (args[i] === '--include' && args[i + 1]) {
|
|
70
|
+
includePatterns = args[i + 1].split(',');
|
|
71
|
+
i++;
|
|
72
|
+
} else if (args[i] === '--exclude' && args[i + 1]) {
|
|
73
|
+
excludePatterns = args[i + 1].split(',');
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ๐ Check if input is a GitHub URL
|
|
80
|
+
*/
|
|
81
|
+
function isGitHubUrl(input) {
|
|
82
|
+
return input.startsWith('http://') || input.startsWith('https://') || input.startsWith('git@');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ๐ฅ Clone GitHub repository to temp directory
|
|
87
|
+
*/
|
|
88
|
+
function cloneRepository(url) {
|
|
89
|
+
console.log(`๐ Cloning repository: ${url}`);
|
|
90
|
+
const tempDir = path.join(process.cwd(), '.swarmdesk-temp', `repo-${Date.now()}`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
94
|
+
execSync(`git clone --depth 1 ${url} ${tempDir}`, { stdio: 'inherit' });
|
|
95
|
+
console.log(`โ
Cloned to: ${tempDir}`);
|
|
96
|
+
return tempDir;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`โ Failed to clone repository: ${error.message}`);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ๐งน Cleanup temporary directory
|
|
105
|
+
*/
|
|
106
|
+
function cleanupTemp(tempDir) {
|
|
107
|
+
if (tempDir && tempDir.includes('.swarmdesk-temp')) {
|
|
108
|
+
try {
|
|
109
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
110
|
+
console.log(`๐งน Cleaned up temp directory`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(`โ ๏ธ Could not cleanup temp directory: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ๐ Get Git metrics for a file
|
|
119
|
+
*/
|
|
120
|
+
function getGitMetrics(filePath, projectRoot) {
|
|
121
|
+
try {
|
|
122
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
123
|
+
|
|
124
|
+
// Get commit count
|
|
125
|
+
const commitCount = execSync(
|
|
126
|
+
`git -C "${projectRoot}" log --oneline -- "${relativePath}" | wc -l`,
|
|
127
|
+
{ encoding: 'utf8' }
|
|
128
|
+
).trim();
|
|
129
|
+
|
|
130
|
+
// Get last commit info
|
|
131
|
+
const lastCommitInfo = execSync(
|
|
132
|
+
`git -C "${projectRoot}" log -1 --format="%H|%an|%ae|%ai|%s" -- "${relativePath}"`,
|
|
133
|
+
{ encoding: 'utf8' }
|
|
134
|
+
).trim();
|
|
135
|
+
|
|
136
|
+
if (lastCommitInfo) {
|
|
137
|
+
const [hash, author, email, date, message] = lastCommitInfo.split('|');
|
|
138
|
+
const commitDate = new Date(date);
|
|
139
|
+
const daysAgo = Math.floor((Date.now() - commitDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
commitCount: parseInt(commitCount) || 0,
|
|
143
|
+
lastCommit: {
|
|
144
|
+
hash: hash.substring(0, 7),
|
|
145
|
+
author,
|
|
146
|
+
email,
|
|
147
|
+
date: commitDate.toISOString(),
|
|
148
|
+
message: message || '',
|
|
149
|
+
daysAgo
|
|
150
|
+
},
|
|
151
|
+
isGitTracked: true
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// File not in git or git not available
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
commitCount: 0,
|
|
160
|
+
lastCommit: null,
|
|
161
|
+
isGitTracked: false
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* ๐ Find all source files
|
|
167
|
+
*/
|
|
168
|
+
function findSourceFiles(dir, includes, excludes) {
|
|
169
|
+
const files = [];
|
|
170
|
+
|
|
171
|
+
function walk(currentDir) {
|
|
172
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
173
|
+
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
176
|
+
const relativePath = path.relative(dir, fullPath);
|
|
177
|
+
|
|
178
|
+
// Skip excluded patterns
|
|
179
|
+
if (excludes.some(pattern => relativePath.includes(pattern))) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (entry.isDirectory()) {
|
|
184
|
+
walk(fullPath);
|
|
185
|
+
} else if (entry.isFile()) {
|
|
186
|
+
const ext = path.extname(entry.name);
|
|
187
|
+
if (['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
|
|
188
|
+
// Check if file is in included patterns
|
|
189
|
+
if (includes.length === 0 || includes.some(pattern => relativePath.startsWith(pattern))) {
|
|
190
|
+
files.push(fullPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
walk(dir);
|
|
198
|
+
return files;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* ๐ Parse TypeScript/JavaScript file using TS compiler API
|
|
203
|
+
*/
|
|
204
|
+
function parseWithTypeScript(filePath, content) {
|
|
205
|
+
const ext = path.extname(filePath);
|
|
206
|
+
const isTypeScript = ['.ts', '.tsx'].includes(ext);
|
|
207
|
+
|
|
208
|
+
const sourceFile = ts.createSourceFile(
|
|
209
|
+
filePath,
|
|
210
|
+
content,
|
|
211
|
+
ts.ScriptTarget.Latest,
|
|
212
|
+
true
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const result = { classes: [], interfaces: [] };
|
|
216
|
+
|
|
217
|
+
function visit(node) {
|
|
218
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
219
|
+
const className = node.name.getText(sourceFile);
|
|
220
|
+
const classInfo = { name: className, extends: null, implements: [], methods: [] };
|
|
221
|
+
|
|
222
|
+
if (node.heritageClauses) {
|
|
223
|
+
for (const clause of node.heritageClauses) {
|
|
224
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
225
|
+
classInfo.extends = clause.types[0].expression.getText(sourceFile);
|
|
226
|
+
} else if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
|
|
227
|
+
classInfo.implements = clause.types.map(type =>
|
|
228
|
+
type.expression.getText(sourceFile)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
node.members.forEach(member => {
|
|
235
|
+
if (ts.isMethodDeclaration(member) && member.name) {
|
|
236
|
+
classInfo.methods.push({
|
|
237
|
+
name: member.name.getText(sourceFile),
|
|
238
|
+
visibility: 'public',
|
|
239
|
+
type: 'method'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
result.classes.push(classInfo);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isTypeScript && ts.isInterfaceDeclaration(node) && node.name) {
|
|
248
|
+
const interfaceName = node.name.getText(sourceFile);
|
|
249
|
+
const ifaceInfo = { name: interfaceName, extends: [] };
|
|
250
|
+
|
|
251
|
+
if (node.heritageClauses) {
|
|
252
|
+
for (const clause of node.heritageClauses) {
|
|
253
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
254
|
+
ifaceInfo.extends = clause.types.map(type =>
|
|
255
|
+
type.expression.getText(sourceFile)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
result.interfaces.push(ifaceInfo);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
ts.forEachChild(node, visit);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
visit(sourceFile);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* ๐ Analyze a single file (Enhanced with TypeScript AST parsing)
|
|
273
|
+
*/
|
|
274
|
+
function analyzeFile(filePath, projectRoot) {
|
|
275
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
276
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
277
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
278
|
+
const packagePath = path.dirname(relativePath);
|
|
279
|
+
|
|
280
|
+
// Parse with TypeScript compiler API
|
|
281
|
+
const tsResults = parseWithTypeScript(filePath, content);
|
|
282
|
+
|
|
283
|
+
// Extract imports
|
|
284
|
+
const dependencies = [];
|
|
285
|
+
const importRegex = /import\s+(?:{[^}]+}|[\w]+|\*\s+as\s+\w+)?\s*(?:,\s*{[^}]+})?\s*from\s+['"]([^'"]+)['"]/g;
|
|
286
|
+
let match;
|
|
287
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
288
|
+
const importPath = match[1];
|
|
289
|
+
// Only track local imports
|
|
290
|
+
if (importPath.startsWith('.') || importPath.startsWith('/')) {
|
|
291
|
+
const depName = path.basename(importPath, path.extname(importPath));
|
|
292
|
+
if (!dependencies.includes(depName)) {
|
|
293
|
+
dependencies.push(depName);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Extract React component or class/function
|
|
299
|
+
const isReactComponent = /export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/.test(content) &&
|
|
300
|
+
(content.includes('import React') || content.includes('from \'react\''));
|
|
301
|
+
|
|
302
|
+
// Use TypeScript parser results if available, otherwise fallback to regex
|
|
303
|
+
let name = fileName;
|
|
304
|
+
let extendsClass = null;
|
|
305
|
+
let implementsInterfaces = [];
|
|
306
|
+
let methods = [];
|
|
307
|
+
|
|
308
|
+
if (tsResults.classes.length > 0) {
|
|
309
|
+
const mainClass = tsResults.classes[0];
|
|
310
|
+
name = mainClass.name;
|
|
311
|
+
extendsClass = mainClass.extends;
|
|
312
|
+
implementsInterfaces = mainClass.implements || [];
|
|
313
|
+
methods = mainClass.methods;
|
|
314
|
+
} else {
|
|
315
|
+
const componentMatch = content.match(/export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/);
|
|
316
|
+
name = componentMatch ? componentMatch[1] : fileName;
|
|
317
|
+
|
|
318
|
+
// Regex fallback for extends/implements
|
|
319
|
+
const extendsMatch = content.match(/class\s+\w+\s+extends\s+(\w+)/);
|
|
320
|
+
if (extendsMatch) extendsClass = extendsMatch[1];
|
|
321
|
+
|
|
322
|
+
const implementsMatch = content.match(/class\s+\w+\s+implements\s+([\w,\s]+)/);
|
|
323
|
+
if (implementsMatch) implementsInterfaces = implementsMatch[1].split(',').map(s => s.trim());
|
|
324
|
+
|
|
325
|
+
const methodMatches = content.match(/(?:function\s+\w+|const\s+\w+\s*=\s*(?:async\s+)?\([^)]*\)\s*=>|^\s*\w+\s*\([^)]*\)\s*{)/gm) || [];
|
|
326
|
+
methods = methodMatches.map((m, i) => ({
|
|
327
|
+
name: m.trim().split(/[\s(]/)[1] || `method_${i}`,
|
|
328
|
+
visibility: 'public',
|
|
329
|
+
type: 'method'
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Calculate complexity (simple metric: conditionals + loops)
|
|
334
|
+
const cyclomaticComplexity = (content.match(/\b(if|else|for|while|switch|case|catch)\b/g) || []).length;
|
|
335
|
+
|
|
336
|
+
// Get git metrics
|
|
337
|
+
const gitMetrics = getGitMetrics(filePath, projectRoot);
|
|
338
|
+
|
|
339
|
+
// Get file stats
|
|
340
|
+
const stats = fs.statSync(filePath);
|
|
341
|
+
const lines = content.split('\n').length;
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
id: `component_${Math.random().toString(36).substring(2, 9)}`,
|
|
345
|
+
name,
|
|
346
|
+
type: 'class',
|
|
347
|
+
subtype: isReactComponent ? 'react_component' : 'utility',
|
|
348
|
+
package: packagePath || 'root',
|
|
349
|
+
filePath: relativePath,
|
|
350
|
+
methods,
|
|
351
|
+
fields: [],
|
|
352
|
+
dependencies,
|
|
353
|
+
extends: extendsClass ? [extendsClass] : [],
|
|
354
|
+
implements: implementsInterfaces,
|
|
355
|
+
complexity: cyclomaticComplexity, // Top-level for compatibility
|
|
356
|
+
complexityMetrics: {
|
|
357
|
+
cyclomaticComplexity,
|
|
358
|
+
cognitiveComplexity: cyclomaticComplexity, // Simplified - would need proper calculation
|
|
359
|
+
nestingDepth: 0, // Placeholder
|
|
360
|
+
linesOfCode: lines,
|
|
361
|
+
methodCount: methods.length,
|
|
362
|
+
threatLevel: cyclomaticComplexity > 15 ? 'CRITICAL' : cyclomaticComplexity > 10 ? 'HIGH' : cyclomaticComplexity > 5 ? 'MODERATE' : 'LOW',
|
|
363
|
+
threatColor: cyclomaticComplexity > 15 ? 'red' : cyclomaticComplexity > 10 ? 'orange' : cyclomaticComplexity > 5 ? 'yellow' : 'green',
|
|
364
|
+
label: cyclomaticComplexity > 15 ? 'CRITICAL' : cyclomaticComplexity > 10 ? 'HIGH' : cyclomaticComplexity > 5 ? 'MODERATE' : 'LOW',
|
|
365
|
+
suggestions: []
|
|
366
|
+
},
|
|
367
|
+
coverageMetrics: {
|
|
368
|
+
hasCoverage: false,
|
|
369
|
+
overallCoverage: 0
|
|
370
|
+
},
|
|
371
|
+
metrics: {
|
|
372
|
+
lines,
|
|
373
|
+
complexity: cyclomaticComplexity,
|
|
374
|
+
methodCount: methods.length,
|
|
375
|
+
coverage: 0
|
|
376
|
+
},
|
|
377
|
+
gitMetrics,
|
|
378
|
+
testMetrics: {
|
|
379
|
+
exists: fs.existsSync(filePath.replace(/\.(jsx?|tsx?)$/, '.test$1')),
|
|
380
|
+
coverage: 0
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* ๐๏ธ Generate UML data structure
|
|
387
|
+
*/
|
|
388
|
+
function generateUML(projectPath, projectName) {
|
|
389
|
+
console.log(`๐ Analyzing project: ${projectPath}`);
|
|
390
|
+
console.log(`๐ฆ Include patterns: ${includePatterns.join(', ')}`);
|
|
391
|
+
console.log(`๐ซ Exclude patterns: ${excludePatterns.join(', ')}`);
|
|
392
|
+
|
|
393
|
+
// Find all source files
|
|
394
|
+
const files = findSourceFiles(projectPath, includePatterns, excludePatterns);
|
|
395
|
+
console.log(`๐ Found ${files.length} source files`);
|
|
396
|
+
|
|
397
|
+
// Analyze each file
|
|
398
|
+
const classes = [];
|
|
399
|
+
const packages = new Map();
|
|
400
|
+
|
|
401
|
+
for (const filePath of files) {
|
|
402
|
+
try {
|
|
403
|
+
const classData = analyzeFile(filePath, projectPath);
|
|
404
|
+
classes.push(classData);
|
|
405
|
+
|
|
406
|
+
// Group by package
|
|
407
|
+
const pkgPath = classData.package;
|
|
408
|
+
if (!packages.has(pkgPath)) {
|
|
409
|
+
packages.set(pkgPath, {
|
|
410
|
+
id: `package_${Math.random().toString(36).substring(2, 9)}`,
|
|
411
|
+
name: pkgPath.split('/').pop() || 'root',
|
|
412
|
+
path: pkgPath,
|
|
413
|
+
classes: []
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
packages.get(pkgPath).classes.push(classData.id);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.warn(`โ ๏ธ Error analyzing ${filePath}: ${error.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ๐ CREATE STUB CLASSES FOR EXTERNAL DEPENDENCIES
|
|
423
|
+
// Find all classes referenced in extends/implements but not defined in codebase
|
|
424
|
+
const definedClasses = new Set(classes.map(c => c.name));
|
|
425
|
+
const externalClasses = new Set();
|
|
426
|
+
|
|
427
|
+
classes.forEach(classData => {
|
|
428
|
+
// Check extends
|
|
429
|
+
if (classData.extends && classData.extends.length > 0) {
|
|
430
|
+
classData.extends.forEach(parentClass => {
|
|
431
|
+
if (!definedClasses.has(parentClass)) {
|
|
432
|
+
externalClasses.add(parentClass);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Check implements
|
|
438
|
+
if (classData.implements && classData.implements.length > 0) {
|
|
439
|
+
classData.implements.forEach(interfaceName => {
|
|
440
|
+
if (!definedClasses.has(interfaceName)) {
|
|
441
|
+
externalClasses.add(interfaceName);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Create stub classes for external dependencies
|
|
448
|
+
if (externalClasses.size > 0) {
|
|
449
|
+
console.log(`๐ฆ Creating ${externalClasses.size} stub classes for external dependencies`);
|
|
450
|
+
|
|
451
|
+
// Create or get external package
|
|
452
|
+
const externalPkgPath = 'external';
|
|
453
|
+
if (!packages.has(externalPkgPath)) {
|
|
454
|
+
packages.set(externalPkgPath, {
|
|
455
|
+
id: 'package_external',
|
|
456
|
+
name: 'External Libraries',
|
|
457
|
+
path: externalPkgPath,
|
|
458
|
+
classes: []
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
externalClasses.forEach(className => {
|
|
463
|
+
const stubClass = {
|
|
464
|
+
id: `external_${className.replace(/\./g, '_').toLowerCase()}`,
|
|
465
|
+
name: className,
|
|
466
|
+
type: 'class',
|
|
467
|
+
subtype: 'external',
|
|
468
|
+
package: externalPkgPath,
|
|
469
|
+
filePath: `external/${className}`,
|
|
470
|
+
methods: [],
|
|
471
|
+
fields: [],
|
|
472
|
+
dependencies: [],
|
|
473
|
+
extends: [],
|
|
474
|
+
implements: [],
|
|
475
|
+
complexity: 0,
|
|
476
|
+
complexityMetrics: {
|
|
477
|
+
cyclomaticComplexity: 0,
|
|
478
|
+
cognitiveComplexity: 0,
|
|
479
|
+
nestingDepth: 0,
|
|
480
|
+
linesOfCode: 75, // Give external stubs modest height (75 lines = ~1.5 units)
|
|
481
|
+
methodCount: 0,
|
|
482
|
+
threatLevel: 'EXTERNAL',
|
|
483
|
+
threatColor: 'gray',
|
|
484
|
+
label: 'External Library',
|
|
485
|
+
suggestions: []
|
|
486
|
+
},
|
|
487
|
+
coverageMetrics: {
|
|
488
|
+
hasCoverage: false,
|
|
489
|
+
overallCoverage: 0
|
|
490
|
+
},
|
|
491
|
+
metrics: {
|
|
492
|
+
lines: 75,
|
|
493
|
+
complexity: 0,
|
|
494
|
+
methodCount: 0,
|
|
495
|
+
coverage: 0
|
|
496
|
+
},
|
|
497
|
+
isExternal: true
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
classes.push(stubClass);
|
|
501
|
+
packages.get(externalPkgPath).classes.push(stubClass.id);
|
|
502
|
+
console.log(` โ
Created stub for ${className}`);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Get project metadata
|
|
507
|
+
let projectDescription = 'Codebase visualization';
|
|
508
|
+
let projectLanguage = 'JavaScript';
|
|
509
|
+
|
|
510
|
+
// Try to read package.json
|
|
511
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
512
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
513
|
+
try {
|
|
514
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
515
|
+
projectName = packageJson.name || projectName;
|
|
516
|
+
projectDescription = packageJson.description || projectDescription;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.warn(`โ ๏ธ Could not read package.json: ${error.message}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Build UML structure
|
|
523
|
+
return {
|
|
524
|
+
version: '6.0',
|
|
525
|
+
generated: new Date().toISOString(),
|
|
526
|
+
project: {
|
|
527
|
+
name: projectName,
|
|
528
|
+
description: projectDescription,
|
|
529
|
+
language: projectLanguage
|
|
530
|
+
},
|
|
531
|
+
packages: Array.from(packages.values()),
|
|
532
|
+
classes
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* ๐ Main execution
|
|
538
|
+
*/
|
|
539
|
+
function main() {
|
|
540
|
+
console.log('๐โก SWARMDESK UML GENERATOR');
|
|
541
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n');
|
|
542
|
+
|
|
543
|
+
let workingPath = targetPath;
|
|
544
|
+
let isTemp = false;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
// Handle GitHub URLs
|
|
548
|
+
if (isGitHubUrl(targetPath)) {
|
|
549
|
+
workingPath = cloneRepository(targetPath);
|
|
550
|
+
isTemp = true;
|
|
551
|
+
} else {
|
|
552
|
+
// Resolve local path
|
|
553
|
+
workingPath = path.resolve(targetPath);
|
|
554
|
+
if (!fs.existsSync(workingPath)) {
|
|
555
|
+
throw new Error(`Path does not exist: ${workingPath}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Extract project name
|
|
560
|
+
const projectName = path.basename(workingPath);
|
|
561
|
+
|
|
562
|
+
// Generate UML
|
|
563
|
+
const umlData = generateUML(workingPath, projectName);
|
|
564
|
+
|
|
565
|
+
// Determine output file
|
|
566
|
+
if (!outputFile) {
|
|
567
|
+
outputFile = path.join(process.cwd(), `${projectName}-uml.json`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Write output
|
|
571
|
+
fs.writeFileSync(outputFile, JSON.stringify(umlData, null, 2));
|
|
572
|
+
|
|
573
|
+
console.log('\nโจ UML Generation Complete!');
|
|
574
|
+
console.log(`๐ Classes analyzed: ${umlData.classes.length}`);
|
|
575
|
+
console.log(`๐ฆ Packages: ${umlData.packages.length}`);
|
|
576
|
+
console.log(`๐พ Output file: ${outputFile}`);
|
|
577
|
+
console.log('\n๐ฎ Load this file in SwarmDesk to visualize in 3D!');
|
|
578
|
+
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error(`\nโ Error: ${error.message}`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
} finally {
|
|
583
|
+
// Cleanup temp directory if needed
|
|
584
|
+
if (isTemp) {
|
|
585
|
+
cleanupTemp(workingPath);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Run if called directly
|
|
591
|
+
if (require.main === module) {
|
|
592
|
+
// Check if running in interactive mode (no arguments) or CLI mode (with arguments)
|
|
593
|
+
const hasCliArgs = process.argv.length > 2;
|
|
594
|
+
|
|
595
|
+
if (!hasCliArgs && process.stdin.isTTY) {
|
|
596
|
+
// No arguments and in a TTY โ Launch TUI mode
|
|
597
|
+
try {
|
|
598
|
+
const tui = require('./tui.js');
|
|
599
|
+
tui.main().catch(error => {
|
|
600
|
+
console.error(`Fatal error: ${error.message}`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
});
|
|
603
|
+
} catch (error) {
|
|
604
|
+
console.error('TUI dependencies not installed. Run: npm install');
|
|
605
|
+
console.error('Falling back to CLI mode. Use --help for usage.');
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
// Arguments provided โ Use CLI mode (backwards compatible)
|
|
610
|
+
main();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
module.exports = { generateUML, analyzeFile, findSourceFiles };
|