@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.
@@ -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 };