@brainwav/diagram 1.0.3 → 1.0.5
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/README.md +57 -3
- package/package.json +1 -1
- package/src/diagram.js +681 -4
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Generate codebase architecture diagrams from source files. No AI required.
|
|
|
11
11
|
- [First-run checklist](#first-run-checklist)
|
|
12
12
|
- [Commands](#commands)
|
|
13
13
|
- [Diagram types](#diagram-types)
|
|
14
|
+
- [AI-focused diagram outputs](#ai-focused-diagram-outputs)
|
|
14
15
|
- [Output formats](#output-formats)
|
|
15
16
|
- [Video and animation prerequisites](#video-and-animation-prerequisites)
|
|
16
17
|
- [Architecture Testing](#architecture-testing)
|
|
@@ -119,7 +120,7 @@ diagram generate . --open
|
|
|
119
120
|
|
|
120
121
|
Options:
|
|
121
122
|
|
|
122
|
-
- `-t, --type <type>` `architecture|sequence|dependency|class|flow` (default: `architecture`)
|
|
123
|
+
- `-t, --type <type>` `architecture|sequence|dependency|class|flow|database|user|events|auth|security` (default: `architecture`)
|
|
123
124
|
- `-f, --focus <module>` focus on one module or directory
|
|
124
125
|
- `-o, --output <file>` write `.mmd`, `.svg`, or `.png`
|
|
125
126
|
- `-m, --max-files <n>` max files to analyze
|
|
@@ -139,6 +140,23 @@ Options:
|
|
|
139
140
|
|
|
140
141
|
- `-o, --output-dir <dir>` output directory (default: `./diagrams`)
|
|
141
142
|
|
|
143
|
+
### `diagram manifest [path]`
|
|
144
|
+
|
|
145
|
+
Summarize the generated `.diagram/manifest.json` artifact.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
diagram manifest .
|
|
149
|
+
diagram manifest . --manifest-dir .diagram --output .diagram/manifest-summary.json
|
|
150
|
+
diagram manifest . --manifest-dir .diagram --require-types architecture,security --fail-on-placeholder
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Options:
|
|
154
|
+
|
|
155
|
+
- `-d, --manifest-dir <dir>` directory containing `manifest.json` (default: `.diagram`)
|
|
156
|
+
- `-o, --output <file>` write summary JSON to file
|
|
157
|
+
- `--require-types <list>` require specific diagram types, comma-separated
|
|
158
|
+
- `--fail-on-placeholder` fail if any diagram entry is a placeholder
|
|
159
|
+
|
|
142
160
|
### `diagram video [path]`
|
|
143
161
|
|
|
144
162
|
Generate an animated video (`.mp4`, `.webm`, `.mov`) from a Mermaid diagram.
|
|
@@ -151,7 +169,7 @@ diagram video . --duration 8 --fps 60 --width 1920 --height 1080
|
|
|
151
169
|
|
|
152
170
|
Options:
|
|
153
171
|
|
|
154
|
-
- `-t, --type <type>` `architecture|sequence|dependency|class|flow` (default: `architecture`)
|
|
172
|
+
- `-t, --type <type>` `architecture|sequence|dependency|class|flow|database|user|events|auth|security` (default: `architecture`)
|
|
155
173
|
- `-o, --output <file>` output file (default: `diagram.mp4`)
|
|
156
174
|
- `-d, --duration <sec>` video duration in seconds (default: `5`)
|
|
157
175
|
- `-f, --fps <n>` frames per second (default: `30`)
|
|
@@ -172,7 +190,7 @@ diagram animate . --theme forest
|
|
|
172
190
|
|
|
173
191
|
Options:
|
|
174
192
|
|
|
175
|
-
- `-t, --type <type>` `architecture|sequence|dependency|class|flow` (default: `architecture`)
|
|
193
|
+
- `-t, --type <type>` `architecture|sequence|dependency|class|flow|database|user|events|auth|security` (default: `architecture`)
|
|
176
194
|
- `-o, --output <file>` output file (default: `diagram-animated.svg`)
|
|
177
195
|
- `--theme <theme>` `default|dark|forest|neutral` (default: `dark`)
|
|
178
196
|
- `-m, --max-files <n>` max files to analyze (default: `100`)
|
|
@@ -186,6 +204,41 @@ Options:
|
|
|
186
204
|
| `dependency` | Internal and external imports | Dependency review |
|
|
187
205
|
| `class` | Class-oriented relationships | OOP-heavy codebases |
|
|
188
206
|
| `flow` | Process/data flow | Control-flow mapping |
|
|
207
|
+
| `database` | Database operations and condition paths | Conditional persistence flows |
|
|
208
|
+
| `user` | User-facing entrypoints and handlers | Interaction flow mapping |
|
|
209
|
+
| `events` | Event streams and async channels | Event-driven architecture |
|
|
210
|
+
| `auth` | Authentication and authorization checks | Credential/identity flow |
|
|
211
|
+
| `security` | Security boundaries and trust paths | Threat/risk analysis |
|
|
212
|
+
|
|
213
|
+
## AI-focused diagram outputs
|
|
214
|
+
|
|
215
|
+
For agent workflows, the Mermaid output is especially useful because it is
|
|
216
|
+
compact, textual, and structured. Feeding `.mmd` into an AI at startup lets it
|
|
217
|
+
understand architecture faster than reading all source files.
|
|
218
|
+
|
|
219
|
+
The generated types cover these high-value areas for automated reasoning:
|
|
220
|
+
|
|
221
|
+
- **Database Operations** — conditional record paths (for example "record exists?"
|
|
222
|
+
/ "not found" branches), storage and mutation decisions.
|
|
223
|
+
- **User Actions and Interactions** — user entrypoints and downstream handler
|
|
224
|
+
chains.
|
|
225
|
+
- **Events and Channels** — internal publishers, workers, listeners, and trigger
|
|
226
|
+
paths.
|
|
227
|
+
- **Authentication Flows** — step-by-step identity and credential checks.
|
|
228
|
+
- **Security and Data Flows** — trust boundaries, sensitive components, and
|
|
229
|
+
integrations to support security review and compliance context.
|
|
230
|
+
|
|
231
|
+
When reviewing PRs, run:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
diagram all . --output-dir .diagram
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
so `.diagram/` includes the new AI-oriented variants beside the classic ones.
|
|
238
|
+
|
|
239
|
+
The command also writes `.diagram/manifest.json` summarizing what diagrams were
|
|
240
|
+
produced and whether any outputs are placeholder/no-data (helpful for CI and
|
|
241
|
+
agent bootstrap checks).
|
|
189
242
|
|
|
190
243
|
## Output formats
|
|
191
244
|
|
|
@@ -194,6 +247,7 @@ Options:
|
|
|
194
247
|
- `.svg`/`.png` rendered images (requires Mermaid CLI)
|
|
195
248
|
- `.mp4`/`.webm`/`.mov` video export (requires Playwright + ffmpeg)
|
|
196
249
|
- Animated `.svg` export (requires Playwright)
|
|
250
|
+
- `.diagram/manifest.json` machine-readable artifact index
|
|
197
251
|
|
|
198
252
|
Install Mermaid CLI for image export:
|
|
199
253
|
|
package/package.json
CHANGED
package/src/diagram.js
CHANGED
|
@@ -226,6 +226,231 @@ function getExternalPackageName(importPath) {
|
|
|
226
226
|
return importPath.split('/')[0] || null;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
const ROLE_PATTERNS = {
|
|
230
|
+
user: [
|
|
231
|
+
'route', 'routes', 'controller', 'controllers', 'handler', 'handlers',
|
|
232
|
+
'api', 'middleware', 'page', 'pages', 'ui', 'frontend', 'web', 'client', 'request'
|
|
233
|
+
],
|
|
234
|
+
auth: [
|
|
235
|
+
'auth', 'authentication', 'authorization', 'session', 'signin', 'login',
|
|
236
|
+
'signup', 'token', 'jwt', 'oauth', 'sso', 'passport', 'identity', 'acl',
|
|
237
|
+
'guard', 'permission', 'password', 'mfa', 'security'
|
|
238
|
+
],
|
|
239
|
+
database: [
|
|
240
|
+
'db', 'database', 'data', 'datastore', 'repository', 'repo', 'model',
|
|
241
|
+
'schema', 'migration', 'query', 'querybuilder', 'prisma', 'typeorm',
|
|
242
|
+
'sequelize', 'mongoose', 'knex', 'drizzle', 'redis', 'postgres', 'mysql',
|
|
243
|
+
'sqlite', 'mongo', 'dynamodb', 'd1'
|
|
244
|
+
],
|
|
245
|
+
events: [
|
|
246
|
+
'event', 'events', 'queue', 'worker', 'cron', 'scheduler', 'webhook',
|
|
247
|
+
'pubsub', 'bus', 'publish', 'subscriber', 'consumer', 'producer',
|
|
248
|
+
'listener', 'trigger'
|
|
249
|
+
],
|
|
250
|
+
integrations: [
|
|
251
|
+
'integration', 'webhook', 'gateway', 'stripe', 'pay', 'sendgrid', 'twilio',
|
|
252
|
+
'sentry', 'github', 'slack', 'analytics', 'mail', 'smtp', 'storage'
|
|
253
|
+
],
|
|
254
|
+
security: [
|
|
255
|
+
'security', 'threat', 'attack', 'rate', 'encrypt', 'decrypt', 'signature',
|
|
256
|
+
'hash', 'verify', 'csrf', 'xss', 'audit', 'compliance', 'policy', 'vault',
|
|
257
|
+
'kms', 'secret', 'key'
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const SUPPORTED_DIAGRAM_TYPES = Object.freeze([
|
|
262
|
+
'architecture',
|
|
263
|
+
'sequence',
|
|
264
|
+
'dependency',
|
|
265
|
+
'class',
|
|
266
|
+
'flow',
|
|
267
|
+
'database',
|
|
268
|
+
'user',
|
|
269
|
+
'events',
|
|
270
|
+
'auth',
|
|
271
|
+
'security',
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
function textHasToken(text, token) {
|
|
275
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
276
|
+
const re = new RegExp(`(^|[\\/._-])${escaped}([\\/._-]|$)`, 'i');
|
|
277
|
+
return re.test(text);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function collectExternalImports(importEntries) {
|
|
281
|
+
const packages = new Set();
|
|
282
|
+
if (!Array.isArray(importEntries)) return [];
|
|
283
|
+
|
|
284
|
+
for (const entry of importEntries) {
|
|
285
|
+
const importPath = getImportPath(entry);
|
|
286
|
+
if (!importPath || importPath.startsWith('.')) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const externalPackage = getExternalPackageName(importPath);
|
|
290
|
+
if (externalPackage) {
|
|
291
|
+
packages.add(externalPackage);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return [...packages];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function inferRoleTags(filePath, originalName, fileContent, importEntries, type) {
|
|
299
|
+
const content = (fileContent || '').toLowerCase();
|
|
300
|
+
const pathText = normalizePath(filePath || '').toLowerCase();
|
|
301
|
+
const nameText = (originalName || '').toLowerCase();
|
|
302
|
+
const externalImports = collectExternalImports(importEntries).join(' ').toLowerCase();
|
|
303
|
+
const combined = `${pathText} ${nameText} ${content} ${externalImports}`;
|
|
304
|
+
|
|
305
|
+
const tags = new Set();
|
|
306
|
+
|
|
307
|
+
for (const [tag, tokens] of Object.entries(ROLE_PATTERNS)) {
|
|
308
|
+
for (const token of tokens) {
|
|
309
|
+
if (textHasToken(combined, token)) {
|
|
310
|
+
tags.add(tag);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (type === 'service') {
|
|
317
|
+
tags.add('service');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (tags.size === 0) {
|
|
321
|
+
tags.add('general');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return [...tags];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function hasRole(component, role) {
|
|
328
|
+
return (Array.isArray(component.roleTags) && component.roleTags.includes(role));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function componentsByRole(components, role) {
|
|
332
|
+
if (!Array.isArray(components)) return [];
|
|
333
|
+
return components.filter((component) => hasRole(component, role));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getExternalPackageList(importEntries) {
|
|
337
|
+
const packages = collectExternalImports(importEntries);
|
|
338
|
+
if (!packages.length) return [];
|
|
339
|
+
return packages.map((pkg) => ({
|
|
340
|
+
name: pkg,
|
|
341
|
+
label: pkg,
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function mapSafeNames(components) {
|
|
346
|
+
const map = new Map();
|
|
347
|
+
const used = new Set();
|
|
348
|
+
|
|
349
|
+
for (const component of components) {
|
|
350
|
+
const rawName = sanitize(component.name || component.originalName || 'node');
|
|
351
|
+
if (!used.has(rawName)) {
|
|
352
|
+
map.set(component, rawName);
|
|
353
|
+
used.add(rawName);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let i = 1;
|
|
358
|
+
let candidate = `${rawName}_${i}`;
|
|
359
|
+
while (used.has(candidate)) {
|
|
360
|
+
i += 1;
|
|
361
|
+
candidate = `${rawName}_${i}`;
|
|
362
|
+
}
|
|
363
|
+
map.set(component, candidate);
|
|
364
|
+
used.add(candidate);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return map;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function byNameIndex(components) {
|
|
371
|
+
const map = new Map();
|
|
372
|
+
if (!Array.isArray(components)) return map;
|
|
373
|
+
for (const component of components) {
|
|
374
|
+
if (component && component.name) {
|
|
375
|
+
map.set(component.name, component);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return map;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveDependencyComponent(component, componentsByName, name) {
|
|
382
|
+
if (!component || !name || !componentsByName) return null;
|
|
383
|
+
return componentsByName.get(name) || null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function collectConnectedComponents(components, seedComponents, maxDepth = 2, maxNodes = 35) {
|
|
387
|
+
if (!Array.isArray(components)) return [];
|
|
388
|
+
if (!Array.isArray(seedComponents) || seedComponents.length === 0) return [];
|
|
389
|
+
|
|
390
|
+
const byName = byNameIndex(components);
|
|
391
|
+
const selected = new Map();
|
|
392
|
+
const queue = [];
|
|
393
|
+
|
|
394
|
+
for (const seed of seedComponents) {
|
|
395
|
+
if (seed && seed.name && !selected.has(seed.name)) {
|
|
396
|
+
selected.set(seed.name, seed);
|
|
397
|
+
queue.push(seed);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let depth = 0;
|
|
402
|
+
const visited = new Set();
|
|
403
|
+
while (queue.length > 0 && depth < maxDepth) {
|
|
404
|
+
const levelSize = queue.length;
|
|
405
|
+
for (let i = 0; i < levelSize; i++) {
|
|
406
|
+
const current = queue.shift();
|
|
407
|
+
if (!current || typeof current.name !== 'string') continue;
|
|
408
|
+
const depthKey = `${current.name}:${depth}`;
|
|
409
|
+
if (visited.has(depthKey)) continue;
|
|
410
|
+
visited.add(depthKey);
|
|
411
|
+
|
|
412
|
+
const next = [];
|
|
413
|
+
for (const depName of current.dependencies || []) {
|
|
414
|
+
const dependency = byName.get(depName);
|
|
415
|
+
if (dependency && !selected.has(depName)) {
|
|
416
|
+
selected.set(depName, dependency);
|
|
417
|
+
next.push(dependency);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const candidate of components) {
|
|
422
|
+
if (selected.has(candidate.name)) continue;
|
|
423
|
+
const reverseDependencies = Array.isArray(candidate.dependencies) ? candidate.dependencies : [];
|
|
424
|
+
if (reverseDependencies.includes(current.name)) {
|
|
425
|
+
selected.set(candidate.name, candidate);
|
|
426
|
+
next.push(candidate);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const n of next) {
|
|
431
|
+
if (selected.size >= maxNodes) break;
|
|
432
|
+
queue.push(n);
|
|
433
|
+
}
|
|
434
|
+
if (selected.size >= maxNodes) break;
|
|
435
|
+
}
|
|
436
|
+
depth += 1;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return [...selected.values()];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function inferDbIntent(component) {
|
|
443
|
+
const source = `${component.filePath || ''} ${component.originalName || ''} ${component.name || ''}`.toLowerCase();
|
|
444
|
+
const hasLookup = /(read|find|query|select|get|lookup|exists|fetch)/.test(source);
|
|
445
|
+
const hasWrite = /(create|insert|update|upsert|save|delete|remove|write|transaction)/.test(source);
|
|
446
|
+
return { hasLookup, hasWrite };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function classifyAsGeneral(component) {
|
|
450
|
+
if (!component || !Array.isArray(component.roleTags)) return false;
|
|
451
|
+
return component.roleTags.includes('general') && component.roleTags.length === 1;
|
|
452
|
+
}
|
|
453
|
+
|
|
229
454
|
// Analysis
|
|
230
455
|
async function analyze(rootPath, options) {
|
|
231
456
|
// Validate maxFiles with strict parsing
|
|
@@ -306,12 +531,16 @@ async function analyze(rootPath, options) {
|
|
|
306
531
|
}
|
|
307
532
|
seenNames.add(uniqueName);
|
|
308
533
|
|
|
534
|
+
const imports = extractImportsWithPositions(content, lang);
|
|
535
|
+
const type = inferType(filePath, content);
|
|
536
|
+
|
|
309
537
|
components.push({
|
|
310
538
|
name: uniqueName,
|
|
311
539
|
originalName: baseName,
|
|
312
540
|
filePath: rel,
|
|
313
|
-
type
|
|
314
|
-
imports
|
|
541
|
+
type,
|
|
542
|
+
imports,
|
|
543
|
+
roleTags: inferRoleTags(rel, baseName, content, imports, type),
|
|
315
544
|
directory: dir,
|
|
316
545
|
});
|
|
317
546
|
} catch (e) {
|
|
@@ -563,6 +792,282 @@ function generateFlow(data) {
|
|
|
563
792
|
return lines.join('\n');
|
|
564
793
|
}
|
|
565
794
|
|
|
795
|
+
function generateDatabase(data) {
|
|
796
|
+
if (!data || !Array.isArray(data.components)) {
|
|
797
|
+
return 'flowchart TD\n Note["No data available"]';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const lines = ['flowchart TD'];
|
|
801
|
+
const seeds = componentsByRole(data.components, 'database');
|
|
802
|
+
if (seeds.length === 0) {
|
|
803
|
+
lines.push(' Note["No database-focused components found"]');
|
|
804
|
+
return lines.join('\n');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const connected = collectConnectedComponents(data.components, seeds, 2, 28);
|
|
808
|
+
const byName = byNameIndex(connected);
|
|
809
|
+
const safeNames = mapSafeNames(connected);
|
|
810
|
+
|
|
811
|
+
lines.push(' UserRequest["User request"]');
|
|
812
|
+
lines.push(' Decision{Record exists?}');
|
|
813
|
+
|
|
814
|
+
const addedEdges = new Set();
|
|
815
|
+
for (const comp of connected) {
|
|
816
|
+
if (!seeds.includes(comp)) continue;
|
|
817
|
+
const safe = safeNames.get(comp);
|
|
818
|
+
if (!safe) continue;
|
|
819
|
+
lines.push(` ${safe}["${escapeMermaid(comp.originalName)}"]`);
|
|
820
|
+
lines.push(` UserRequest --> ${safe}`);
|
|
821
|
+
|
|
822
|
+
const intent = inferDbIntent(comp);
|
|
823
|
+
if (intent.hasLookup) {
|
|
824
|
+
const lookup = `${safe}_lookup`;
|
|
825
|
+
const create = `${safe}_create`;
|
|
826
|
+
const update = `${safe}_update`;
|
|
827
|
+
lines.push(` ${safe} --> ${lookup}["lookup query"]`);
|
|
828
|
+
lines.push(` ${lookup} --> Decision`);
|
|
829
|
+
lines.push(` Decision -->|found| ${update}["update or modify"]`);
|
|
830
|
+
lines.push(` Decision -->|not found| ${create}["insert/create"]`);
|
|
831
|
+
lines.push(` ${update} --> ${safe}_result["result"]`);
|
|
832
|
+
lines.push(` ${create} --> ${safe}_result["result"]`);
|
|
833
|
+
} else if (intent.hasWrite) {
|
|
834
|
+
const write = `${safe}_write`;
|
|
835
|
+
lines.push(` ${safe} --> ${write}["write/update"]`);
|
|
836
|
+
lines.push(` ${write} --> ${safe}_result["result"]`);
|
|
837
|
+
} else {
|
|
838
|
+
const result = `${safe}_result`;
|
|
839
|
+
lines.push(` ${safe} --> ${result}["result"]`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
for (const depName of comp.dependencies || []) {
|
|
843
|
+
const dep = byName.get(depName);
|
|
844
|
+
if (!dep || !safeNames.has(dep)) continue;
|
|
845
|
+
const edge = `${safe}->${safeNames.get(dep)}`;
|
|
846
|
+
if (!addedEdges.has(edge)) {
|
|
847
|
+
addedEdges.add(edge);
|
|
848
|
+
lines.push(` ${safe} --> ${safeNames.get(dep)}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
lines.push(' classDef dbNode fill:#0ea5e9,color:#fff');
|
|
854
|
+
lines.push(' classDef decisionNode fill:#0284c7,color:#fff');
|
|
855
|
+
return lines.join('\n');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function generateUserInteractions(data) {
|
|
859
|
+
if (!data || !Array.isArray(data.components)) {
|
|
860
|
+
return 'flowchart LR\n Note["No data available"]';
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const lines = ['flowchart LR'];
|
|
864
|
+
const seeds = componentsByRole(data.components, 'user');
|
|
865
|
+
if (seeds.length === 0) {
|
|
866
|
+
lines.push(' Note["No user-facing components found"]');
|
|
867
|
+
return lines.join('\n');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const connected = collectConnectedComponents(data.components, seeds, 1, 30);
|
|
871
|
+
const byName = byNameIndex(connected);
|
|
872
|
+
const safeNames = mapSafeNames(connected);
|
|
873
|
+
const edges = new Set();
|
|
874
|
+
|
|
875
|
+
lines.push(' User(("User"))');
|
|
876
|
+
for (const seed of seeds) {
|
|
877
|
+
const safe = safeNames.get(seed);
|
|
878
|
+
if (!safe) continue;
|
|
879
|
+
lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
|
|
880
|
+
lines.push(` User --> ${safe}`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
for (const comp of connected) {
|
|
884
|
+
const from = safeNames.get(comp);
|
|
885
|
+
if (!from) continue;
|
|
886
|
+
for (const depName of comp.dependencies || []) {
|
|
887
|
+
const dep = byName.get(depName);
|
|
888
|
+
if (!dep) continue;
|
|
889
|
+
const to = safeNames.get(dep);
|
|
890
|
+
if (!to) continue;
|
|
891
|
+
const key = `${from}->${to}`;
|
|
892
|
+
if (!edges.has(key)) {
|
|
893
|
+
edges.add(key);
|
|
894
|
+
lines.push(` ${from} --> ${to}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
lines.push(' classDef userNode fill:#16a34a,color:#fff');
|
|
900
|
+
return lines.join('\n');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function generateEvents(data) {
|
|
904
|
+
if (!data || !Array.isArray(data.components)) {
|
|
905
|
+
return 'flowchart TD\n Note["No data available"]';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const lines = ['flowchart TD'];
|
|
909
|
+
const seeds = componentsByRole(data.components, 'events');
|
|
910
|
+
if (seeds.length === 0) {
|
|
911
|
+
lines.push(' Note["No event/channels components found"]');
|
|
912
|
+
return lines.join('\n');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const connected = collectConnectedComponents(data.components, seeds, 2, 30);
|
|
916
|
+
const byName = byNameIndex(connected);
|
|
917
|
+
const safeNames = mapSafeNames(connected);
|
|
918
|
+
const edges = new Set();
|
|
919
|
+
|
|
920
|
+
lines.push(' subgraph Channels["Event channels / queues"]');
|
|
921
|
+
for (const component of connected) {
|
|
922
|
+
const safe = safeNames.get(component);
|
|
923
|
+
if (!safe) continue;
|
|
924
|
+
const isEventSource = seeds.includes(component);
|
|
925
|
+
if (isEventSource) {
|
|
926
|
+
lines.push(` ${safe}{{"${escapeMermaid(component.originalName)}"}}`);
|
|
927
|
+
} else {
|
|
928
|
+
lines.push(` ${safe}["${escapeMermaid(component.originalName)}"]`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
lines.push(' end');
|
|
932
|
+
|
|
933
|
+
for (const comp of connected) {
|
|
934
|
+
const from = safeNames.get(comp);
|
|
935
|
+
if (!from) continue;
|
|
936
|
+
for (const depName of comp.dependencies || []) {
|
|
937
|
+
const dep = byName.get(depName);
|
|
938
|
+
if (!dep) continue;
|
|
939
|
+
const to = safeNames.get(dep);
|
|
940
|
+
if (!to) continue;
|
|
941
|
+
const edge = `${from}->${to}`;
|
|
942
|
+
if (!edges.has(edge)) {
|
|
943
|
+
edges.add(edge);
|
|
944
|
+
const label = seeds.includes(comp) ? '|emit|' : '|consume|';
|
|
945
|
+
lines.push(` ${from} -->${label} ${to}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
lines.push(' classDef eventNode fill:#db2777,color:#fff');
|
|
951
|
+
return lines.join('\n');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function generateAuth(data) {
|
|
955
|
+
if (!data || !Array.isArray(data.components)) {
|
|
956
|
+
return 'flowchart TD\n Note["No data available"]';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const lines = ['flowchart TD'];
|
|
960
|
+
const seeds = componentsByRole(data.components, 'auth');
|
|
961
|
+
if (seeds.length === 0) {
|
|
962
|
+
lines.push(' Note["No authentication components found"]');
|
|
963
|
+
return lines.join('\n');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const connected = collectConnectedComponents(data.components, seeds, 2, 24);
|
|
967
|
+
const byName = byNameIndex(connected);
|
|
968
|
+
const safeNames = mapSafeNames(connected);
|
|
969
|
+
const edges = new Set();
|
|
970
|
+
|
|
971
|
+
lines.push(' Request["Authentication request"]');
|
|
972
|
+
lines.push(' Boundary{"Auth Boundary"}');
|
|
973
|
+
lines.push(' Request --> Boundary');
|
|
974
|
+
|
|
975
|
+
for (const seed of seeds) {
|
|
976
|
+
const safe = safeNames.get(seed);
|
|
977
|
+
if (!safe) continue;
|
|
978
|
+
lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
|
|
979
|
+
const key = `Boundary->${safe}`;
|
|
980
|
+
if (!edges.has(key)) {
|
|
981
|
+
edges.add(key);
|
|
982
|
+
lines.push(` Boundary --> ${safe}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
for (const comp of connected) {
|
|
987
|
+
const from = safeNames.get(comp);
|
|
988
|
+
if (!from) continue;
|
|
989
|
+
for (const depName of comp.dependencies || []) {
|
|
990
|
+
const dep = byName.get(depName);
|
|
991
|
+
if (!dep) continue;
|
|
992
|
+
const to = safeNames.get(dep);
|
|
993
|
+
if (!to) continue;
|
|
994
|
+
const key = `${from}->${to}`;
|
|
995
|
+
if (!edges.has(key)) {
|
|
996
|
+
edges.add(key);
|
|
997
|
+
lines.push(` ${from} --> ${to}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const providerSet = new Set();
|
|
1003
|
+
for (const seed of seeds) {
|
|
1004
|
+
for (const pkg of collectExternalImports(seed.imports || [])) {
|
|
1005
|
+
providerSet.add(pkg);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
for (const provider of providerSet) {
|
|
1009
|
+
const providerNode = sanitize(provider);
|
|
1010
|
+
lines.push(` ${providerNode}[("${escapeMermaid(provider)}")]`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
lines.push(' classDef authNode fill:#7c3aed,color:#fff');
|
|
1014
|
+
return lines.join('\n');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function generateSecurity(data) {
|
|
1018
|
+
if (!data || !Array.isArray(data.components)) {
|
|
1019
|
+
return 'flowchart TD\n Note["No data available"]';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const lines = ['flowchart TD'];
|
|
1023
|
+
const seeds = [
|
|
1024
|
+
...componentsByRole(data.components, 'security'),
|
|
1025
|
+
...componentsByRole(data.components, 'auth'),
|
|
1026
|
+
...componentsByRole(data.components, 'integrations'),
|
|
1027
|
+
].filter((value, index, arr) => arr.indexOf(value) === index);
|
|
1028
|
+
|
|
1029
|
+
if (seeds.length === 0) {
|
|
1030
|
+
lines.push(' Note["No security-focused components found"]');
|
|
1031
|
+
return lines.join('\n');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const connected = collectConnectedComponents(data.components, seeds, 2, 40);
|
|
1035
|
+
const byName = byNameIndex(connected);
|
|
1036
|
+
const safeNames = mapSafeNames(connected);
|
|
1037
|
+
const edges = new Set();
|
|
1038
|
+
|
|
1039
|
+
lines.push(' Untrusted["Untrusted input"]');
|
|
1040
|
+
for (const seed of seeds) {
|
|
1041
|
+
const safe = safeNames.get(seed);
|
|
1042
|
+
if (!safe) continue;
|
|
1043
|
+
lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
|
|
1044
|
+
const key = `Untrusted->${safe}`;
|
|
1045
|
+
if (!edges.has(key)) {
|
|
1046
|
+
edges.add(key);
|
|
1047
|
+
lines.push(` Untrusted --> ${safe}`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
for (const comp of connected) {
|
|
1052
|
+
const from = safeNames.get(comp);
|
|
1053
|
+
if (!from) continue;
|
|
1054
|
+
for (const depName of comp.dependencies || []) {
|
|
1055
|
+
const dep = byName.get(depName);
|
|
1056
|
+
if (!dep) continue;
|
|
1057
|
+
const to = safeNames.get(dep);
|
|
1058
|
+
if (!to) continue;
|
|
1059
|
+
const key = `${from}->${to}`;
|
|
1060
|
+
if (!edges.has(key)) {
|
|
1061
|
+
edges.add(key);
|
|
1062
|
+
lines.push(` ${from} --> ${to}`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
lines.push(' classDef securityNode fill:#dc2626,color:#fff');
|
|
1068
|
+
return lines.join('\n');
|
|
1069
|
+
}
|
|
1070
|
+
|
|
566
1071
|
function generate(data, type, focus) {
|
|
567
1072
|
switch (type) {
|
|
568
1073
|
case 'architecture': return generateArchitecture(data, focus);
|
|
@@ -570,12 +1075,79 @@ function generate(data, type, focus) {
|
|
|
570
1075
|
case 'dependency': return generateDependency(data, focus);
|
|
571
1076
|
case 'class': return generateClass(data);
|
|
572
1077
|
case 'flow': return generateFlow(data);
|
|
1078
|
+
case 'database': return generateDatabase(data);
|
|
1079
|
+
case 'user': return generateUserInteractions(data);
|
|
1080
|
+
case 'events': return generateEvents(data);
|
|
1081
|
+
case 'auth': return generateAuth(data);
|
|
1082
|
+
case 'security': return generateSecurity(data);
|
|
573
1083
|
default:
|
|
574
1084
|
console.warn(chalk.yellow(`⚠️ Unknown diagram type "${type}", using architecture`));
|
|
575
1085
|
return generateArchitecture(data, focus);
|
|
576
1086
|
}
|
|
577
1087
|
}
|
|
578
1088
|
|
|
1089
|
+
function isPlaceholderDiagram(mermaidCode) {
|
|
1090
|
+
if (!mermaidCode || typeof mermaidCode !== 'string') return true;
|
|
1091
|
+
const compact = mermaidCode.toLowerCase();
|
|
1092
|
+
return compact.includes('note["no data available"]')
|
|
1093
|
+
|| compact.includes('note["no components found')
|
|
1094
|
+
|| compact.includes('no services detected')
|
|
1095
|
+
|| compact.includes('note "no data available"')
|
|
1096
|
+
|| compact.includes('note "no classes found"')
|
|
1097
|
+
|| compact.includes('note["no database-focused components found"]')
|
|
1098
|
+
|| compact.includes('note["no user-facing components found"]')
|
|
1099
|
+
|| compact.includes('note["no event/channels components found"]')
|
|
1100
|
+
|| compact.includes('note["no authentication components found"]')
|
|
1101
|
+
|| compact.includes('note["no security-focused components found"]')
|
|
1102
|
+
|| compact.includes('no architecture data');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function toManifestEntry(type, filePath, mermaidCode, rootPath) {
|
|
1106
|
+
const lines = typeof mermaidCode === 'string' ? mermaidCode.split('\n') : [];
|
|
1107
|
+
return {
|
|
1108
|
+
type,
|
|
1109
|
+
file: path.basename(filePath),
|
|
1110
|
+
outputPath: rootPath ? path.relative(rootPath, filePath) : filePath,
|
|
1111
|
+
lines: lines.length,
|
|
1112
|
+
bytes: Buffer.byteLength(mermaidCode || '', 'utf8'),
|
|
1113
|
+
isPlaceholder: isPlaceholderDiagram(mermaidCode),
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function parseCommaSeparatedList(value) {
|
|
1118
|
+
if (!value || typeof value !== 'string') return [];
|
|
1119
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function buildManifestSummary(manifest) {
|
|
1123
|
+
if (!manifest || !Array.isArray(manifest.diagrams)) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const diagrams = manifest.diagrams
|
|
1128
|
+
.map((diagram) => ({
|
|
1129
|
+
...diagram,
|
|
1130
|
+
isPlaceholder: Boolean(diagram.isPlaceholder),
|
|
1131
|
+
}))
|
|
1132
|
+
.filter((entry) => entry && typeof entry.type === 'string' && entry.file);
|
|
1133
|
+
|
|
1134
|
+
const missing = SUPPORTED_DIAGRAM_TYPES.filter(
|
|
1135
|
+
(type) => !diagrams.some((diagram) => diagram.type === type)
|
|
1136
|
+
);
|
|
1137
|
+
const placeholderTypes = diagrams.filter((diagram) => diagram.isPlaceholder).map((diagram) => diagram.type);
|
|
1138
|
+
|
|
1139
|
+
return {
|
|
1140
|
+
generatedAt: manifest.generatedAt || new Date().toISOString(),
|
|
1141
|
+
rootPath: manifest.rootPath,
|
|
1142
|
+
diagramDir: manifest.diagramDir,
|
|
1143
|
+
totalDiagrams: diagrams.length,
|
|
1144
|
+
placeholders: placeholderTypes.length,
|
|
1145
|
+
placeholderTypes,
|
|
1146
|
+
missingTypes: missing,
|
|
1147
|
+
diagrams,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
579
1151
|
// URL shortening for large diagrams
|
|
580
1152
|
function createMermaidUrl(mermaidCode) {
|
|
581
1153
|
// If diagram is very large, provide text file instead
|
|
@@ -760,7 +1332,7 @@ program
|
|
|
760
1332
|
program
|
|
761
1333
|
.command('generate [path]')
|
|
762
1334
|
.description('Generate a diagram')
|
|
763
|
-
.option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow', 'architecture')
|
|
1335
|
+
.option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow, database, user, events, auth, security', 'architecture')
|
|
764
1336
|
.option('-f, --focus <module>', 'Focus on specific module')
|
|
765
1337
|
.option('-o, --output <file>', 'Output file (SVG/PNG)')
|
|
766
1338
|
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
@@ -868,18 +1440,123 @@ program
|
|
|
868
1440
|
|
|
869
1441
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
870
1442
|
|
|
871
|
-
const types = [
|
|
1443
|
+
const types = [...SUPPORTED_DIAGRAM_TYPES];
|
|
1444
|
+
const manifest = {
|
|
1445
|
+
generatedAt: new Date().toISOString(),
|
|
1446
|
+
rootPath: root,
|
|
1447
|
+
diagramDir: path.relative(root, outDir) || '.',
|
|
1448
|
+
diagrams: [],
|
|
1449
|
+
};
|
|
872
1450
|
|
|
873
1451
|
for (const type of types) {
|
|
874
1452
|
const mermaid = generate(data, type);
|
|
875
1453
|
const file = path.join(outDir, `${type}.mmd`);
|
|
876
1454
|
fs.writeFileSync(file, mermaid);
|
|
1455
|
+
manifest.diagrams.push(toManifestEntry(type, file, mermaid, root));
|
|
877
1456
|
console.log(chalk.green('✅'), type, '→', file);
|
|
878
1457
|
}
|
|
1458
|
+
|
|
1459
|
+
const manifestPath = path.join(outDir, 'manifest.json');
|
|
1460
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1461
|
+
console.log(chalk.green('✅ manifest'), '→', manifestPath);
|
|
879
1462
|
|
|
880
1463
|
console.log(chalk.cyan('\n🔗 Preview all at: https://mermaid.live'));
|
|
881
1464
|
});
|
|
882
1465
|
|
|
1466
|
+
program
|
|
1467
|
+
.command('manifest [path]')
|
|
1468
|
+
.description('Summarize manifest.json from a diagram output directory')
|
|
1469
|
+
.option('-d, --manifest-dir <dir>', 'Directory containing manifest.json', '.diagram')
|
|
1470
|
+
.option('-o, --output <file>', 'Write summary JSON to a file')
|
|
1471
|
+
.option('--require-types <list>', 'Require all listed diagram types, comma-separated')
|
|
1472
|
+
.option('--fail-on-placeholder', 'Fail if any diagram was a placeholder')
|
|
1473
|
+
.action(async (targetPath, options) => {
|
|
1474
|
+
const root = resolveRootPathOrExit(targetPath);
|
|
1475
|
+
const manifestDir = path.join(root, options.manifestDir || '.diagram');
|
|
1476
|
+
const manifestPath = path.join(manifestDir, 'manifest.json');
|
|
1477
|
+
|
|
1478
|
+
let safeManifestPath;
|
|
1479
|
+
try {
|
|
1480
|
+
safeManifestPath = validateExistingPathInRoot(manifestPath, root, 'manifest path');
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
console.error(chalk.red('❌ Manifest error:'), err.message);
|
|
1483
|
+
process.exit(2);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
let manifestRaw;
|
|
1487
|
+
try {
|
|
1488
|
+
manifestRaw = fs.readFileSync(safeManifestPath, 'utf8');
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
console.error(chalk.red('❌ Manifest read failed:'), err.message);
|
|
1491
|
+
process.exit(2);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
let parsedManifest;
|
|
1495
|
+
try {
|
|
1496
|
+
parsedManifest = JSON.parse(manifestRaw);
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
console.error(chalk.red('❌ Manifest parse failed:'), err.message);
|
|
1499
|
+
process.exit(2);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const summary = buildManifestSummary(parsedManifest);
|
|
1503
|
+
if (!summary) {
|
|
1504
|
+
console.error(chalk.red('❌ Invalid manifest format'));
|
|
1505
|
+
process.exit(2);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const required = parseCommaSeparatedList(options.requireTypes);
|
|
1509
|
+
const missingRequired = required.filter((type) => !summary.diagrams.some((d) => d.type === type));
|
|
1510
|
+
summary.required = {
|
|
1511
|
+
requested: required,
|
|
1512
|
+
missing: missingRequired,
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
if (required.length > 0 && missingRequired.length > 0) {
|
|
1516
|
+
console.error(chalk.red(`❌ Manifest missing required diagram types: ${missingRequired.join(', ')}`));
|
|
1517
|
+
process.exit(2);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (options.failOnPlaceholder && summary.placeholders > 0) {
|
|
1521
|
+
console.error(chalk.yellow(`⚠️ Manifest includes ${summary.placeholders} placeholder diagram(s)`));
|
|
1522
|
+
process.exit(2);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (options.output) {
|
|
1526
|
+
let safeOutput;
|
|
1527
|
+
try {
|
|
1528
|
+
safeOutput = validateOutputPath(options.output, root);
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1531
|
+
process.exit(2);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const outputDir = path.dirname(safeOutput);
|
|
1535
|
+
if (!fs.existsSync(outputDir)) {
|
|
1536
|
+
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
fs.writeFileSync(safeOutput, `${JSON.stringify(summary, null, 2)}\n`);
|
|
1540
|
+
console.log(chalk.green('✅ manifest summary'), '→', safeOutput);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
console.log(chalk.blue('\n📘 Manifest summary for'), safeManifestPath);
|
|
1545
|
+
console.log(` Total: ${summary.totalDiagrams}`);
|
|
1546
|
+
console.log(` Placeholder: ${summary.placeholders}`);
|
|
1547
|
+
if (summary.missingTypes.length > 0) {
|
|
1548
|
+
console.log(chalk.yellow(` Missing expected (all supported): ${summary.missingTypes.join(', ')}`));
|
|
1549
|
+
}
|
|
1550
|
+
if (summary.placeholderTypes.length > 0) {
|
|
1551
|
+
console.log(chalk.yellow(` Placeholder types: ${summary.placeholderTypes.join(', ')}`));
|
|
1552
|
+
}
|
|
1553
|
+
console.log('');
|
|
1554
|
+
for (const entry of summary.diagrams) {
|
|
1555
|
+
const status = entry.isPlaceholder ? chalk.yellow('placeholder') : chalk.green('ok');
|
|
1556
|
+
console.log(` ${status} ${entry.type} -> ${entry.file}`);
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
|
|
883
1560
|
program
|
|
884
1561
|
.command('video [path]')
|
|
885
1562
|
.description('Generate an animated video of the diagram')
|