@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/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
|
+
}
|