@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/tui.js ADDED
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * šŸŽ®āš” SWARMDESK UML GENERATOR - TUI MODE
4
+ * Interactive Text User Interface for SwarmDesk UML Generation
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const inquirer = require('inquirer');
10
+ const ora = require('ora');
11
+ const chalk = require('chalk');
12
+ const Table = require('cli-table3');
13
+ const boxen = require('boxen');
14
+ const gradient = require('gradient-string');
15
+ const figlet = require('figlet');
16
+ const { generateUML, analyzeFile, findSourceFiles } = require('./uml-generator.js');
17
+
18
+ /**
19
+ * šŸŽØ Display fancy welcome banner
20
+ */
21
+ function showBanner() {
22
+ console.clear();
23
+ const banner = figlet.textSync('SWARMDESK', {
24
+ font: 'ANSI Shadow',
25
+ horizontalLayout: 'default'
26
+ });
27
+ console.log(gradient.pastel.multiline(banner));
28
+ console.log(chalk.cyan('━'.repeat(80)));
29
+ console.log(chalk.white.bold(' šŸ” UML City Builder - Interactive Mode'));
30
+ console.log(chalk.cyan('━'.repeat(80)));
31
+ console.log();
32
+ }
33
+
34
+ /**
35
+ * šŸ  Suggest recent/common projects
36
+ */
37
+ function findSuggestedProjects() {
38
+ const suggestions = [];
39
+ const homeDir = require('os').homedir();
40
+
41
+ // Common project locations
42
+ const commonPaths = [
43
+ path.join(homeDir, 'lab', 'madness_interactive', 'projects'),
44
+ path.join(homeDir, 'projects'),
45
+ path.join(homeDir, 'dev'),
46
+ path.join(homeDir, 'Documents', 'projects'),
47
+ process.cwd()
48
+ ];
49
+
50
+ for (const basePath of commonPaths) {
51
+ if (fs.existsSync(basePath)) {
52
+ try {
53
+ const entries = fs.readdirSync(basePath, { withFileTypes: true });
54
+ for (const entry of entries.slice(0, 5)) { // Limit to 5 per location
55
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
56
+ const fullPath = path.join(basePath, entry.name);
57
+ // Check if it has package.json (likely a project)
58
+ if (fs.existsSync(path.join(fullPath, 'package.json'))) {
59
+ suggestions.push({
60
+ name: `${entry.name} (${basePath})`,
61
+ value: fullPath,
62
+ short: entry.name
63
+ });
64
+ }
65
+ }
66
+ }
67
+ } catch (err) {
68
+ // Skip directories we can't read
69
+ }
70
+ }
71
+ }
72
+
73
+ return suggestions.slice(0, 10); // Return top 10
74
+ }
75
+
76
+ /**
77
+ * šŸ“ Main menu - Project selection
78
+ */
79
+ async function selectProject() {
80
+ const suggestions = findSuggestedProjects();
81
+
82
+ const choices = [
83
+ { name: 'šŸ“‚ Browse for local directory...', value: 'browse' },
84
+ { name: '🌐 Clone from GitHub URL...', value: 'github' },
85
+ { name: 'šŸ“ Use current directory', value: process.cwd() },
86
+ new inquirer.Separator('─── Suggested Projects ───')
87
+ ];
88
+
89
+ if (suggestions.length > 0) {
90
+ choices.push(...suggestions);
91
+ }
92
+
93
+ choices.push(
94
+ new inquirer.Separator('───────────────────────'),
95
+ { name: 'āŒ Exit', value: 'exit' }
96
+ );
97
+
98
+ const { project } = await inquirer.prompt([
99
+ {
100
+ type: 'list',
101
+ name: 'project',
102
+ message: 'Select a project to analyze:',
103
+ choices,
104
+ pageSize: 15
105
+ }
106
+ ]);
107
+
108
+ if (project === 'exit') {
109
+ console.log(chalk.yellow('\nšŸ‘‹ Exiting SwarmDesk UML Generator\n'));
110
+ process.exit(0);
111
+ }
112
+
113
+ if (project === 'browse') {
114
+ const { customPath } = await inquirer.prompt([
115
+ {
116
+ type: 'input',
117
+ name: 'customPath',
118
+ message: 'Enter path to project:',
119
+ default: process.cwd(),
120
+ validate: (input) => {
121
+ if (fs.existsSync(input)) {
122
+ return true;
123
+ }
124
+ return 'Path does not exist. Please enter a valid path.';
125
+ }
126
+ }
127
+ ]);
128
+ return customPath;
129
+ }
130
+
131
+ if (project === 'github') {
132
+ const { githubUrl } = await inquirer.prompt([
133
+ {
134
+ type: 'input',
135
+ name: 'githubUrl',
136
+ message: 'Enter GitHub repository URL:',
137
+ validate: (input) => {
138
+ if (input.startsWith('http') || input.startsWith('git@')) {
139
+ return true;
140
+ }
141
+ return 'Please enter a valid GitHub URL (https://github.com/...)';
142
+ }
143
+ }
144
+ ]);
145
+ return githubUrl;
146
+ }
147
+
148
+ return project;
149
+ }
150
+
151
+ /**
152
+ * āš™ļø Configure analysis options
153
+ */
154
+ async function configureOptions(projectPath) {
155
+ const { customize } = await inquirer.prompt([
156
+ {
157
+ type: 'confirm',
158
+ name: 'customize',
159
+ message: 'Customize include/exclude patterns?',
160
+ default: false
161
+ }
162
+ ]);
163
+
164
+ let includePatterns = ['src', 'lib', 'components', 'pages', 'utils', 'hooks', 'services'];
165
+ let excludePatterns = ['node_modules', 'dist', 'build', '.git', 'coverage', 'test', '__tests__'];
166
+
167
+ if (customize) {
168
+ const { include, exclude } = await inquirer.prompt([
169
+ {
170
+ type: 'input',
171
+ name: 'include',
172
+ message: 'Include patterns (comma-separated):',
173
+ default: includePatterns.join(', ')
174
+ },
175
+ {
176
+ type: 'input',
177
+ name: 'exclude',
178
+ message: 'Exclude patterns (comma-separated):',
179
+ default: excludePatterns.join(', ')
180
+ }
181
+ ]);
182
+ includePatterns = include.split(',').map(s => s.trim());
183
+ excludePatterns = exclude.split(',').map(s => s.trim());
184
+ }
185
+
186
+ const projectName = path.basename(projectPath);
187
+ const { outputFile } = await inquirer.prompt([
188
+ {
189
+ type: 'input',
190
+ name: 'outputFile',
191
+ message: 'Output file name:',
192
+ default: `${projectName}-uml.json`
193
+ }
194
+ ]);
195
+
196
+ return {
197
+ includePatterns,
198
+ excludePatterns,
199
+ outputFile
200
+ };
201
+ }
202
+
203
+ /**
204
+ * šŸ—ļø Run analysis with progress indicators
205
+ */
206
+ async function runAnalysis(projectPath, options) {
207
+ console.log('\n');
208
+
209
+ // Step 1: Finding files
210
+ const findingSpinner = ora({
211
+ text: 'Scanning project files...',
212
+ color: 'cyan'
213
+ }).start();
214
+
215
+ const files = findSourceFiles(projectPath, options.includePatterns, options.excludePatterns);
216
+ findingSpinner.succeed(chalk.green(`Found ${files.length} source files`));
217
+
218
+ if (files.length === 0) {
219
+ console.log(boxen(chalk.yellow('āš ļø No source files found!\n\nTry adjusting your include patterns.'), {
220
+ padding: 1,
221
+ borderColor: 'yellow',
222
+ borderStyle: 'round'
223
+ }));
224
+ return null;
225
+ }
226
+
227
+ // Step 2: Analyzing files
228
+ const analyzingSpinner = ora({
229
+ text: 'Analyzing code structure...',
230
+ color: 'magenta'
231
+ }).start();
232
+
233
+ const classes = [];
234
+ const packages = new Map();
235
+ let analyzed = 0;
236
+
237
+ for (const filePath of files) {
238
+ try {
239
+ const classData = analyzeFile(filePath, projectPath);
240
+ classes.push(classData);
241
+
242
+ const pkgPath = classData.package;
243
+ if (!packages.has(pkgPath)) {
244
+ packages.set(pkgPath, {
245
+ id: `package_${Math.random().toString(36).substring(2, 9)}`,
246
+ name: pkgPath.split('/').pop() || 'root',
247
+ path: pkgPath,
248
+ classes: []
249
+ });
250
+ }
251
+ packages.get(pkgPath).classes.push(classData.id);
252
+
253
+ analyzed++;
254
+ if (analyzed % 10 === 0) {
255
+ analyzingSpinner.text = `Analyzing... ${analyzed}/${files.length} files`;
256
+ }
257
+ } catch (error) {
258
+ // Continue on error
259
+ }
260
+ }
261
+
262
+ analyzingSpinner.succeed(chalk.green(`Analyzed ${classes.length} components`));
263
+
264
+ // Step 3: Get project metadata
265
+ const metadataSpinner = ora({
266
+ text: 'Reading project metadata...',
267
+ color: 'blue'
268
+ }).start();
269
+
270
+ let projectName = path.basename(projectPath);
271
+ let projectDescription = 'Codebase visualization';
272
+ let projectLanguage = 'JavaScript';
273
+
274
+ const packageJsonPath = path.join(projectPath, 'package.json');
275
+ if (fs.existsSync(packageJsonPath)) {
276
+ try {
277
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
278
+ projectName = packageJson.name || projectName;
279
+ projectDescription = packageJson.description || projectDescription;
280
+ } catch (error) {
281
+ // Use defaults
282
+ }
283
+ }
284
+
285
+ metadataSpinner.succeed(chalk.green('Project metadata loaded'));
286
+
287
+ // Build UML structure
288
+ const umlData = {
289
+ version: '6.0',
290
+ generated: new Date().toISOString(),
291
+ project: {
292
+ name: projectName,
293
+ description: projectDescription,
294
+ language: projectLanguage
295
+ },
296
+ packages: Array.from(packages.values()),
297
+ classes
298
+ };
299
+
300
+ return umlData;
301
+ }
302
+
303
+ /**
304
+ * šŸ“Š Display results table
305
+ */
306
+ function displayResults(umlData) {
307
+ console.log('\n');
308
+ console.log(boxen(
309
+ chalk.bold.white('šŸŽ‰ Analysis Complete!'),
310
+ {
311
+ padding: 1,
312
+ margin: 1,
313
+ borderColor: 'green',
314
+ borderStyle: 'round'
315
+ }
316
+ ));
317
+
318
+ const table = new Table({
319
+ head: [chalk.cyan('Metric'), chalk.cyan('Value')],
320
+ style: {
321
+ head: [],
322
+ border: ['cyan']
323
+ }
324
+ });
325
+
326
+ table.push(
327
+ ['Project Name', chalk.white.bold(umlData.project.name)],
328
+ ['Description', chalk.gray(umlData.project.description)],
329
+ ['Components', chalk.green(umlData.classes.length.toString())],
330
+ ['Packages', chalk.yellow(umlData.packages.length.toString())],
331
+ ['Generated', chalk.gray(new Date(umlData.generated).toLocaleString())]
332
+ );
333
+
334
+ console.log(table.toString());
335
+
336
+ // Top 5 most complex components
337
+ const sortedByComplexity = [...umlData.classes]
338
+ .sort((a, b) => b.metrics.complexity - a.metrics.complexity)
339
+ .slice(0, 5);
340
+
341
+ if (sortedByComplexity.length > 0) {
342
+ console.log('\n' + chalk.bold('šŸ”„ Most Complex Components:'));
343
+ const complexityTable = new Table({
344
+ head: [chalk.cyan('Component'), chalk.cyan('Complexity'), chalk.cyan('Lines')],
345
+ style: { head: [], border: ['gray'] }
346
+ });
347
+
348
+ for (const cls of sortedByComplexity) {
349
+ const complexityColor = cls.metrics.complexity > 20 ? chalk.red :
350
+ cls.metrics.complexity > 10 ? chalk.yellow :
351
+ chalk.green;
352
+ complexityTable.push([
353
+ chalk.white(cls.name),
354
+ complexityColor(cls.metrics.complexity.toString()),
355
+ chalk.gray(cls.metrics.lines.toString())
356
+ ]);
357
+ }
358
+ console.log(complexityTable.toString());
359
+ }
360
+
361
+ // ASCII Art City Preview
362
+ console.log('\n' + chalk.bold('šŸ™ļø City Preview (Building Heights):'));
363
+ const maxHeight = Math.max(...umlData.classes.map(c => c.metrics.lines));
364
+ const buildings = umlData.classes.slice(0, 20); // First 20 buildings
365
+
366
+ let cityArt = '';
367
+ for (let i = 0; i < buildings.length; i++) {
368
+ const height = Math.ceil((buildings[i].metrics.lines / maxHeight) * 5);
369
+ const building = 'ā–ˆ'.repeat(height);
370
+ const color = buildings[i].metrics.complexity > 15 ? chalk.red :
371
+ buildings[i].metrics.complexity > 8 ? chalk.yellow :
372
+ chalk.green;
373
+ cityArt += color(building) + ' ';
374
+ }
375
+ console.log(' ' + cityArt);
376
+ console.log(chalk.gray(' (Taller = More Lines, Red = Complex, Green = Simple)'));
377
+ }
378
+
379
+ /**
380
+ * šŸ’¾ Save output
381
+ */
382
+ async function saveOutput(umlData, outputFile) {
383
+ const saveSpinner = ora({
384
+ text: `Saving to ${outputFile}...`,
385
+ color: 'cyan'
386
+ }).start();
387
+
388
+ try {
389
+ fs.writeFileSync(outputFile, JSON.stringify(umlData, null, 2));
390
+ saveSpinner.succeed(chalk.green(`Saved to ${outputFile}`));
391
+
392
+ console.log('\n' + boxen(
393
+ chalk.white.bold('šŸŽ® Next Steps:\n\n') +
394
+ chalk.gray('1. ') + chalk.white('Load in SwarmDesk 3D viewer\n') +
395
+ chalk.gray('2. ') + chalk.white('Press ') + chalk.cyan('I') + chalk.white(' to cycle data sources\n') +
396
+ chalk.gray('3. ') + chalk.white('Explore your code in 3D!'),
397
+ {
398
+ padding: 1,
399
+ margin: 1,
400
+ borderColor: 'cyan',
401
+ borderStyle: 'round'
402
+ }
403
+ ));
404
+
405
+ return true;
406
+ } catch (error) {
407
+ saveSpinner.fail(chalk.red(`Failed to save: ${error.message}`));
408
+ return false;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * šŸ” Ask if user wants to analyze another project
414
+ */
415
+ async function askContinue() {
416
+ const { again } = await inquirer.prompt([
417
+ {
418
+ type: 'confirm',
419
+ name: 'again',
420
+ message: '\nAnalyze another project?',
421
+ default: false
422
+ }
423
+ ]);
424
+ return again;
425
+ }
426
+
427
+ /**
428
+ * šŸš€ Main TUI flow
429
+ */
430
+ async function main() {
431
+ showBanner();
432
+
433
+ let continueAnalyzing = true;
434
+
435
+ while (continueAnalyzing) {
436
+ try {
437
+ // Step 1: Select project
438
+ const projectPath = await selectProject();
439
+
440
+ // Step 2: Configure options
441
+ const options = await configureOptions(projectPath);
442
+
443
+ // Step 3: Run analysis
444
+ const umlData = await runAnalysis(projectPath, options);
445
+
446
+ if (umlData) {
447
+ // Step 4: Display results
448
+ displayResults(umlData);
449
+
450
+ // Step 5: Save output
451
+ await saveOutput(umlData, options.outputFile);
452
+
453
+ // Step 6: Ask to continue
454
+ continueAnalyzing = await askContinue();
455
+ } else {
456
+ continueAnalyzing = await askContinue();
457
+ }
458
+
459
+ if (continueAnalyzing) {
460
+ console.clear();
461
+ showBanner();
462
+ }
463
+
464
+ } catch (error) {
465
+ if (error.isTtyError) {
466
+ console.error(chalk.red('\nāŒ Interactive mode not supported in this environment'));
467
+ process.exit(1);
468
+ } else if (error.message === 'User force closed the prompt') {
469
+ console.log(chalk.yellow('\n\nšŸ‘‹ Exiting...\n'));
470
+ process.exit(0);
471
+ } else {
472
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
473
+ continueAnalyzing = await askContinue();
474
+ }
475
+ }
476
+ }
477
+
478
+ console.log(chalk.yellow('\nšŸ‘‹ Thanks for using SwarmDesk UML Generator!\n'));
479
+ console.log(chalk.gray('šŸ§™ā€ā™‚ļø From the Mad Laboratory with ') + chalk.red('ā¤ļø\n'));
480
+ }
481
+
482
+ module.exports = { main };
483
+
484
+ // Run if called directly
485
+ if (require.main === module) {
486
+ main().catch(error => {
487
+ console.error(chalk.red(`Fatal error: ${error.message}`));
488
+ process.exit(1);
489
+ });
490
+ }