@brainwav/diagram 1.0.8 → 1.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/.diagram/contracts/machine-command-coverage.json +73 -0
- package/.diagram/migration/finalization-policy.json +20 -0
- package/LICENSE +202 -21
- package/README.md +132 -339
- package/package.json +46 -13
- package/scripts/refresh-diagram-context.sh +274 -182
- package/src/analyzers/default-analyzer.js +11 -0
- package/src/analyzers/index.js +34 -0
- package/src/artifacts/agent-context.js +105 -0
- package/src/artifacts/artifact-budget.js +224 -0
- package/src/artifacts/brief.js +153 -0
- package/src/artifacts/evidence-manifest.js +206 -0
- package/src/artifacts/evidence-summary.js +29 -0
- package/src/commands/analyze.js +125 -0
- package/src/commands/changed.js +185 -0
- package/src/commands/context.js +110 -0
- package/src/commands/diff.js +142 -0
- package/src/commands/doctor.js +335 -0
- package/src/commands/explain.js +273 -0
- package/src/commands/generate-all.js +170 -0
- package/src/commands/generate-animated.js +50 -0
- package/src/commands/generate-video.js +65 -0
- package/src/commands/generate.js +522 -0
- package/src/commands/init.js +123 -0
- package/src/commands/output.js +76 -0
- package/src/commands/scan.js +624 -0
- package/src/commands/shared.js +396 -0
- package/src/commands/validate.js +328 -0
- package/src/commands/video-shared.js +105 -0
- package/src/commands/workflow-pr.js +26 -0
- package/src/confidence/pipeline.js +186 -0
- package/src/config/diagramrc.js +79 -0
- package/src/context/build-context-pack.js +291 -0
- package/src/context/normalize-diagram-manifest.js +282 -0
- package/src/core/analysis-generation-analyze-components.js +102 -0
- package/src/core/analysis-generation-analyze-dependencies.js +33 -0
- package/src/core/analysis-generation-analyze-files.js +48 -0
- package/src/core/analysis-generation-analyze-options.js +73 -0
- package/src/core/analysis-generation-analyze.js +63 -0
- package/src/core/analysis-generation-constants.js +53 -0
- package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
- package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
- package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
- package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
- package/src/core/analysis-generation-diagrams-core.js +12 -0
- package/src/core/analysis-generation-diagrams-empty.js +68 -0
- package/src/core/analysis-generation-diagrams-erd.js +59 -0
- package/src/core/analysis-generation-diagrams-limit.js +27 -0
- package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
- package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
- package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
- package/src/core/analysis-generation-diagrams-role-data.js +182 -0
- package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
- package/src/core/analysis-generation-diagrams-role-security.js +129 -0
- package/src/core/analysis-generation-diagrams-role.js +25 -0
- package/src/core/analysis-generation-diagrams.js +182 -0
- package/src/core/analysis-generation-role-tags-constants.js +55 -0
- package/src/core/analysis-generation-role-tags-imports.js +32 -0
- package/src/core/analysis-generation-role-tags-infer.js +49 -0
- package/src/core/analysis-generation-role-tags-match.js +19 -0
- package/src/core/analysis-generation-role-tags.js +7 -0
- package/src/core/analysis-generation-utils-core.js +308 -0
- package/src/core/analysis-generation-utils-graph.js +321 -0
- package/src/core/analysis-generation-utils-resolution.js +76 -0
- package/src/core/analysis-generation-utils.js +9 -0
- package/src/core/analysis-generation.js +44 -0
- package/src/diagram.js +178 -1761
- package/src/formatters/console.js +198 -0
- package/src/formatters/index.js +41 -0
- package/src/formatters/json.js +113 -0
- package/src/formatters/junit.js +123 -0
- package/src/graph.js +159 -0
- package/src/incremental/cache.js +210 -0
- package/src/ir/architecture-ir.js +48 -0
- package/src/migration/evidence.js +262 -0
- package/src/migration/finalization-policy.js +35 -0
- package/src/renderers/report-html.js +265 -0
- package/src/rules/factory.js +108 -0
- package/src/rules/types/base.js +54 -0
- package/src/rules/types/import-rule.js +286 -0
- package/src/rules.js +380 -0
- package/src/schema/erd-confidence.js +56 -0
- package/src/schema/erd-extractor.js +504 -0
- package/src/schema/erd-model.js +176 -0
- package/src/schema/rules-schema.js +170 -0
- package/src/utils/suggestions.js +67 -0
- package/src/video.js +4 -5
- package/src/workflow/git-helpers.js +576 -0
- package/src/workflow/pr-command.js +694 -0
- package/src/workflow/pr-impact.js +848 -0
- package/src/workflow/sort-utils.js +16 -0
package/src/diagram.js
CHANGED
|
@@ -1,1809 +1,226 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { glob } = require('glob');
|
|
7
4
|
const chalk = require('chalk');
|
|
8
|
-
const
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const crypto = require('crypto');
|
|
11
|
-
const zlib = require('zlib');
|
|
12
|
-
const { getOpenCommand, getNpxCommandCandidates } = require('./utils/commands');
|
|
13
|
-
|
|
14
|
-
// Read version from package.json
|
|
5
|
+
const path = require('path');
|
|
15
6
|
const packageJson = require('../package.json');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
7
|
+
const { loadDiagramRc } = require('./config/diagramrc');
|
|
8
|
+
const { registerAnalyzeCommand } = require('./commands/analyze');
|
|
9
|
+
const { registerGenerateCommand } = require('./commands/generate');
|
|
10
|
+
const { registerGenerateAllCommand } = require('./commands/generate-all');
|
|
11
|
+
const { registerScanCommand } = require('./commands/scan');
|
|
12
|
+
const { registerValidateCommand } = require('./commands/validate');
|
|
13
|
+
const { registerDiffCommand } = require('./commands/diff');
|
|
14
|
+
const { registerGenerateVideoCommand } = require('./commands/generate-video');
|
|
15
|
+
const { registerGenerateAnimatedCommand } = require('./commands/generate-animated');
|
|
16
|
+
const { registerDoctorCommand } = require('./commands/doctor');
|
|
17
|
+
const { registerChangedCommand } = require('./commands/changed');
|
|
18
|
+
const { registerContextCommand } = require('./commands/context');
|
|
19
|
+
const { registerExplainCommand } = require('./commands/explain');
|
|
20
|
+
const { registerInitCommand } = require('./commands/init');
|
|
21
|
+
const { registerWorkflowPrCommand } = require('./commands/workflow-pr');
|
|
22
|
+
const {
|
|
23
|
+
escapeHtml,
|
|
24
|
+
groupChangePaths,
|
|
25
|
+
buildRiskNarrative,
|
|
26
|
+
buildSummaryMeta,
|
|
27
|
+
generateHtmlExplainer,
|
|
28
|
+
} = require('./workflow/pr-impact');
|
|
29
|
+
|
|
30
|
+
const CANONICAL_COMMAND_NAME = 'archscope';
|
|
31
|
+
const COMPATIBILITY_COMMAND_NAME = 'diagram';
|
|
32
|
+
const COMPATIBILITY_NOTICE =
|
|
33
|
+
`Compatibility notice: '${COMPATIBILITY_COMMAND_NAME}' remains supported during migration. Use '${CANONICAL_COMMAND_NAME}' for canonical usage.`;
|
|
30
34
|
|
|
31
35
|
const program = new Command();
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const map = {
|
|
38
|
-
'.ts': 'typescript', '.tsx': 'typescript',
|
|
39
|
-
'.mts': 'typescript', '.cts': 'typescript',
|
|
40
|
-
'.js': 'javascript', '.jsx': 'javascript',
|
|
41
|
-
'.mjs': 'javascript', '.cjs': 'javascript',
|
|
42
|
-
'.py': 'python', '.go': 'go', '.rs': 'rust',
|
|
43
|
-
'.java': 'java', '.rb': 'ruby', '.php': 'php',
|
|
44
|
-
};
|
|
45
|
-
return map[ext] || 'unknown';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function inferType(filePath, content) {
|
|
49
|
-
const base = path.basename(filePath).toLowerCase();
|
|
50
|
-
if (base.includes('service')) return 'service';
|
|
51
|
-
if (base.includes('component') || base.endsWith('.tsx') || base.endsWith('.jsx')) return 'component';
|
|
52
|
-
if (content.includes('class ') && content.includes('extends')) return 'class';
|
|
53
|
-
if (content.includes('export default function') || content.includes('export function')) return 'function';
|
|
54
|
-
if (content.includes('module.exports') || content.includes('export ')) return 'module';
|
|
55
|
-
return 'file';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function extractImports(content, lang) {
|
|
59
|
-
const imports = [];
|
|
60
|
-
if (lang === 'typescript' || lang === 'javascript') {
|
|
61
|
-
// ES6 imports with timeout protection against ReDoS
|
|
62
|
-
const es6Regex = /import\s+(?:(?:\{[^}]*?\}|\*\s+as\s+\w+|\w+)\s+from\s+)?["']([^"']+)["']/g;
|
|
63
|
-
const es6 = [...content.matchAll(es6Regex)];
|
|
64
|
-
es6.forEach(m => imports.push(m[1]));
|
|
65
|
-
// CommonJS requires
|
|
66
|
-
const cjs = [...content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)];
|
|
67
|
-
cjs.forEach(m => imports.push(m[1]));
|
|
68
|
-
// Dynamic imports
|
|
69
|
-
const dynamic = [...content.matchAll(/import\s*\(\s*["']([^"']+)["']\s*\)/g)];
|
|
70
|
-
dynamic.forEach(m => imports.push(m[1]));
|
|
71
|
-
} else if (lang === 'python') {
|
|
72
|
-
const py = [...content.matchAll(/(?:from|import)\s+([\w.]+)/g)];
|
|
73
|
-
py.forEach(m => imports.push(m[1]));
|
|
74
|
-
} else if (lang === 'go') {
|
|
75
|
-
const go = [...content.matchAll(/import\s+(?:\(\s*)?["']([^"']+)["']/g)];
|
|
76
|
-
go.forEach(m => imports.push(m[1]));
|
|
77
|
-
}
|
|
78
|
-
return imports;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Extract imports with line number information
|
|
83
|
-
* @param {string} content - File content
|
|
84
|
-
* @param {string} lang - Language
|
|
85
|
-
* @returns {Array<{path: string, line: number}>}
|
|
86
|
-
*/
|
|
87
|
-
function extractImportsWithPositions(content, lang) {
|
|
88
|
-
const imports = [];
|
|
89
|
-
const lines = content.split(/\r?\n/);
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i < lines.length; i++) {
|
|
92
|
-
const line = lines[i];
|
|
93
|
-
const lineNum = i + 1;
|
|
94
|
-
|
|
95
|
-
if (lang === 'typescript' || lang === 'javascript') {
|
|
96
|
-
// ES6 imports
|
|
97
|
-
const es6 = line.match(/import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?["']([^"']+)["']/);
|
|
98
|
-
if (es6) {
|
|
99
|
-
imports.push({ path: es6[1], line: lineNum });
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// CommonJS requires
|
|
104
|
-
const cjs = line.match(/require\s*\(\s*["']([^"']+)["']\s*\)/);
|
|
105
|
-
if (cjs) {
|
|
106
|
-
imports.push({ path: cjs[1], line: lineNum });
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Dynamic imports
|
|
111
|
-
const dynamic = line.match(/import\s*\(\s*["']([^"']+)["']\s*\)/);
|
|
112
|
-
if (dynamic) {
|
|
113
|
-
imports.push({ path: dynamic[1], line: lineNum });
|
|
114
|
-
}
|
|
115
|
-
} else if (lang === 'python') {
|
|
116
|
-
const py = line.match(/(?:from|import)\s+([\w.]+)/);
|
|
117
|
-
if (py) {
|
|
118
|
-
imports.push({ path: py[1], line: lineNum });
|
|
119
|
-
}
|
|
120
|
-
} else if (lang === 'go') {
|
|
121
|
-
const go = line.match(/import\s+(?:\(\s*)?["']([^"']+)["']/);
|
|
122
|
-
if (go) {
|
|
123
|
-
imports.push({ path: go[1], line: lineNum });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return imports;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function sanitize(name) {
|
|
132
|
-
// Ensure unique, valid mermaid ID
|
|
133
|
-
const base = name.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[0-9]/, '_$&');
|
|
134
|
-
// Add hash suffix to prevent collisions (using SHA-256)
|
|
135
|
-
const hash = crypto.createHash('sha256').update(name).digest('hex').slice(0, 8);
|
|
136
|
-
return `${base}_${hash}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function escapeMermaid(str) {
|
|
140
|
-
if (!str) return '';
|
|
141
|
-
return str
|
|
142
|
-
.replace(/"/g, '\\"')
|
|
143
|
-
.replace(/\[/g, '\\[')
|
|
144
|
-
.replace(/\]/g, '\\]')
|
|
145
|
-
.replace(/\(/g, '\\(')
|
|
146
|
-
.replace(/\)/g, '\\)')
|
|
147
|
-
.replace(/#/g, '\\#')
|
|
148
|
-
.replace(/</g, '\\<')
|
|
149
|
-
.replace(/>/g, '\\>')
|
|
150
|
-
.replace(/\{/g, '\\{')
|
|
151
|
-
.replace(/\}/g, '\\}')
|
|
152
|
-
.replace(/\|/g, '\\|');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function normalizePath(inputPath) {
|
|
156
|
-
// Always use forward slashes for consistency
|
|
157
|
-
return inputPath.replace(/\\/g, '/');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const IMPORT_RESOLUTION_SUFFIXES = [
|
|
161
|
-
'',
|
|
162
|
-
'.ts',
|
|
163
|
-
'.tsx',
|
|
164
|
-
'.js',
|
|
165
|
-
'.jsx',
|
|
166
|
-
'.mjs',
|
|
167
|
-
'.mts',
|
|
168
|
-
'.cts',
|
|
169
|
-
'/index.ts',
|
|
170
|
-
'/index.tsx',
|
|
171
|
-
'/index.js',
|
|
172
|
-
'/index.jsx',
|
|
173
|
-
'/index.mjs',
|
|
174
|
-
'/index.mts',
|
|
175
|
-
'/index.cts'
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
function toComparablePath(p) {
|
|
179
|
-
return normalizePath(String(p || '')).replace(/^\.\//, '');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function getImportPath(importInfo) {
|
|
183
|
-
if (typeof importInfo === 'string') return importInfo;
|
|
184
|
-
if (importInfo && typeof importInfo.path === 'string') return importInfo.path;
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function resolveInternalImport(fromFilePath, importPath, rootPath) {
|
|
189
|
-
if (typeof fromFilePath !== 'string' || typeof importPath !== 'string') {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
if (!importPath.startsWith('.')) {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const fromDir = path.dirname(fromFilePath);
|
|
37
|
+
program
|
|
38
|
+
.name(CANONICAL_COMMAND_NAME)
|
|
39
|
+
.description('Generate architecture evidence for humans and AI agents')
|
|
40
|
+
.version(packageJson.version);
|
|
197
41
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
42
|
+
registerAnalyzeCommand(program);
|
|
43
|
+
registerGenerateCommand(program);
|
|
44
|
+
registerGenerateAllCommand(program);
|
|
45
|
+
registerScanCommand(program);
|
|
46
|
+
registerValidateCommand(program);
|
|
47
|
+
registerDiffCommand(program);
|
|
48
|
+
registerGenerateVideoCommand(program);
|
|
49
|
+
registerGenerateAnimatedCommand(program);
|
|
50
|
+
registerDoctorCommand(program);
|
|
51
|
+
registerChangedCommand(program);
|
|
52
|
+
registerContextCommand(program);
|
|
53
|
+
registerExplainCommand(program);
|
|
54
|
+
registerInitCommand(program);
|
|
55
|
+
registerWorkflowPrCommand(program);
|
|
56
|
+
|
|
57
|
+
program.on('command:*', function (operands) {
|
|
58
|
+
console.error(chalk.red(`\n🤖 AI Agent Error: Unknown command '${operands[0]}'\n`));
|
|
59
|
+
console.error(chalk.white('Use the canonical command set:\n'));
|
|
60
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} init [path]`) + chalk.gray(' - Scaffold .architecture.yml, .diagramrc, and CI sample step'));
|
|
61
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} doctor [path]`) + chalk.gray(' - Check local tooling and environment health'));
|
|
62
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} analyze [path]`) + chalk.gray(' - Analyze codebase structure'));
|
|
63
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} scan [path]`) + chalk.gray(' - Initialize architecture evidence pack manifest'));
|
|
64
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} generate [path]`) + chalk.gray(' - Generate one diagram type'));
|
|
65
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} generate-all [path]`) + chalk.gray(' - Generate all diagram types'));
|
|
66
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} changed [path]`) + chalk.gray(' - Analyze only git-changed files'));
|
|
67
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} context [path]`) + chalk.gray(' - Refresh AI context pack artifacts'));
|
|
68
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} explain <component> [path]`) + chalk.gray(' - Explain a local dependency neighborhood'));
|
|
69
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} validate [path]`) + chalk.gray(' - Validate architecture against .architecture.yml'));
|
|
70
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} workflow pr [path]`) + chalk.gray(' - Compute PR blast-radius and risk score'));
|
|
71
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} diff <base> <head>`) + chalk.gray(' - Compare architecture snapshots'));
|
|
72
|
+
console.error(chalk.gray('\nOptional advanced media commands:'));
|
|
73
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} generate-video [path]`) + chalk.gray(' - Generate animated video output'));
|
|
74
|
+
console.error(chalk.cyan(` ${CANONICAL_COMMAND_NAME} generate-animated [path]`) + chalk.gray(' - Generate animated SVG output\n'));
|
|
75
|
+
console.error(chalk.white(`Use ${chalk.cyan(`${CANONICAL_COMMAND_NAME} --help`)} for full option details.`));
|
|
76
|
+
console.error(chalk.white(`Use ${chalk.cyan('--format json')} instead of ${chalk.cyan('--json')} for machine output.`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function getInvocationName(argv, env = process.env) {
|
|
81
|
+
const candidates = [
|
|
82
|
+
argv[1],
|
|
83
|
+
env._,
|
|
84
|
+
env.npm_lifecycle_script,
|
|
85
|
+
argv[0],
|
|
86
|
+
];
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
const name = path.basename(String(candidate || '').trim());
|
|
89
|
+
if (name && name !== 'node' && name !== 'diagram.js') {
|
|
90
|
+
return name;
|
|
204
91
|
}
|
|
205
|
-
return relativeToRoot;
|
|
206
92
|
}
|
|
207
|
-
|
|
208
|
-
// Fallback for precomputed data without root path
|
|
209
|
-
const posixFromDir = normalizePath(fromDir);
|
|
210
|
-
const posixImport = normalizePath(importPath);
|
|
211
|
-
return toComparablePath(path.posix.normalize(path.posix.join(posixFromDir, posixImport)));
|
|
93
|
+
return path.basename(argv[1] || '');
|
|
212
94
|
}
|
|
213
95
|
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
const candidates = new Set(
|
|
217
|
-
IMPORT_RESOLUTION_SUFFIXES.map(suffix => toComparablePath(comparablePath + suffix))
|
|
218
|
-
);
|
|
219
|
-
return components.find(c => candidates.has(toComparablePath(c.filePath)));
|
|
96
|
+
function isCompatibilityInvocation(argv, env = process.env) {
|
|
97
|
+
return getInvocationName(argv, env) === COMPATIBILITY_COMMAND_NAME;
|
|
220
98
|
}
|
|
221
99
|
|
|
222
|
-
function
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
if (importPath.startsWith('@')) {
|
|
226
|
-
const [scope, pkg] = importPath.split('/');
|
|
227
|
-
return scope && pkg ? `${scope}/${pkg}` : scope || null;
|
|
100
|
+
function emitCompatibilityInvocationNotice(argv, env = process.env) {
|
|
101
|
+
if (isCompatibilityInvocation(argv, env)) {
|
|
102
|
+
console.error(chalk.yellow(COMPATIBILITY_NOTICE));
|
|
228
103
|
}
|
|
229
|
-
return importPath.split('/')[0] || null;
|
|
230
104
|
}
|
|
231
105
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
],
|
|
253
|
-
integrations: [
|
|
254
|
-
'integration', 'webhook', 'gateway', 'stripe', 'pay', 'sendgrid', 'twilio',
|
|
255
|
-
'sentry', 'github', 'slack', 'analytics', 'mail', 'smtp', 'storage'
|
|
256
|
-
],
|
|
257
|
-
security: [
|
|
258
|
-
'security', 'threat', 'attack', 'rate', 'encrypt', 'decrypt', 'signature',
|
|
259
|
-
'hash', 'verify', 'csrf', 'xss', 'audit', 'compliance', 'policy', 'vault',
|
|
260
|
-
'kms', 'secret', 'key'
|
|
261
|
-
],
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const SUPPORTED_DIAGRAM_TYPES = Object.freeze([
|
|
265
|
-
'architecture',
|
|
266
|
-
'sequence',
|
|
267
|
-
'dependency',
|
|
268
|
-
'class',
|
|
269
|
-
'flow',
|
|
270
|
-
'database',
|
|
271
|
-
'user',
|
|
272
|
-
'events',
|
|
273
|
-
'auth',
|
|
274
|
-
'security',
|
|
275
|
-
]);
|
|
276
|
-
|
|
277
|
-
function textHasToken(text, token) {
|
|
278
|
-
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
279
|
-
const re = new RegExp(`(^|[\\/._-])${escaped}([\\/._-]|$)`, 'i');
|
|
280
|
-
return re.test(text);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function collectExternalImports(importEntries) {
|
|
284
|
-
const packages = new Set();
|
|
285
|
-
if (!Array.isArray(importEntries)) return [];
|
|
286
|
-
|
|
287
|
-
for (const entry of importEntries) {
|
|
288
|
-
const importPath = getImportPath(entry);
|
|
289
|
-
if (!importPath || importPath.startsWith('.')) {
|
|
290
|
-
continue;
|
|
106
|
+
/**
|
|
107
|
+
* Determine which top-level subcommand name is active from a CLI argument list.
|
|
108
|
+
*
|
|
109
|
+
* Scans the provided `argv` (typically `process.argv`) and returns the first token
|
|
110
|
+
* that represents a top-level command — i.e. the first non-flag token that is not
|
|
111
|
+
* the value for an option that expects a value. Returns `null` if no such token is found.
|
|
112
|
+
*
|
|
113
|
+
* @param {string[]} argv - The complete argument vector (e.g. `process.argv`).
|
|
114
|
+
* @returns {string|null} The active top-level subcommand name, or `null` if none is present.
|
|
115
|
+
*/
|
|
116
|
+
function findActiveCommand(argv) {
|
|
117
|
+
const flagsWithValue = new Set();
|
|
118
|
+
const stack = [program];
|
|
119
|
+
while (stack.length > 0) {
|
|
120
|
+
const command = stack.pop();
|
|
121
|
+
for (const option of command.options || []) {
|
|
122
|
+
const expectsValue = Boolean(option.required || option.optional || option.variadic);
|
|
123
|
+
if (!expectsValue) continue;
|
|
124
|
+
if (option.short) flagsWithValue.add(option.short);
|
|
125
|
+
if (option.long) flagsWithValue.add(option.long);
|
|
291
126
|
}
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
packages.add(externalPackage);
|
|
127
|
+
for (const subcommand of command.commands || []) {
|
|
128
|
+
stack.push(subcommand);
|
|
295
129
|
}
|
|
296
130
|
}
|
|
297
131
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const nameText = (originalName || '').toLowerCase();
|
|
305
|
-
const externalImports = collectExternalImports(importEntries).join(' ').toLowerCase();
|
|
306
|
-
const combined = `${pathText} ${nameText} ${content} ${externalImports}`;
|
|
307
|
-
|
|
308
|
-
const tags = new Set();
|
|
309
|
-
|
|
310
|
-
for (const [tag, tokens] of Object.entries(ROLE_PATTERNS)) {
|
|
311
|
-
for (const token of tokens) {
|
|
312
|
-
if (textHasToken(combined, token)) {
|
|
313
|
-
tags.add(tag);
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
132
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
133
|
+
const current = argv[i];
|
|
134
|
+
if (current.startsWith('-')) continue;
|
|
135
|
+
const prev = argv[i - 1];
|
|
136
|
+
if (!flagsWithValue.has(prev)) {
|
|
137
|
+
return current;
|
|
316
138
|
}
|
|
317
139
|
}
|
|
318
|
-
|
|
319
|
-
if (type === 'service') {
|
|
320
|
-
tags.add('service');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (tags.size === 0) {
|
|
324
|
-
tags.add('general');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return [...tags];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function hasRole(component, role) {
|
|
331
|
-
return (Array.isArray(component.roleTags) && component.roleTags.includes(role));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function componentsByRole(components, role) {
|
|
335
|
-
if (!Array.isArray(components)) return [];
|
|
336
|
-
return components.filter((component) => hasRole(component, role));
|
|
140
|
+
return null;
|
|
337
141
|
}
|
|
338
142
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Rewrite deprecated flags and command aliases in a CLI argument vector to their current canonical forms.
|
|
145
|
+
*
|
|
146
|
+
* Logs short deprecation notes to stderr for any rewritten tokens and returns a new argv array with replacements applied.
|
|
147
|
+
*
|
|
148
|
+
* @param {string[]} argv - The original process-style argument array (e.g. process.argv).
|
|
149
|
+
* @returns {string[]} The rewritten argument array with deprecated flags and command aliases replaced.
|
|
150
|
+
*/
|
|
151
|
+
function resolveAliasArgs(argv) {
|
|
152
|
+
const resolvedArgs = [];
|
|
153
|
+
let commandFound = false;
|
|
154
|
+
const activeCommand = findActiveCommand(argv);
|
|
347
155
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const used = new Set();
|
|
156
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
157
|
+
const arg = argv[i];
|
|
351
158
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
map.set(component, rawName);
|
|
356
|
-
used.add(rawName);
|
|
159
|
+
if (arg === '--json' || arg === '-j') {
|
|
160
|
+
console.error(chalk.yellow(`🤖 Note for AI Agent: '${arg}' is deprecated. Using '--format json' automatically.`));
|
|
161
|
+
resolvedArgs.push('--format', 'json');
|
|
357
162
|
continue;
|
|
358
163
|
}
|
|
359
164
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
candidate = `${rawName}_${i}`;
|
|
365
|
-
}
|
|
366
|
-
map.set(component, candidate);
|
|
367
|
-
used.add(candidate);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return map;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function byNameIndex(components) {
|
|
374
|
-
const map = new Map();
|
|
375
|
-
if (!Array.isArray(components)) return map;
|
|
376
|
-
for (const component of components) {
|
|
377
|
-
if (component && component.name) {
|
|
378
|
-
map.set(component.name, component);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return map;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function resolveDependencyComponent(component, componentsByName, name) {
|
|
385
|
-
if (!component || !name || !componentsByName) return null;
|
|
386
|
-
return componentsByName.get(name) || null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function collectConnectedComponents(components, seedComponents, maxDepth = 2, maxNodes = 35) {
|
|
390
|
-
if (!Array.isArray(components)) return [];
|
|
391
|
-
if (!Array.isArray(seedComponents) || seedComponents.length === 0) return [];
|
|
392
|
-
|
|
393
|
-
const byName = byNameIndex(components);
|
|
394
|
-
const selected = new Map();
|
|
395
|
-
const queue = [];
|
|
396
|
-
|
|
397
|
-
for (const seed of seedComponents) {
|
|
398
|
-
if (seed && seed.name && !selected.has(seed.name)) {
|
|
399
|
-
selected.set(seed.name, seed);
|
|
400
|
-
queue.push(seed);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
let depth = 0;
|
|
405
|
-
const visited = new Set();
|
|
406
|
-
while (queue.length > 0 && depth < maxDepth) {
|
|
407
|
-
const levelSize = queue.length;
|
|
408
|
-
for (let i = 0; i < levelSize; i++) {
|
|
409
|
-
const current = queue.shift();
|
|
410
|
-
if (!current || typeof current.name !== 'string') continue;
|
|
411
|
-
const depthKey = `${current.name}:${depth}`;
|
|
412
|
-
if (visited.has(depthKey)) continue;
|
|
413
|
-
visited.add(depthKey);
|
|
414
|
-
|
|
415
|
-
const next = [];
|
|
416
|
-
for (const depName of current.dependencies || []) {
|
|
417
|
-
const dependency = byName.get(depName);
|
|
418
|
-
if (dependency && !selected.has(depName)) {
|
|
419
|
-
selected.set(depName, dependency);
|
|
420
|
-
next.push(dependency);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
for (const candidate of components) {
|
|
425
|
-
if (selected.has(candidate.name)) continue;
|
|
426
|
-
const reverseDependencies = Array.isArray(candidate.dependencies) ? candidate.dependencies : [];
|
|
427
|
-
if (reverseDependencies.includes(current.name)) {
|
|
428
|
-
selected.set(candidate.name, candidate);
|
|
429
|
-
next.push(candidate);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
for (const n of next) {
|
|
434
|
-
if (selected.size >= maxNodes) break;
|
|
435
|
-
queue.push(n);
|
|
436
|
-
}
|
|
437
|
-
if (selected.size >= maxNodes) break;
|
|
438
|
-
}
|
|
439
|
-
depth += 1;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return [...selected.values()];
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function inferDbIntent(component) {
|
|
446
|
-
const source = `${component.filePath || ''} ${component.originalName || ''} ${component.name || ''}`.toLowerCase();
|
|
447
|
-
const hasLookup = /(read|find|query|select|get|lookup|exists|fetch)/.test(source);
|
|
448
|
-
const hasWrite = /(create|insert|update|upsert|save|delete|remove|write|transaction)/.test(source);
|
|
449
|
-
return { hasLookup, hasWrite };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function classifyAsGeneral(component) {
|
|
453
|
-
if (!component || !Array.isArray(component.roleTags)) return false;
|
|
454
|
-
return component.roleTags.includes('general') && component.roleTags.length === 1;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Analysis
|
|
458
|
-
async function analyze(rootPath, options) {
|
|
459
|
-
// Validate maxFiles with strict parsing
|
|
460
|
-
let maxFiles = parseInt(options.maxFiles, 10);
|
|
461
|
-
if (isNaN(maxFiles) || maxFiles < 1 || maxFiles > 10000) {
|
|
462
|
-
maxFiles = 100;
|
|
463
|
-
}
|
|
464
|
-
// Extra safety: ensure within safe bounds
|
|
465
|
-
maxFiles = Math.min(Math.max(maxFiles, 1), 10000);
|
|
466
|
-
|
|
467
|
-
// Validate patterns type
|
|
468
|
-
let patterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'];
|
|
469
|
-
if (options.patterns) {
|
|
470
|
-
if (typeof options.patterns !== 'string') {
|
|
471
|
-
throw new TypeError('patterns must be a string');
|
|
472
|
-
}
|
|
473
|
-
patterns = options.patterns.split(',');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
let exclude = ['node_modules/**', '.git/**', 'dist/**', 'build/**', '*.test.*', '*.spec.*'];
|
|
477
|
-
if (options.exclude) {
|
|
478
|
-
if (typeof options.exclude !== 'string') {
|
|
479
|
-
throw new TypeError('exclude must be a string');
|
|
480
|
-
}
|
|
481
|
-
exclude = options.exclude.split(',');
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const files = [];
|
|
485
|
-
for (const pattern of patterns) {
|
|
486
|
-
if (!pattern || pattern.trim() === '') continue;
|
|
487
|
-
try {
|
|
488
|
-
const matches = await glob(pattern.trim(), { cwd: rootPath, absolute: true, ignore: exclude });
|
|
489
|
-
files.push(...matches);
|
|
490
|
-
} catch (e) {
|
|
491
|
-
console.warn(chalk.yellow(`⚠️ Invalid pattern: ${pattern}`));
|
|
165
|
+
if (arg === '-o' && activeCommand === 'generate-all') {
|
|
166
|
+
console.error(chalk.yellow(`🤖 Note for AI Agent: '-o' for generate-all is now '-O'. Continuing with '-O'.`));
|
|
167
|
+
resolvedArgs.push('-O');
|
|
168
|
+
continue;
|
|
492
169
|
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const uniqueFiles = [...new Set(files)].slice(0, maxFiles);
|
|
496
|
-
const components = [];
|
|
497
|
-
const languages = {};
|
|
498
|
-
const directories = new Set();
|
|
499
|
-
const entryPoints = [];
|
|
500
|
-
const seenNames = new Set();
|
|
501
170
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
console.warn(chalk.yellow(`⚠️ Skipping large file: ${path.basename(filePath)} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`));
|
|
171
|
+
if (!commandFound && i >= 2 && arg === activeCommand) {
|
|
172
|
+
if (arg === 'test') {
|
|
173
|
+
console.error(chalk.yellow(`🤖 Note for AI Agent: 'test' was renamed to 'validate'. Continuing execution...`));
|
|
174
|
+
resolvedArgs.push('validate');
|
|
175
|
+
commandFound = true;
|
|
508
176
|
continue;
|
|
509
177
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
rel = './' + rel;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
languages[lang] = (languages[lang] || 0) + 1;
|
|
519
|
-
if (dir !== '.') directories.add(dir);
|
|
520
|
-
|
|
521
|
-
// Support more entry point patterns (with escaped regex)
|
|
522
|
-
const entryPattern = /\/(index|main|app|server)\.(ts|js|tsx|jsx|mts|mjs|py|go|rs)$/i;
|
|
523
|
-
if (entryPattern.test(rel)) {
|
|
524
|
-
entryPoints.push(rel);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Handle duplicate names
|
|
528
|
-
let baseName = path.basename(filePath, path.extname(filePath));
|
|
529
|
-
let uniqueName = baseName;
|
|
530
|
-
let counter = 1;
|
|
531
|
-
while (seenNames.has(uniqueName)) {
|
|
532
|
-
uniqueName = `${baseName}_${counter}`;
|
|
533
|
-
counter++;
|
|
534
|
-
}
|
|
535
|
-
seenNames.add(uniqueName);
|
|
536
|
-
|
|
537
|
-
const imports = extractImportsWithPositions(content, lang);
|
|
538
|
-
const type = inferType(filePath, content);
|
|
539
|
-
|
|
540
|
-
components.push({
|
|
541
|
-
name: uniqueName,
|
|
542
|
-
originalName: baseName,
|
|
543
|
-
filePath: rel,
|
|
544
|
-
type,
|
|
545
|
-
imports,
|
|
546
|
-
roleTags: inferRoleTags(rel, baseName, content, imports, type),
|
|
547
|
-
directory: dir,
|
|
548
|
-
});
|
|
549
|
-
} catch (e) {
|
|
550
|
-
if (process.env.DEBUG) {
|
|
551
|
-
// Sanitize path to avoid info disclosure - show only basename
|
|
552
|
-
const safePath = path.basename(filePath);
|
|
553
|
-
console.error(chalk.gray(`Skipped ${safePath}: ${e.message}`));
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Resolve dependencies
|
|
559
|
-
for (const comp of components) {
|
|
560
|
-
comp.dependencies = [];
|
|
561
|
-
for (const imp of comp.imports) {
|
|
562
|
-
const importPath = getImportPath(imp);
|
|
563
|
-
if (!importPath) continue;
|
|
564
|
-
const resolved = resolveInternalImport(comp.filePath, importPath, rootPath);
|
|
565
|
-
if (!resolved) continue;
|
|
566
|
-
const dep = findComponentByResolvedPath(components, resolved);
|
|
567
|
-
if (dep) comp.dependencies.push(dep.name);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return { rootPath, components, entryPoints, languages, directories: [...directories].sort() };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Diagram generators
|
|
575
|
-
function generateArchitecture(data, focus) {
|
|
576
|
-
if (!data || !Array.isArray(data.components)) {
|
|
577
|
-
return 'graph TD\n Note["No data available"]';
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const lines = ['graph TD'];
|
|
581
|
-
const focusNorm = focus ? normalizePath(focus) : null;
|
|
582
|
-
// Use exact path matching for focus
|
|
583
|
-
const comps = focusNorm
|
|
584
|
-
? data.components.filter(c => {
|
|
585
|
-
const normalizedFilePath = normalizePath(c.filePath || '');
|
|
586
|
-
const normalizedName = c.name || '';
|
|
587
|
-
// Check if focus is at path boundary
|
|
588
|
-
return normalizedFilePath === focusNorm ||
|
|
589
|
-
normalizedFilePath.startsWith(focusNorm + '/') ||
|
|
590
|
-
normalizedName === focusNorm;
|
|
591
|
-
})
|
|
592
|
-
: data.components;
|
|
593
|
-
|
|
594
|
-
if (comps.length === 0) {
|
|
595
|
-
lines.push(' Note["No components found' + (focus ? ' for focus: ' + escapeMermaid(focus) : '') + '"]');
|
|
596
|
-
return lines.join('\n');
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const byDir = new Map();
|
|
600
|
-
for (const c of comps) {
|
|
601
|
-
const dir = c.directory || 'root';
|
|
602
|
-
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
603
|
-
byDir.get(dir).push(c);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
for (const [dir, items] of byDir) {
|
|
607
|
-
if (items.length === 0) continue;
|
|
608
|
-
lines.push(` subgraph ${sanitize(dir)}["${escapeMermaid(dir)}"]`);
|
|
609
|
-
for (const c of items) {
|
|
610
|
-
const shape = c.type === 'service' ? '[[' : '[';
|
|
611
|
-
const end = c.type === 'service' ? ']]' : ']';
|
|
612
|
-
lines.push(` ${sanitize(c.name)}${shape}"${escapeMermaid(c.originalName)}"${end}`);
|
|
613
|
-
}
|
|
614
|
-
lines.push(' end');
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
for (const c of comps) {
|
|
618
|
-
for (const d of c.dependencies) {
|
|
619
|
-
if (comps.find(x => x.name === d)) {
|
|
620
|
-
lines.push(` ${sanitize(c.name)} --> ${sanitize(d)}`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Track styled nodes to avoid duplicates
|
|
626
|
-
const styledNodes = new Set();
|
|
627
|
-
for (const ep of data.entryPoints) {
|
|
628
|
-
const epName = path.basename(ep, path.extname(ep));
|
|
629
|
-
const comp = comps.find(c => c.originalName === epName);
|
|
630
|
-
if (comp && !styledNodes.has(comp.name)) {
|
|
631
|
-
lines.push(` style ${sanitize(comp.name)} fill:#4f46e5,color:#fff`);
|
|
632
|
-
styledNodes.add(comp.name);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return lines.join('\n');
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function generateSequence(data) {
|
|
640
|
-
if (!data || !Array.isArray(data.components)) {
|
|
641
|
-
return 'sequenceDiagram\n Note over User,App: No data available';
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const lines = ['sequenceDiagram'];
|
|
645
|
-
// Use configurable limit with warning
|
|
646
|
-
const MAX_SERVICES = 6;
|
|
647
|
-
const services = data.components.filter(c => c.type === 'service' || c.name === 'index').slice(0, MAX_SERVICES);
|
|
648
|
-
if (data.components.length > MAX_SERVICES) {
|
|
649
|
-
console.warn(chalk.yellow(`⚠️ Sequence diagram limited to ${MAX_SERVICES} services`));
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (services.length === 0) {
|
|
653
|
-
lines.push(' Note over User,App: No services detected');
|
|
654
|
-
return lines.join('\n');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Track used sanitized names to prevent collisions
|
|
658
|
-
const usedNames = new Map();
|
|
659
|
-
const getSafeName = (service) => {
|
|
660
|
-
const base = sanitize(service.name);
|
|
661
|
-
if (!usedNames.has(base)) {
|
|
662
|
-
usedNames.set(base, service.name);
|
|
663
|
-
return base;
|
|
664
|
-
}
|
|
665
|
-
// Collision - append number
|
|
666
|
-
let i = 1;
|
|
667
|
-
let newName = `${base}_${i}`;
|
|
668
|
-
while (usedNames.has(newName)) {
|
|
669
|
-
i++;
|
|
670
|
-
newName = `${base}_${i}`;
|
|
671
|
-
}
|
|
672
|
-
usedNames.set(newName, service.name);
|
|
673
|
-
return newName;
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
const safeNames = services.map(getSafeName);
|
|
677
|
-
|
|
678
|
-
for (let i = 0; i < services.length; i++) {
|
|
679
|
-
lines.push(` participant ${safeNames[i]} as ${escapeMermaid(services[i].originalName)}`);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
for (let i = 0; i < services.length - 1; i++) {
|
|
683
|
-
lines.push(` ${safeNames[i]}->>${safeNames[i+1]}: calls`);
|
|
684
|
-
}
|
|
685
|
-
return lines.join('\n');
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function generateDependency(data, focus) {
|
|
689
|
-
if (!data || !Array.isArray(data.components)) {
|
|
690
|
-
return 'graph LR\n Note["No data available"]';
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const lines = ['graph LR'];
|
|
694
|
-
const focusNorm = focus ? normalizePath(focus) : null;
|
|
695
|
-
const comps = focusNorm ? data.components.filter(c => {
|
|
696
|
-
const normalizedPath = normalizePath(c.filePath || '');
|
|
697
|
-
return normalizedPath === focusNorm || normalizedPath.startsWith(focusNorm + '/');
|
|
698
|
-
}) : data.components;
|
|
699
|
-
|
|
700
|
-
if (comps.length === 0) {
|
|
701
|
-
lines.push(' Note["No components found"]');
|
|
702
|
-
return lines.join('\n');
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const external = new Set();
|
|
706
|
-
|
|
707
|
-
for (const c of comps) {
|
|
708
|
-
const imports = Array.isArray(c.imports) ? c.imports : [];
|
|
709
|
-
for (const importInfo of imports) {
|
|
710
|
-
const importPath = getImportPath(importInfo);
|
|
711
|
-
if (!importPath) continue;
|
|
712
|
-
if (!importPath.startsWith('.')) {
|
|
713
|
-
const pkg = getExternalPackageName(importPath);
|
|
714
|
-
if (pkg) {
|
|
715
|
-
external.add(pkg);
|
|
716
|
-
lines.push(` ${sanitize(pkg)}["${escapeMermaid(pkg)}"] --> ${sanitize(c.name)}`);
|
|
717
|
-
}
|
|
718
|
-
} else {
|
|
719
|
-
const basePath = resolveInternalImport(c.filePath, importPath, data.rootPath);
|
|
720
|
-
if (!basePath) continue;
|
|
721
|
-
const resolved = findComponentByResolvedPath(comps, basePath);
|
|
722
|
-
if (resolved) lines.push(` ${sanitize(c.name)} --> ${sanitize(resolved.name)}`);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
for (const e of external) {
|
|
728
|
-
lines.push(` style ${sanitize(e)} fill:#f59e0b,color:#fff`);
|
|
729
|
-
}
|
|
730
|
-
return lines.join('\n');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function generateClass(data) {
|
|
734
|
-
if (!data || !Array.isArray(data.components)) {
|
|
735
|
-
return 'classDiagram\n note "No data available"';
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const lines = ['classDiagram'];
|
|
739
|
-
const MAX_CLASSES = 20;
|
|
740
|
-
const classes = data.components.filter(c => c.type === 'class' || c.type === 'component').slice(0, MAX_CLASSES);
|
|
741
|
-
if (data.components.length > MAX_CLASSES) {
|
|
742
|
-
console.warn(chalk.yellow(`⚠️ Class diagram limited to ${MAX_CLASSES} classes`));
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (classes.length === 0) {
|
|
746
|
-
lines.push(' note "No classes found"');
|
|
747
|
-
return lines.join('\n');
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
for (const c of classes) {
|
|
751
|
-
lines.push(` class ${sanitize(c.name)} {`);
|
|
752
|
-
lines.push(` +${escapeMermaid(c.filePath)}`);
|
|
753
|
-
lines.push(' }');
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
for (const c of classes) {
|
|
757
|
-
const deps = (c.dependencies || []).slice(0, 3);
|
|
758
|
-
for (const d of deps) {
|
|
759
|
-
if (classes.find(x => x.name === d)) {
|
|
760
|
-
lines.push(` ${sanitize(c.name)} --> ${sanitize(d)}`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return lines.join('\n');
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function generateFlow(data) {
|
|
768
|
-
if (!data || !Array.isArray(data.components)) {
|
|
769
|
-
return 'flowchart TD\n Start(["Start"])\n End(["End"])\n Start --> End';
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const lines = ['flowchart TD'];
|
|
773
|
-
lines.push(' Start(["Start"])');
|
|
774
|
-
const MAX_COMPONENTS = 8;
|
|
775
|
-
const comps = data.components.slice(0, MAX_COMPONENTS);
|
|
776
|
-
if (data.components.length > MAX_COMPONENTS) {
|
|
777
|
-
console.warn(chalk.yellow(`⚠️ Flow diagram limited to ${MAX_COMPONENTS} components`));
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (comps.length === 0) {
|
|
781
|
-
lines.push(' End(["End"])');
|
|
782
|
-
lines.push(' Start --> End');
|
|
783
|
-
return lines.join('\n');
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
let prev = 'Start';
|
|
787
|
-
for (const c of comps) {
|
|
788
|
-
const safeName = sanitize(c.name);
|
|
789
|
-
lines.push(` ${safeName}["${escapeMermaid(c.originalName)}"]`);
|
|
790
|
-
lines.push(` ${prev} --> ${safeName}`);
|
|
791
|
-
prev = safeName;
|
|
792
|
-
}
|
|
793
|
-
lines.push(' End(["End"])');
|
|
794
|
-
lines.push(` ${prev} --> End`);
|
|
795
|
-
return lines.join('\n');
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function generateDatabase(data) {
|
|
799
|
-
if (!data || !Array.isArray(data.components)) {
|
|
800
|
-
return 'flowchart TD\n Note["No data available"]';
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const lines = ['flowchart TD'];
|
|
804
|
-
const seeds = componentsByRole(data.components, 'database');
|
|
805
|
-
if (seeds.length === 0) {
|
|
806
|
-
lines.push(' Note["No database-focused components found"]');
|
|
807
|
-
return lines.join('\n');
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
const connected = collectConnectedComponents(data.components, seeds, 2, 28);
|
|
811
|
-
const byName = byNameIndex(connected);
|
|
812
|
-
const safeNames = mapSafeNames(connected);
|
|
813
|
-
|
|
814
|
-
lines.push(' UserRequest["User request"]');
|
|
815
|
-
lines.push(' Decision{Record exists?}');
|
|
816
|
-
|
|
817
|
-
const addedEdges = new Set();
|
|
818
|
-
for (const comp of connected) {
|
|
819
|
-
if (!seeds.includes(comp)) continue;
|
|
820
|
-
const safe = safeNames.get(comp);
|
|
821
|
-
if (!safe) continue;
|
|
822
|
-
lines.push(` ${safe}["${escapeMermaid(comp.originalName)}"]`);
|
|
823
|
-
lines.push(` UserRequest --> ${safe}`);
|
|
824
|
-
|
|
825
|
-
const intent = inferDbIntent(comp);
|
|
826
|
-
if (intent.hasLookup) {
|
|
827
|
-
const lookup = `${safe}_lookup`;
|
|
828
|
-
const create = `${safe}_create`;
|
|
829
|
-
const update = `${safe}_update`;
|
|
830
|
-
lines.push(` ${safe} --> ${lookup}["lookup query"]`);
|
|
831
|
-
lines.push(` ${lookup} --> Decision`);
|
|
832
|
-
lines.push(` Decision -->|found| ${update}["update or modify"]`);
|
|
833
|
-
lines.push(` Decision -->|not found| ${create}["insert/create"]`);
|
|
834
|
-
lines.push(` ${update} --> ${safe}_result["result"]`);
|
|
835
|
-
lines.push(` ${create} --> ${safe}_result["result"]`);
|
|
836
|
-
} else if (intent.hasWrite) {
|
|
837
|
-
const write = `${safe}_write`;
|
|
838
|
-
lines.push(` ${safe} --> ${write}["write/update"]`);
|
|
839
|
-
lines.push(` ${write} --> ${safe}_result["result"]`);
|
|
840
|
-
} else {
|
|
841
|
-
const result = `${safe}_result`;
|
|
842
|
-
lines.push(` ${safe} --> ${result}["result"]`);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
for (const depName of comp.dependencies || []) {
|
|
846
|
-
const dep = byName.get(depName);
|
|
847
|
-
if (!dep || !safeNames.has(dep)) continue;
|
|
848
|
-
const edge = `${safe}->${safeNames.get(dep)}`;
|
|
849
|
-
if (!addedEdges.has(edge)) {
|
|
850
|
-
addedEdges.add(edge);
|
|
851
|
-
lines.push(` ${safe} --> ${safeNames.get(dep)}`);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
lines.push(' classDef dbNode fill:#0ea5e9,color:#fff');
|
|
857
|
-
lines.push(' classDef decisionNode fill:#0284c7,color:#fff');
|
|
858
|
-
return lines.join('\n');
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function generateUserInteractions(data) {
|
|
862
|
-
if (!data || !Array.isArray(data.components)) {
|
|
863
|
-
return 'flowchart LR\n Note["No data available"]';
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const lines = ['flowchart LR'];
|
|
867
|
-
const seeds = componentsByRole(data.components, 'user');
|
|
868
|
-
if (seeds.length === 0) {
|
|
869
|
-
lines.push(' Note["No user-facing components found"]');
|
|
870
|
-
return lines.join('\n');
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const connected = collectConnectedComponents(data.components, seeds, 1, 30);
|
|
874
|
-
const byName = byNameIndex(connected);
|
|
875
|
-
const safeNames = mapSafeNames(connected);
|
|
876
|
-
const edges = new Set();
|
|
877
|
-
|
|
878
|
-
lines.push(' User(("User"))');
|
|
879
|
-
for (const seed of seeds) {
|
|
880
|
-
const safe = safeNames.get(seed);
|
|
881
|
-
if (!safe) continue;
|
|
882
|
-
lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
|
|
883
|
-
lines.push(` User --> ${safe}`);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
for (const comp of connected) {
|
|
887
|
-
const from = safeNames.get(comp);
|
|
888
|
-
if (!from) continue;
|
|
889
|
-
for (const depName of comp.dependencies || []) {
|
|
890
|
-
const dep = byName.get(depName);
|
|
891
|
-
if (!dep) continue;
|
|
892
|
-
const to = safeNames.get(dep);
|
|
893
|
-
if (!to) continue;
|
|
894
|
-
const key = `${from}->${to}`;
|
|
895
|
-
if (!edges.has(key)) {
|
|
896
|
-
edges.add(key);
|
|
897
|
-
lines.push(` ${from} --> ${to}`);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
lines.push(' classDef userNode fill:#16a34a,color:#fff');
|
|
903
|
-
return lines.join('\n');
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function generateEvents(data) {
|
|
907
|
-
if (!data || !Array.isArray(data.components)) {
|
|
908
|
-
return 'flowchart TD\n Note["No data available"]';
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const lines = ['flowchart TD'];
|
|
912
|
-
const seeds = componentsByRole(data.components, 'events');
|
|
913
|
-
if (seeds.length === 0) {
|
|
914
|
-
lines.push(' Note["No event/channels components found"]');
|
|
915
|
-
return lines.join('\n');
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const connected = collectConnectedComponents(data.components, seeds, 2, 30);
|
|
919
|
-
const byName = byNameIndex(connected);
|
|
920
|
-
const safeNames = mapSafeNames(connected);
|
|
921
|
-
const edges = new Set();
|
|
922
|
-
|
|
923
|
-
lines.push(' subgraph Channels["Event channels / queues"]');
|
|
924
|
-
for (const component of connected) {
|
|
925
|
-
const safe = safeNames.get(component);
|
|
926
|
-
if (!safe) continue;
|
|
927
|
-
const isEventSource = seeds.includes(component);
|
|
928
|
-
if (isEventSource) {
|
|
929
|
-
lines.push(` ${safe}{{"${escapeMermaid(component.originalName)}"}}`);
|
|
930
|
-
} else {
|
|
931
|
-
lines.push(` ${safe}["${escapeMermaid(component.originalName)}"]`);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
lines.push(' end');
|
|
935
|
-
|
|
936
|
-
for (const comp of connected) {
|
|
937
|
-
const from = safeNames.get(comp);
|
|
938
|
-
if (!from) continue;
|
|
939
|
-
for (const depName of comp.dependencies || []) {
|
|
940
|
-
const dep = byName.get(depName);
|
|
941
|
-
if (!dep) continue;
|
|
942
|
-
const to = safeNames.get(dep);
|
|
943
|
-
if (!to) continue;
|
|
944
|
-
const edge = `${from}->${to}`;
|
|
945
|
-
if (!edges.has(edge)) {
|
|
946
|
-
edges.add(edge);
|
|
947
|
-
const label = seeds.includes(comp) ? '|emit|' : '|consume|';
|
|
948
|
-
lines.push(` ${from} -->${label} ${to}`);
|
|
178
|
+
if (arg === 'all') {
|
|
179
|
+
console.error(chalk.yellow(`🤖 Note for AI Agent: 'all' was renamed to 'generate-all'. Continuing execution...`));
|
|
180
|
+
resolvedArgs.push('generate-all');
|
|
181
|
+
commandFound = true;
|
|
182
|
+
continue;
|
|
949
183
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
function generateAuth(data) {
|
|
958
|
-
if (!data || !Array.isArray(data.components)) {
|
|
959
|
-
return 'flowchart TD\n Note["No data available"]';
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const lines = ['flowchart TD'];
|
|
963
|
-
const seeds = componentsByRole(data.components, 'auth');
|
|
964
|
-
if (seeds.length === 0) {
|
|
965
|
-
lines.push(' Note["No authentication components found"]');
|
|
966
|
-
return lines.join('\n');
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const connected = collectConnectedComponents(data.components, seeds, 2, 24);
|
|
970
|
-
const byName = byNameIndex(connected);
|
|
971
|
-
const safeNames = mapSafeNames(connected);
|
|
972
|
-
const edges = new Set();
|
|
973
|
-
|
|
974
|
-
lines.push(' Request["Authentication request"]');
|
|
975
|
-
lines.push(' Boundary{"Auth Boundary"}');
|
|
976
|
-
lines.push(' Request --> Boundary');
|
|
977
|
-
|
|
978
|
-
for (const seed of seeds) {
|
|
979
|
-
const safe = safeNames.get(seed);
|
|
980
|
-
if (!safe) continue;
|
|
981
|
-
lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
|
|
982
|
-
const key = `Boundary->${safe}`;
|
|
983
|
-
if (!edges.has(key)) {
|
|
984
|
-
edges.add(key);
|
|
985
|
-
lines.push(` Boundary --> ${safe}`);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
for (const comp of connected) {
|
|
990
|
-
const from = safeNames.get(comp);
|
|
991
|
-
if (!from) continue;
|
|
992
|
-
for (const depName of comp.dependencies || []) {
|
|
993
|
-
const dep = byName.get(depName);
|
|
994
|
-
if (!dep) continue;
|
|
995
|
-
const to = safeNames.get(dep);
|
|
996
|
-
if (!to) continue;
|
|
997
|
-
const key = `${from}->${to}`;
|
|
998
|
-
if (!edges.has(key)) {
|
|
999
|
-
edges.add(key);
|
|
1000
|
-
lines.push(` ${from} --> ${to}`);
|
|
184
|
+
if (arg === 'video') {
|
|
185
|
+
console.error(chalk.yellow(`🤖 Note for AI Agent: 'video' was renamed to 'generate-video'. Continuing execution...`));
|
|
186
|
+
resolvedArgs.push('generate-video');
|
|
187
|
+
commandFound = true;
|
|
188
|
+
continue;
|
|
1001
189
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
for (const pkg of collectExternalImports(seed.imports || [])) {
|
|
1008
|
-
providerSet.add(pkg);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
for (const provider of providerSet) {
|
|
1012
|
-
const providerNode = sanitize(provider);
|
|
1013
|
-
lines.push(` ${providerNode}[("${escapeMermaid(provider)}")]`);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
lines.push(' classDef authNode fill:#7c3aed,color:#fff');
|
|
1017
|
-
return lines.join('\n');
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function generateSecurity(data) {
|
|
1021
|
-
if (!data || !Array.isArray(data.components)) {
|
|
1022
|
-
return 'flowchart TD\n Note["No data available"]';
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const lines = ['flowchart TD'];
|
|
1026
|
-
const seeds = [
|
|
1027
|
-
...componentsByRole(data.components, 'security'),
|
|
1028
|
-
...componentsByRole(data.components, 'auth'),
|
|
1029
|
-
...componentsByRole(data.components, 'integrations'),
|
|
1030
|
-
].filter((value, index, arr) => arr.indexOf(value) === index);
|
|
1031
|
-
|
|
1032
|
-
if (seeds.length === 0) {
|
|
1033
|
-
lines.push(' Note["No security-focused components found"]');
|
|
1034
|
-
return lines.join('\n');
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const connected = collectConnectedComponents(data.components, seeds, 2, 40);
|
|
1038
|
-
const byName = byNameIndex(connected);
|
|
1039
|
-
const safeNames = mapSafeNames(connected);
|
|
1040
|
-
const edges = new Set();
|
|
1041
|
-
|
|
1042
|
-
lines.push(' Untrusted["Untrusted input"]');
|
|
1043
|
-
for (const seed of seeds) {
|
|
1044
|
-
const safe = safeNames.get(seed);
|
|
1045
|
-
if (!safe) continue;
|
|
1046
|
-
lines.push(` ${safe}["${escapeMermaid(seed.originalName)}"]`);
|
|
1047
|
-
const key = `Untrusted->${safe}`;
|
|
1048
|
-
if (!edges.has(key)) {
|
|
1049
|
-
edges.add(key);
|
|
1050
|
-
lines.push(` Untrusted --> ${safe}`);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
for (const comp of connected) {
|
|
1055
|
-
const from = safeNames.get(comp);
|
|
1056
|
-
if (!from) continue;
|
|
1057
|
-
for (const depName of comp.dependencies || []) {
|
|
1058
|
-
const dep = byName.get(depName);
|
|
1059
|
-
if (!dep) continue;
|
|
1060
|
-
const to = safeNames.get(dep);
|
|
1061
|
-
if (!to) continue;
|
|
1062
|
-
const key = `${from}->${to}`;
|
|
1063
|
-
if (!edges.has(key)) {
|
|
1064
|
-
edges.add(key);
|
|
1065
|
-
lines.push(` ${from} --> ${to}`);
|
|
190
|
+
if (arg === 'animate') {
|
|
191
|
+
console.error(chalk.yellow(`🤖 Note for AI Agent: 'animate' was renamed to 'generate-animated'. Continuing execution...`));
|
|
192
|
+
resolvedArgs.push('generate-animated');
|
|
193
|
+
commandFound = true;
|
|
194
|
+
continue;
|
|
1066
195
|
}
|
|
196
|
+
commandFound = true;
|
|
1067
197
|
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
lines.push(' classDef securityNode fill:#dc2626,color:#fff');
|
|
1071
|
-
return lines.join('\n');
|
|
1072
|
-
}
|
|
1073
198
|
|
|
1074
|
-
|
|
1075
|
-
switch (type) {
|
|
1076
|
-
case 'architecture': return generateArchitecture(data, focus);
|
|
1077
|
-
case 'sequence': return generateSequence(data);
|
|
1078
|
-
case 'dependency': return generateDependency(data, focus);
|
|
1079
|
-
case 'class': return generateClass(data);
|
|
1080
|
-
case 'flow': return generateFlow(data);
|
|
1081
|
-
case 'database': return generateDatabase(data);
|
|
1082
|
-
case 'user': return generateUserInteractions(data);
|
|
1083
|
-
case 'events': return generateEvents(data);
|
|
1084
|
-
case 'auth': return generateAuth(data);
|
|
1085
|
-
case 'security': return generateSecurity(data);
|
|
1086
|
-
default:
|
|
1087
|
-
console.warn(chalk.yellow(`⚠️ Unknown diagram type "${type}", using architecture`));
|
|
1088
|
-
return generateArchitecture(data, focus);
|
|
199
|
+
resolvedArgs.push(arg);
|
|
1089
200
|
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function isPlaceholderDiagram(mermaidCode) {
|
|
1093
|
-
if (!mermaidCode || typeof mermaidCode !== 'string') return true;
|
|
1094
|
-
const compact = mermaidCode.toLowerCase();
|
|
1095
|
-
return compact.includes('note["no data available"]')
|
|
1096
|
-
|| compact.includes('note["no components found')
|
|
1097
|
-
|| compact.includes('no services detected')
|
|
1098
|
-
|| compact.includes('note "no data available"')
|
|
1099
|
-
|| compact.includes('note "no classes found"')
|
|
1100
|
-
|| compact.includes('note["no database-focused components found"]')
|
|
1101
|
-
|| compact.includes('note["no user-facing components found"]')
|
|
1102
|
-
|| compact.includes('note["no event/channels components found"]')
|
|
1103
|
-
|| compact.includes('note["no authentication components found"]')
|
|
1104
|
-
|| compact.includes('note["no security-focused components found"]')
|
|
1105
|
-
|| compact.includes('no architecture data');
|
|
1106
|
-
}
|
|
1107
201
|
|
|
1108
|
-
|
|
1109
|
-
const lines = typeof mermaidCode === 'string' ? mermaidCode.split('\n') : [];
|
|
1110
|
-
return {
|
|
1111
|
-
type,
|
|
1112
|
-
file: path.basename(filePath),
|
|
1113
|
-
outputPath: rootPath ? path.relative(rootPath, filePath) : filePath,
|
|
1114
|
-
lines: lines.length,
|
|
1115
|
-
bytes: Buffer.byteLength(mermaidCode || '', 'utf8'),
|
|
1116
|
-
isPlaceholder: isPlaceholderDiagram(mermaidCode),
|
|
1117
|
-
};
|
|
202
|
+
return resolvedArgs;
|
|
1118
203
|
}
|
|
1119
204
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
205
|
+
if (require.main === module) {
|
|
206
|
+
const diagramRc = loadDiagramRc(process.cwd());
|
|
207
|
+
program.diagramContext = { diagramRc };
|
|
208
|
+
const resolvedArgs = resolveAliasArgs(process.argv);
|
|
209
|
+
emitCompatibilityInvocationNotice(process.argv);
|
|
210
|
+
program.parse(resolvedArgs);
|
|
1123
211
|
}
|
|
1124
212
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
const missing = SUPPORTED_DIAGRAM_TYPES.filter(
|
|
1138
|
-
(type) => !diagrams.some((diagram) => diagram.type === type)
|
|
1139
|
-
);
|
|
1140
|
-
const placeholderTypes = diagrams.filter((diagram) => diagram.isPlaceholder).map((diagram) => diagram.type);
|
|
1141
|
-
|
|
1142
|
-
return {
|
|
1143
|
-
generatedAt: manifest.generatedAt || new Date().toISOString(),
|
|
1144
|
-
rootPath: manifest.rootPath,
|
|
1145
|
-
diagramDir: manifest.diagramDir,
|
|
1146
|
-
totalDiagrams: diagrams.length,
|
|
1147
|
-
placeholders: placeholderTypes.length,
|
|
1148
|
-
placeholderTypes,
|
|
1149
|
-
missingTypes: missing,
|
|
1150
|
-
diagrams,
|
|
213
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
214
|
+
module.exports = {
|
|
215
|
+
CANONICAL_COMMAND_NAME,
|
|
216
|
+
COMPATIBILITY_COMMAND_NAME,
|
|
217
|
+
COMPATIBILITY_NOTICE,
|
|
218
|
+
generateHtmlExplainer,
|
|
219
|
+
getInvocationName,
|
|
220
|
+
groupChangePaths,
|
|
221
|
+
buildRiskNarrative,
|
|
222
|
+
buildSummaryMeta,
|
|
223
|
+
escapeHtml,
|
|
224
|
+
isCompatibilityInvocation,
|
|
1151
225
|
};
|
|
1152
226
|
}
|
|
1153
|
-
|
|
1154
|
-
// URL shortening for large diagrams
|
|
1155
|
-
function createMermaidUrl(mermaidCode) {
|
|
1156
|
-
// If diagram is very large, provide text file instead
|
|
1157
|
-
if (mermaidCode.length > 5000) {
|
|
1158
|
-
return { url: null, large: true };
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
try {
|
|
1162
|
-
const payload = JSON.stringify({ code: mermaidCode });
|
|
1163
|
-
const compressed = zlib.deflateSync(payload);
|
|
1164
|
-
const encoded = compressed
|
|
1165
|
-
.toString('base64')
|
|
1166
|
-
.replace(/\+/g, '-')
|
|
1167
|
-
.replace(/\//g, '_')
|
|
1168
|
-
.replace(/=+$/g, '');
|
|
1169
|
-
const url = `https://mermaid.live/edit#pako:${encoded}`;
|
|
1170
|
-
|
|
1171
|
-
// Check if URL is too long for browser
|
|
1172
|
-
if (url.length > 8000) {
|
|
1173
|
-
return { url: null, large: true };
|
|
1174
|
-
}
|
|
1175
|
-
return { url, large: false };
|
|
1176
|
-
} catch (e) {
|
|
1177
|
-
return { url: null, large: true };
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Validate output path to prevent directory traversal
|
|
1182
|
-
function validateOutputPath(outputPath, rootPath) {
|
|
1183
|
-
if (typeof outputPath !== 'string' || outputPath.trim() === '') {
|
|
1184
|
-
throw new Error('Invalid path: output path is required');
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// Security: Check for null bytes
|
|
1188
|
-
if (outputPath.includes('\0')) {
|
|
1189
|
-
throw new Error('Invalid path: null bytes detected');
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// Resolve symlinks to prevent symlink attacks
|
|
1193
|
-
let realRoot;
|
|
1194
|
-
try {
|
|
1195
|
-
realRoot = fs.realpathSync(rootPath);
|
|
1196
|
-
} catch (e) {
|
|
1197
|
-
throw new Error(`Invalid project path: ${rootPath}`);
|
|
1198
|
-
}
|
|
1199
|
-
const resolved = path.isAbsolute(outputPath)
|
|
1200
|
-
? path.resolve(outputPath)
|
|
1201
|
-
: path.resolve(realRoot, outputPath);
|
|
1202
|
-
|
|
1203
|
-
const resolveViaExistingAncestor = (targetPath) => {
|
|
1204
|
-
const pending = [];
|
|
1205
|
-
let probe = targetPath;
|
|
1206
|
-
|
|
1207
|
-
while (!fs.existsSync(probe)) {
|
|
1208
|
-
pending.unshift(path.basename(probe));
|
|
1209
|
-
const parent = path.dirname(probe);
|
|
1210
|
-
if (parent === probe) {
|
|
1211
|
-
break;
|
|
1212
|
-
}
|
|
1213
|
-
probe = parent;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const canonicalBase = fs.realpathSync(probe);
|
|
1217
|
-
return path.join(canonicalBase, ...pending);
|
|
1218
|
-
};
|
|
1219
|
-
|
|
1220
|
-
const canonicalResolved = resolveViaExistingAncestor(resolved);
|
|
1221
|
-
const relative = path.relative(realRoot, canonicalResolved);
|
|
1222
|
-
|
|
1223
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
1224
|
-
throw new Error(`Invalid path: directory traversal detected in "${outputPath}"`);
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
return canonicalResolved;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
function resolveRootPathOrExit(targetPath) {
|
|
1231
|
-
const root = path.resolve(targetPath || '.');
|
|
1232
|
-
try {
|
|
1233
|
-
const stats = fs.statSync(root);
|
|
1234
|
-
if (!stats.isDirectory()) {
|
|
1235
|
-
console.error(chalk.red('❌ Path error:'), `Target is not a directory: ${root}`);
|
|
1236
|
-
process.exit(2);
|
|
1237
|
-
}
|
|
1238
|
-
} catch (error) {
|
|
1239
|
-
console.error(chalk.red('❌ Path error:'), `Target directory not found: ${root}`);
|
|
1240
|
-
process.exit(2);
|
|
1241
|
-
}
|
|
1242
|
-
return root;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
function openPreviewUrl(url) {
|
|
1246
|
-
const { cmd, args } = getOpenCommand(url, process.platform);
|
|
1247
|
-
try {
|
|
1248
|
-
const child = spawn(cmd, args, {
|
|
1249
|
-
stdio: 'ignore',
|
|
1250
|
-
detached: true,
|
|
1251
|
-
windowsHide: true
|
|
1252
|
-
});
|
|
1253
|
-
child.on('error', (err) => {
|
|
1254
|
-
console.error(chalk.yellow('⚠️ Failed to open browser:'), err.message);
|
|
1255
|
-
});
|
|
1256
|
-
child.unref();
|
|
1257
|
-
} catch (err) {
|
|
1258
|
-
console.error(chalk.yellow('⚠️ Failed to open browser:'), err.message);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function runMermaidCli(args) {
|
|
1263
|
-
const candidates = getNpxCommandCandidates(process.platform);
|
|
1264
|
-
let lastError = null;
|
|
1265
|
-
for (const candidate of candidates) {
|
|
1266
|
-
try {
|
|
1267
|
-
execFileSync(candidate, args, { stdio: 'pipe', windowsHide: true });
|
|
1268
|
-
return;
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
lastError = error;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
if (lastError) {
|
|
1274
|
-
throw lastError;
|
|
1275
|
-
}
|
|
1276
|
-
throw new Error('npx command not found');
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
const ALLOWED_THEMES = ['default', 'dark', 'forest', 'neutral', 'light'];
|
|
1280
|
-
|
|
1281
|
-
function normalizeThemeOption(theme, fallback = 'default') {
|
|
1282
|
-
const normalized = String(theme || fallback).toLowerCase();
|
|
1283
|
-
return ALLOWED_THEMES.includes(normalized) ? normalized : fallback;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function validateExistingPathInRoot(targetPath, rootPath, label = 'path') {
|
|
1287
|
-
const realRoot = fs.realpathSync(rootPath);
|
|
1288
|
-
const realTarget = fs.realpathSync(targetPath);
|
|
1289
|
-
const relative = path.relative(realRoot, realTarget);
|
|
1290
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
1291
|
-
throw new Error(`Invalid ${label}: path escapes project root`);
|
|
1292
|
-
}
|
|
1293
|
-
return realTarget;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// Commands
|
|
1297
|
-
program
|
|
1298
|
-
.name('diagram')
|
|
1299
|
-
.description('Generate architecture diagrams from code')
|
|
1300
|
-
.version(packageJson.version);
|
|
1301
|
-
|
|
1302
|
-
program
|
|
1303
|
-
.command('analyze [path]')
|
|
1304
|
-
.description('Analyze codebase structure')
|
|
1305
|
-
.option('-p, --patterns <list>', 'File patterns (comma-separated)', '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs')
|
|
1306
|
-
.option('-e, --exclude <list>', 'Exclude patterns', 'node_modules/**,.git/**,dist/**')
|
|
1307
|
-
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
1308
|
-
.option('-j, --json', 'Output as JSON')
|
|
1309
|
-
.action(async (targetPath, options) => {
|
|
1310
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1311
|
-
if (!options.json) {
|
|
1312
|
-
console.log(chalk.blue('Analyzing'), root);
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
const data = await analyze(root, options);
|
|
1316
|
-
|
|
1317
|
-
if (options.json) {
|
|
1318
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1319
|
-
} else {
|
|
1320
|
-
console.log(chalk.green('\n📊 Summary'));
|
|
1321
|
-
console.log(` Files: ${data.components.length}`);
|
|
1322
|
-
console.log(` Languages: ${Object.entries(data.languages).map(([k,v]) => `${k}(${v})`).join(', ') || 'none'}`);
|
|
1323
|
-
console.log(` Entry points: ${data.entryPoints.join(', ') || 'none'}`);
|
|
1324
|
-
console.log(`\n${chalk.yellow('Components:')}`);
|
|
1325
|
-
data.components.slice(0, 15).forEach(c => {
|
|
1326
|
-
const deps = c.dependencies.length > 0 ? ` → ${c.dependencies.slice(0, 3).join(', ')}` : '';
|
|
1327
|
-
console.log(` ${c.originalName} (${c.type})${deps}`);
|
|
1328
|
-
});
|
|
1329
|
-
if (data.components.length > 15) {
|
|
1330
|
-
console.log(chalk.gray(` ... and ${data.components.length - 15} more`));
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
program
|
|
1336
|
-
.command('generate [path]')
|
|
1337
|
-
.description('Generate a diagram')
|
|
1338
|
-
.option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow, database, user, events, auth, security', 'architecture')
|
|
1339
|
-
.option('-f, --focus <module>', 'Focus on specific module')
|
|
1340
|
-
.option('-o, --output <file>', 'Output file (SVG/PNG)')
|
|
1341
|
-
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
1342
|
-
.option('--theme <theme>', 'Theme: default, dark, forest, neutral', 'default')
|
|
1343
|
-
.option('--open', 'Open in browser')
|
|
1344
|
-
.action(async (targetPath, options) => {
|
|
1345
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1346
|
-
const requestedTheme = String(options.theme || 'default').toLowerCase();
|
|
1347
|
-
const safeTheme = normalizeThemeOption(options.theme, 'default');
|
|
1348
|
-
if (requestedTheme !== safeTheme) {
|
|
1349
|
-
console.warn(chalk.yellow(`⚠️ Unknown theme "${options.theme}", using "${safeTheme}"`));
|
|
1350
|
-
}
|
|
1351
|
-
console.log(chalk.blue('Generating'), options.type, 'diagram for', root);
|
|
1352
|
-
|
|
1353
|
-
const data = await analyze(root, options);
|
|
1354
|
-
const mermaid = generate(data, options.type, options.focus);
|
|
1355
|
-
|
|
1356
|
-
console.log(chalk.green('\n📐 Mermaid Diagram:\n'));
|
|
1357
|
-
console.log('```mermaid');
|
|
1358
|
-
console.log(mermaid);
|
|
1359
|
-
console.log('```\n');
|
|
1360
|
-
|
|
1361
|
-
// Preview URL
|
|
1362
|
-
const { url, large } = createMermaidUrl(mermaid);
|
|
1363
|
-
|
|
1364
|
-
if (large || !url) {
|
|
1365
|
-
console.log(chalk.yellow('⚠️ Diagram is too large for preview URL.'));
|
|
1366
|
-
console.log(chalk.cyan('💾 Save to file:'), 'diagram generate . --output diagram.svg');
|
|
1367
|
-
} else {
|
|
1368
|
-
console.log(chalk.cyan('🔗 Preview:'), url);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// Save to file if requested
|
|
1372
|
-
if (options.output) {
|
|
1373
|
-
// Validate output path for security
|
|
1374
|
-
let safeOutput;
|
|
1375
|
-
try {
|
|
1376
|
-
safeOutput = validateOutputPath(options.output, root);
|
|
1377
|
-
} catch (err) {
|
|
1378
|
-
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1379
|
-
process.exit(2);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Ensure output directory exists
|
|
1383
|
-
const outputDir = path.dirname(safeOutput);
|
|
1384
|
-
if (!fs.existsSync(outputDir)) {
|
|
1385
|
-
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
const ext = path.extname(options.output).toLowerCase();
|
|
1389
|
-
if (ext === '.md' || ext === '.mmd') {
|
|
1390
|
-
fs.writeFileSync(safeOutput, mermaid);
|
|
1391
|
-
console.log(chalk.green('✅ Saved to'), options.output);
|
|
1392
|
-
} else {
|
|
1393
|
-
// Try to render
|
|
1394
|
-
let tempFile = null;
|
|
1395
|
-
try {
|
|
1396
|
-
// Use crypto for secure random filename
|
|
1397
|
-
const randomId = crypto.randomBytes(16).toString('hex');
|
|
1398
|
-
tempFile = path.join(os.tmpdir(), `diagram-${Date.now()}-${randomId}.mmd`);
|
|
1399
|
-
fs.writeFileSync(tempFile, `%%{init: {'theme': '${safeTheme}'}}%%\n${mermaid}`);
|
|
1400
|
-
runMermaidCli(['-y', '@mermaid-js/mermaid-cli', 'mmdc', '-i', tempFile, '-o', safeOutput, '-b', 'transparent']);
|
|
1401
|
-
fs.unlinkSync(tempFile);
|
|
1402
|
-
console.log(chalk.green('✅ Rendered to'), options.output);
|
|
1403
|
-
} catch (e) {
|
|
1404
|
-
if (tempFile && fs.existsSync(tempFile)) {
|
|
1405
|
-
try { fs.unlinkSync(tempFile); } catch (e2) {}
|
|
1406
|
-
}
|
|
1407
|
-
console.error(chalk.red('❌ Could not render output file. Install mermaid-cli: npm i -g @mermaid-js/mermaid-cli'));
|
|
1408
|
-
if (process.env.DEBUG) console.error(chalk.gray(e.message));
|
|
1409
|
-
process.exit(2);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
if (options.open && url) {
|
|
1415
|
-
// Security: Validate URL protocol
|
|
1416
|
-
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
1417
|
-
console.error(chalk.red('❌ Invalid URL protocol'));
|
|
1418
|
-
} else {
|
|
1419
|
-
openPreviewUrl(url);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
program
|
|
1425
|
-
.command('all [path]')
|
|
1426
|
-
.description('Generate all diagram types')
|
|
1427
|
-
.option('-o, --output-dir <dir>', 'Output directory', './diagrams')
|
|
1428
|
-
.option('-p, --patterns <list>', 'File patterns', '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs')
|
|
1429
|
-
.option('-e, --exclude <list>', 'Exclude patterns', 'node_modules/**,.git/**,dist/**')
|
|
1430
|
-
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
1431
|
-
.action(async (targetPath, options) => {
|
|
1432
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1433
|
-
let outDir;
|
|
1434
|
-
try {
|
|
1435
|
-
outDir = validateOutputPath(options.outputDir, root);
|
|
1436
|
-
} catch (err) {
|
|
1437
|
-
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1438
|
-
process.exit(2);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
console.log(chalk.blue('Analyzing'), root);
|
|
1442
|
-
const data = await analyze(root, options);
|
|
1443
|
-
|
|
1444
|
-
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
1445
|
-
|
|
1446
|
-
const types = [...SUPPORTED_DIAGRAM_TYPES];
|
|
1447
|
-
const manifest = {
|
|
1448
|
-
generatedAt: new Date().toISOString(),
|
|
1449
|
-
rootPath: root,
|
|
1450
|
-
diagramDir: path.relative(root, outDir) || '.',
|
|
1451
|
-
diagrams: [],
|
|
1452
|
-
};
|
|
1453
|
-
|
|
1454
|
-
for (const type of types) {
|
|
1455
|
-
const mermaid = generate(data, type);
|
|
1456
|
-
const file = path.join(outDir, `${type}.mmd`);
|
|
1457
|
-
fs.writeFileSync(file, mermaid);
|
|
1458
|
-
manifest.diagrams.push(toManifestEntry(type, file, mermaid, root));
|
|
1459
|
-
console.log(chalk.green('✅'), type, '→', file);
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
const manifestPath = path.join(outDir, 'manifest.json');
|
|
1463
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1464
|
-
console.log(chalk.green('✅ manifest'), '→', manifestPath);
|
|
1465
|
-
|
|
1466
|
-
console.log(chalk.cyan('\n🔗 Preview all at: https://mermaid.live'));
|
|
1467
|
-
});
|
|
1468
|
-
|
|
1469
|
-
program
|
|
1470
|
-
.command('manifest [path]')
|
|
1471
|
-
.description('Summarize manifest.json from a diagram output directory')
|
|
1472
|
-
.option('-d, --manifest-dir <dir>', 'Directory containing manifest.json', '.diagram')
|
|
1473
|
-
.option('-o, --output <file>', 'Write summary JSON to a file')
|
|
1474
|
-
.option('--require-types <list>', 'Require all listed diagram types, comma-separated')
|
|
1475
|
-
.option(
|
|
1476
|
-
'--fail-on-placeholder',
|
|
1477
|
-
'Fail if any required diagram was a placeholder (or any placeholder if no required types are set)'
|
|
1478
|
-
)
|
|
1479
|
-
.action(async (targetPath, options) => {
|
|
1480
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1481
|
-
const manifestDir = path.join(root, options.manifestDir || '.diagram');
|
|
1482
|
-
const manifestPath = path.join(manifestDir, 'manifest.json');
|
|
1483
|
-
|
|
1484
|
-
let safeManifestPath;
|
|
1485
|
-
try {
|
|
1486
|
-
safeManifestPath = validateExistingPathInRoot(manifestPath, root, 'manifest path');
|
|
1487
|
-
} catch (err) {
|
|
1488
|
-
console.error(chalk.red('❌ Manifest error:'), err.message);
|
|
1489
|
-
process.exit(2);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
let manifestRaw;
|
|
1493
|
-
try {
|
|
1494
|
-
manifestRaw = fs.readFileSync(safeManifestPath, 'utf8');
|
|
1495
|
-
} catch (err) {
|
|
1496
|
-
console.error(chalk.red('❌ Manifest read failed:'), err.message);
|
|
1497
|
-
process.exit(2);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
let parsedManifest;
|
|
1501
|
-
try {
|
|
1502
|
-
parsedManifest = JSON.parse(manifestRaw);
|
|
1503
|
-
} catch (err) {
|
|
1504
|
-
console.error(chalk.red('❌ Manifest parse failed:'), err.message);
|
|
1505
|
-
process.exit(2);
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
const summary = buildManifestSummary(parsedManifest);
|
|
1509
|
-
if (!summary) {
|
|
1510
|
-
console.error(chalk.red('❌ Invalid manifest format'));
|
|
1511
|
-
process.exit(2);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
const required = parseCommaSeparatedList(options.requireTypes);
|
|
1515
|
-
const missingRequired = required.filter((type) => !summary.diagrams.some((d) => d.type === type));
|
|
1516
|
-
summary.required = {
|
|
1517
|
-
requested: required,
|
|
1518
|
-
missing: missingRequired,
|
|
1519
|
-
};
|
|
1520
|
-
|
|
1521
|
-
if (required.length > 0 && missingRequired.length > 0) {
|
|
1522
|
-
console.error(chalk.red(`❌ Manifest missing required diagram types: ${missingRequired.join(', ')}`));
|
|
1523
|
-
process.exit(2);
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
const placeholderTypesToCheck = required.length > 0
|
|
1527
|
-
? summary.placeholderTypes.filter((type) => required.includes(type))
|
|
1528
|
-
: summary.placeholderTypes;
|
|
1529
|
-
|
|
1530
|
-
if (options.failOnPlaceholder && placeholderTypesToCheck.length > 0) {
|
|
1531
|
-
console.error(
|
|
1532
|
-
chalk.yellow(
|
|
1533
|
-
`⚠️ Manifest includes ${placeholderTypesToCheck.length} required placeholder diagram(s): ${placeholderTypesToCheck.join(', ')}`
|
|
1534
|
-
)
|
|
1535
|
-
);
|
|
1536
|
-
process.exit(2);
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
if (options.output) {
|
|
1540
|
-
let safeOutput;
|
|
1541
|
-
try {
|
|
1542
|
-
safeOutput = validateOutputPath(options.output, root);
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1545
|
-
process.exit(2);
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
const outputDir = path.dirname(safeOutput);
|
|
1549
|
-
if (!fs.existsSync(outputDir)) {
|
|
1550
|
-
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
fs.writeFileSync(safeOutput, `${JSON.stringify(summary, null, 2)}\n`);
|
|
1554
|
-
console.log(chalk.green('✅ manifest summary'), '→', safeOutput);
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
console.log(chalk.blue('\n📘 Manifest summary for'), safeManifestPath);
|
|
1559
|
-
console.log(` Total: ${summary.totalDiagrams}`);
|
|
1560
|
-
console.log(` Placeholder: ${summary.placeholders}`);
|
|
1561
|
-
if (summary.missingTypes.length > 0) {
|
|
1562
|
-
console.log(chalk.yellow(` Missing expected (all supported): ${summary.missingTypes.join(', ')}`));
|
|
1563
|
-
}
|
|
1564
|
-
if (summary.placeholderTypes.length > 0) {
|
|
1565
|
-
console.log(chalk.yellow(` Placeholder types: ${summary.placeholderTypes.join(', ')}`));
|
|
1566
|
-
}
|
|
1567
|
-
console.log('');
|
|
1568
|
-
for (const entry of summary.diagrams) {
|
|
1569
|
-
const status = entry.isPlaceholder ? chalk.yellow('placeholder') : chalk.green('ok');
|
|
1570
|
-
console.log(` ${status} ${entry.type} -> ${entry.file}`);
|
|
1571
|
-
}
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
program
|
|
1575
|
-
.command('video [path]')
|
|
1576
|
-
.description('Generate an animated video of the diagram')
|
|
1577
|
-
.option('-t, --type <type>', 'Diagram type', 'architecture')
|
|
1578
|
-
.option('-o, --output <file>', 'Output file (.mp4, .webm, .mov)', 'diagram.mp4')
|
|
1579
|
-
.option('-d, --duration <sec>', 'Video duration in seconds', '5')
|
|
1580
|
-
.option('-f, --fps <n>', 'Frames per second', '30')
|
|
1581
|
-
.option('--width <n>', 'Video width', '1280')
|
|
1582
|
-
.option('--height <n>', 'Video height', '720')
|
|
1583
|
-
.option('--theme <theme>', 'Theme: default, dark, forest, neutral', 'dark')
|
|
1584
|
-
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
1585
|
-
.action(async (targetPath, options) => {
|
|
1586
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1587
|
-
const safeTheme = normalizeThemeOption(options.theme, 'dark');
|
|
1588
|
-
|
|
1589
|
-
// Validate output path
|
|
1590
|
-
let safeOutput;
|
|
1591
|
-
try {
|
|
1592
|
-
safeOutput = validateOutputPath(options.output, root);
|
|
1593
|
-
} catch (err) {
|
|
1594
|
-
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1595
|
-
process.exit(2);
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
const outputDir = path.dirname(safeOutput);
|
|
1599
|
-
if (!fs.existsSync(outputDir)) {
|
|
1600
|
-
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
console.log(chalk.blue('🎬 Generating video for'), root);
|
|
1604
|
-
|
|
1605
|
-
const data = await analyze(root, options);
|
|
1606
|
-
const mermaid = generate(data, options.type);
|
|
1607
|
-
|
|
1608
|
-
const { generateVideo } = getVideoModule();
|
|
1609
|
-
|
|
1610
|
-
await generateVideo(mermaid, safeOutput, {
|
|
1611
|
-
duration: parseInt(options.duration) || 5,
|
|
1612
|
-
fps: parseInt(options.fps) || 30,
|
|
1613
|
-
width: parseInt(options.width) || 1280,
|
|
1614
|
-
height: parseInt(options.height) || 720,
|
|
1615
|
-
theme: safeTheme
|
|
1616
|
-
});
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
program
|
|
1620
|
-
.command('animate [path]')
|
|
1621
|
-
.description('Generate animated SVG with CSS animations')
|
|
1622
|
-
.option('-t, --type <type>', 'Diagram type', 'architecture')
|
|
1623
|
-
.option('-o, --output <file>', 'Output file', 'diagram-animated.svg')
|
|
1624
|
-
.option('--theme <theme>', 'Theme', 'dark')
|
|
1625
|
-
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
1626
|
-
.action(async (targetPath, options) => {
|
|
1627
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1628
|
-
const safeTheme = normalizeThemeOption(options.theme, 'dark');
|
|
1629
|
-
|
|
1630
|
-
// Validate output path
|
|
1631
|
-
let safeOutput;
|
|
1632
|
-
try {
|
|
1633
|
-
safeOutput = validateOutputPath(options.output, root);
|
|
1634
|
-
} catch (err) {
|
|
1635
|
-
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1636
|
-
process.exit(2);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
const outputDir = path.dirname(safeOutput);
|
|
1640
|
-
if (!fs.existsSync(outputDir)) {
|
|
1641
|
-
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
console.log(chalk.blue('✨ Generating animated SVG for'), root);
|
|
1645
|
-
|
|
1646
|
-
const data = await analyze(root, options);
|
|
1647
|
-
const mermaid = generate(data, options.type);
|
|
1648
|
-
|
|
1649
|
-
const { generateAnimatedSVG } = getVideoModule();
|
|
1650
|
-
|
|
1651
|
-
await generateAnimatedSVG(mermaid, safeOutput, {
|
|
1652
|
-
theme: safeTheme
|
|
1653
|
-
});
|
|
1654
|
-
});
|
|
1655
|
-
|
|
1656
|
-
program
|
|
1657
|
-
.command('test [path]')
|
|
1658
|
-
.description('Validate architecture against .architecture.yml rules')
|
|
1659
|
-
.option('-c, --config <file>', 'Config file path', '.architecture.yml')
|
|
1660
|
-
.option('-f, --format <format>', 'Output format: console, json, junit', 'console')
|
|
1661
|
-
.option('-o, --output <file>', 'Output file (for json/junit formats)')
|
|
1662
|
-
.option('-p, --patterns <list>', 'File patterns', '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs')
|
|
1663
|
-
.option('-e, --exclude <list>', 'Exclude patterns', 'node_modules/**,.git/**,dist/**')
|
|
1664
|
-
.option('-m, --max-files <n>', 'Max files to analyze', '100')
|
|
1665
|
-
.option('--dry-run', 'Preview file matching without validation', false)
|
|
1666
|
-
.option('--verbose', 'Show detailed output', false)
|
|
1667
|
-
.option('--init', 'Generate starter configuration file', false)
|
|
1668
|
-
.option('--force', 'Overwrite existing configuration with --init', false)
|
|
1669
|
-
.action(async (targetPath, options) => {
|
|
1670
|
-
const { RulesEngine } = require('./rules');
|
|
1671
|
-
const { ComponentGraph } = require('./graph');
|
|
1672
|
-
const { RuleFactory } = require('./rules/factory');
|
|
1673
|
-
const { formatResults } = require('./formatters/index');
|
|
1674
|
-
const { validateConfig, getDefaultConfig } = require('./schema/rules-schema');
|
|
1675
|
-
const YAML = require('yaml');
|
|
1676
|
-
|
|
1677
|
-
const root = resolveRootPathOrExit(targetPath);
|
|
1678
|
-
const engine = new RulesEngine();
|
|
1679
|
-
const startTime = Date.now();
|
|
1680
|
-
const outputsMachineFormat =
|
|
1681
|
-
!options.output && (options.format === 'json' || options.format === 'junit');
|
|
1682
|
-
const quietMachineOutput = outputsMachineFormat && !options.verbose;
|
|
1683
|
-
|
|
1684
|
-
// Init mode - generate starter config
|
|
1685
|
-
if (options.init) {
|
|
1686
|
-
const configPath = path.join(root, '.architecture.yml');
|
|
1687
|
-
|
|
1688
|
-
if (fs.existsSync(configPath) && !options.force) {
|
|
1689
|
-
console.error(chalk.yellow('⚠️ Configuration already exists:'), configPath);
|
|
1690
|
-
console.log(chalk.gray(' Use --force to overwrite'));
|
|
1691
|
-
process.exit(2);
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
const defaultConfig = getDefaultConfig();
|
|
1695
|
-
const yaml = YAML.stringify(defaultConfig, {
|
|
1696
|
-
indent: 2,
|
|
1697
|
-
lineWidth: 0
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
fs.writeFileSync(configPath, yaml);
|
|
1701
|
-
console.log(chalk.green('✅ Created configuration:'), configPath);
|
|
1702
|
-
console.log(chalk.gray('\nEdit the file to define your architecture rules, then run:'));
|
|
1703
|
-
console.log(chalk.cyan(' diagram test'));
|
|
1704
|
-
process.exit(0);
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// Find or use specified config
|
|
1708
|
-
let configPath = options.config;
|
|
1709
|
-
if (!path.isAbsolute(configPath)) {
|
|
1710
|
-
configPath = path.join(root, configPath);
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
// Validate config path is within project root (security check)
|
|
1714
|
-
const relativeConfigPath = path.relative(root, configPath);
|
|
1715
|
-
if (relativeConfigPath.startsWith('..') || path.isAbsolute(relativeConfigPath)) {
|
|
1716
|
-
console.error(chalk.red('❌ Invalid config path: directory traversal detected'));
|
|
1717
|
-
process.exit(2);
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
if (!fs.existsSync(configPath)) {
|
|
1721
|
-
// Try to find config in root
|
|
1722
|
-
const found = engine.findConfig(root);
|
|
1723
|
-
if (!found) {
|
|
1724
|
-
console.error(chalk.red('❌ No .architecture.yml found. Run: diagram test --init'));
|
|
1725
|
-
process.exit(2);
|
|
1726
|
-
}
|
|
1727
|
-
configPath = found;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// Load config
|
|
1731
|
-
let config;
|
|
1732
|
-
try {
|
|
1733
|
-
config = engine.loadConfig(configPath);
|
|
1734
|
-
} catch (error) {
|
|
1735
|
-
console.error(chalk.red('❌ Config error:'), error.message);
|
|
1736
|
-
process.exit(2);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
// Validate config against schema
|
|
1740
|
-
const validation = validateConfig(config);
|
|
1741
|
-
if (!validation.valid) {
|
|
1742
|
-
console.error(chalk.red('❌ Schema validation failed:'));
|
|
1743
|
-
for (const err of validation.errors) {
|
|
1744
|
-
console.error(chalk.red(` • ${err.path}: ${err.message}`));
|
|
1745
|
-
}
|
|
1746
|
-
process.exit(2);
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
// Analyze codebase
|
|
1750
|
-
if (!quietMachineOutput) {
|
|
1751
|
-
console.log(chalk.blue('🔍 Analyzing'), root);
|
|
1752
|
-
}
|
|
1753
|
-
const data = await analyze(root, options);
|
|
1754
|
-
const graph = new ComponentGraph(data);
|
|
1755
|
-
|
|
1756
|
-
// Create rules
|
|
1757
|
-
let rules;
|
|
1758
|
-
try {
|
|
1759
|
-
rules = RuleFactory.createRules(config);
|
|
1760
|
-
} catch (error) {
|
|
1761
|
-
console.error(chalk.red('❌ Rule error:'), error.message);
|
|
1762
|
-
process.exit(2);
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
// Dry run mode - just show file matching
|
|
1766
|
-
if (options.dryRun) {
|
|
1767
|
-
const preview = engine.previewMatches(rules, graph);
|
|
1768
|
-
console.log(chalk.cyan('\n📋 Dry Run - File Matching Preview\n'));
|
|
1769
|
-
for (const rule of preview.rules) {
|
|
1770
|
-
console.log(chalk.bold(rule.name));
|
|
1771
|
-
console.log(' Layer:', chalk.gray(Array.isArray(rule.layer) ? rule.layer.join(', ') : rule.layer));
|
|
1772
|
-
console.log(' Matched files:', rule.matchedFiles.length);
|
|
1773
|
-
if (options.verbose) {
|
|
1774
|
-
for (const file of rule.matchedFiles) {
|
|
1775
|
-
console.log(' -', file);
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
console.log();
|
|
1779
|
-
}
|
|
1780
|
-
process.exit(0);
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
// Run validation
|
|
1784
|
-
if (!quietMachineOutput) {
|
|
1785
|
-
console.log(chalk.blue('🧪 Validating'), rules.length, 'rules...\n');
|
|
1786
|
-
}
|
|
1787
|
-
const results = engine.validate(rules, graph);
|
|
1788
|
-
|
|
1789
|
-
// Validate output path if specified
|
|
1790
|
-
let safeOutput = options.output;
|
|
1791
|
-
if (safeOutput) {
|
|
1792
|
-
try {
|
|
1793
|
-
safeOutput = validateOutputPath(safeOutput, root);
|
|
1794
|
-
} catch (err) {
|
|
1795
|
-
console.error(chalk.red('❌ Output path error:'), err.message);
|
|
1796
|
-
process.exit(2);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
// Output results
|
|
1801
|
-
const exitCode = formatResults(results, options.format, {
|
|
1802
|
-
output: safeOutput,
|
|
1803
|
-
verbose: options.verbose
|
|
1804
|
-
}, startTime);
|
|
1805
|
-
|
|
1806
|
-
process.exit(exitCode);
|
|
1807
|
-
});
|
|
1808
|
-
|
|
1809
|
-
program.parse();
|