@brainwav/diagram 1.0.7 → 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.
Files changed (91) hide show
  1. package/.diagram/contracts/machine-command-coverage.json +73 -0
  2. package/.diagram/migration/finalization-policy.json +20 -0
  3. package/LICENSE +202 -21
  4. package/README.md +132 -339
  5. package/package.json +46 -13
  6. package/scripts/refresh-diagram-context.sh +274 -182
  7. package/src/analyzers/default-analyzer.js +11 -0
  8. package/src/analyzers/index.js +34 -0
  9. package/src/artifacts/agent-context.js +105 -0
  10. package/src/artifacts/artifact-budget.js +224 -0
  11. package/src/artifacts/brief.js +153 -0
  12. package/src/artifacts/evidence-manifest.js +206 -0
  13. package/src/artifacts/evidence-summary.js +29 -0
  14. package/src/commands/analyze.js +125 -0
  15. package/src/commands/changed.js +185 -0
  16. package/src/commands/context.js +110 -0
  17. package/src/commands/diff.js +142 -0
  18. package/src/commands/doctor.js +335 -0
  19. package/src/commands/explain.js +273 -0
  20. package/src/commands/generate-all.js +170 -0
  21. package/src/commands/generate-animated.js +50 -0
  22. package/src/commands/generate-video.js +65 -0
  23. package/src/commands/generate.js +522 -0
  24. package/src/commands/init.js +123 -0
  25. package/src/commands/output.js +76 -0
  26. package/src/commands/scan.js +624 -0
  27. package/src/commands/shared.js +396 -0
  28. package/src/commands/validate.js +328 -0
  29. package/src/commands/video-shared.js +105 -0
  30. package/src/commands/workflow-pr.js +26 -0
  31. package/src/confidence/pipeline.js +186 -0
  32. package/src/config/diagramrc.js +79 -0
  33. package/src/context/build-context-pack.js +291 -0
  34. package/src/context/normalize-diagram-manifest.js +282 -0
  35. package/src/core/analysis-generation-analyze-components.js +102 -0
  36. package/src/core/analysis-generation-analyze-dependencies.js +33 -0
  37. package/src/core/analysis-generation-analyze-files.js +48 -0
  38. package/src/core/analysis-generation-analyze-options.js +73 -0
  39. package/src/core/analysis-generation-analyze.js +63 -0
  40. package/src/core/analysis-generation-constants.js +53 -0
  41. package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
  42. package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
  43. package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
  44. package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
  45. package/src/core/analysis-generation-diagrams-core.js +12 -0
  46. package/src/core/analysis-generation-diagrams-empty.js +68 -0
  47. package/src/core/analysis-generation-diagrams-erd.js +59 -0
  48. package/src/core/analysis-generation-diagrams-limit.js +27 -0
  49. package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
  50. package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
  51. package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
  52. package/src/core/analysis-generation-diagrams-role-data.js +182 -0
  53. package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
  54. package/src/core/analysis-generation-diagrams-role-security.js +129 -0
  55. package/src/core/analysis-generation-diagrams-role.js +25 -0
  56. package/src/core/analysis-generation-diagrams.js +182 -0
  57. package/src/core/analysis-generation-role-tags-constants.js +55 -0
  58. package/src/core/analysis-generation-role-tags-imports.js +32 -0
  59. package/src/core/analysis-generation-role-tags-infer.js +49 -0
  60. package/src/core/analysis-generation-role-tags-match.js +19 -0
  61. package/src/core/analysis-generation-role-tags.js +7 -0
  62. package/src/core/analysis-generation-utils-core.js +308 -0
  63. package/src/core/analysis-generation-utils-graph.js +321 -0
  64. package/src/core/analysis-generation-utils-resolution.js +76 -0
  65. package/src/core/analysis-generation-utils.js +9 -0
  66. package/src/core/analysis-generation.js +44 -0
  67. package/src/diagram.js +180 -1760
  68. package/src/formatters/console.js +198 -0
  69. package/src/formatters/index.js +41 -0
  70. package/src/formatters/json.js +113 -0
  71. package/src/formatters/junit.js +123 -0
  72. package/src/graph.js +159 -0
  73. package/src/incremental/cache.js +210 -0
  74. package/src/ir/architecture-ir.js +48 -0
  75. package/src/migration/evidence.js +262 -0
  76. package/src/migration/finalization-policy.js +35 -0
  77. package/src/renderers/report-html.js +265 -0
  78. package/src/rules/factory.js +108 -0
  79. package/src/rules/types/base.js +54 -0
  80. package/src/rules/types/import-rule.js +286 -0
  81. package/src/rules.js +380 -0
  82. package/src/schema/erd-confidence.js +56 -0
  83. package/src/schema/erd-extractor.js +504 -0
  84. package/src/schema/erd-model.js +176 -0
  85. package/src/schema/rules-schema.js +170 -0
  86. package/src/utils/suggestions.js +67 -0
  87. package/src/video.js +4 -5
  88. package/src/workflow/git-helpers.js +576 -0
  89. package/src/workflow/pr-command.js +694 -0
  90. package/src/workflow/pr-impact.js +848 -0
  91. package/src/workflow/sort-utils.js +16 -0
package/src/diagram.js CHANGED
@@ -1,1806 +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 { spawn, execFileSync } = require('child_process');
9
- const os = require('os');
10
- const crypto = require('crypto');
11
- const zlib = require('zlib');
12
- const { getOpenCommand, getNpxCommandCandidates } = require('./utils/commands');
13
-
14
- // Video generation (lazy loaded)
15
- let videoModule;
16
- function getVideoModule() {
17
- if (!videoModule) {
18
- try {
19
- videoModule = require('./video.js');
20
- } catch (e) {
21
- console.error(chalk.red('❌ Video generation requires Playwright. Install with: npm install playwright'));
22
- process.exit(1);
23
- }
24
- }
25
- return videoModule;
26
- }
5
+ const path = require('path');
6
+ const packageJson = require('../package.json');
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.`;
27
34
 
28
35
  const program = new Command();
29
36
 
30
- // Utility functions
31
- function detectLanguage(filePath) {
32
- if (typeof filePath !== 'string') return 'unknown';
33
- const ext = path.extname(filePath).toLowerCase();
34
- const map = {
35
- '.ts': 'typescript', '.tsx': 'typescript',
36
- '.mts': 'typescript', '.cts': 'typescript',
37
- '.js': 'javascript', '.jsx': 'javascript',
38
- '.mjs': 'javascript', '.cjs': 'javascript',
39
- '.py': 'python', '.go': 'go', '.rs': 'rust',
40
- '.java': 'java', '.rb': 'ruby', '.php': 'php',
41
- };
42
- return map[ext] || 'unknown';
43
- }
44
-
45
- function inferType(filePath, content) {
46
- const base = path.basename(filePath).toLowerCase();
47
- if (base.includes('service')) return 'service';
48
- if (base.includes('component') || base.endsWith('.tsx') || base.endsWith('.jsx')) return 'component';
49
- if (content.includes('class ') && content.includes('extends')) return 'class';
50
- if (content.includes('export default function') || content.includes('export function')) return 'function';
51
- if (content.includes('module.exports') || content.includes('export ')) return 'module';
52
- return 'file';
53
- }
54
-
55
- function extractImports(content, lang) {
56
- const imports = [];
57
- if (lang === 'typescript' || lang === 'javascript') {
58
- // ES6 imports with timeout protection against ReDoS
59
- const es6Regex = /import\s+(?:(?:\{[^}]*?\}|\*\s+as\s+\w+|\w+)\s+from\s+)?["']([^"']+)["']/g;
60
- const es6 = [...content.matchAll(es6Regex)];
61
- es6.forEach(m => imports.push(m[1]));
62
- // CommonJS requires
63
- const cjs = [...content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)];
64
- cjs.forEach(m => imports.push(m[1]));
65
- // Dynamic imports
66
- const dynamic = [...content.matchAll(/import\s*\(\s*["']([^"']+)["']\s*\)/g)];
67
- dynamic.forEach(m => imports.push(m[1]));
68
- } else if (lang === 'python') {
69
- const py = [...content.matchAll(/(?:from|import)\s+([\w.]+)/g)];
70
- py.forEach(m => imports.push(m[1]));
71
- } else if (lang === 'go') {
72
- const go = [...content.matchAll(/import\s+(?:\(\s*)?["']([^"']+)["']/g)];
73
- go.forEach(m => imports.push(m[1]));
74
- }
75
- return imports;
76
- }
77
-
78
- /**
79
- * Extract imports with line number information
80
- * @param {string} content - File content
81
- * @param {string} lang - Language
82
- * @returns {Array<{path: string, line: number}>}
83
- */
84
- function extractImportsWithPositions(content, lang) {
85
- const imports = [];
86
- const lines = content.split(/\r?\n/);
87
-
88
- for (let i = 0; i < lines.length; i++) {
89
- const line = lines[i];
90
- const lineNum = i + 1;
91
-
92
- if (lang === 'typescript' || lang === 'javascript') {
93
- // ES6 imports
94
- const es6 = line.match(/import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?["']([^"']+)["']/);
95
- if (es6) {
96
- imports.push({ path: es6[1], line: lineNum });
97
- continue;
98
- }
99
-
100
- // CommonJS requires
101
- const cjs = line.match(/require\s*\(\s*["']([^"']+)["']\s*\)/);
102
- if (cjs) {
103
- imports.push({ path: cjs[1], line: lineNum });
104
- continue;
105
- }
106
-
107
- // Dynamic imports
108
- const dynamic = line.match(/import\s*\(\s*["']([^"']+)["']\s*\)/);
109
- if (dynamic) {
110
- imports.push({ path: dynamic[1], line: lineNum });
111
- }
112
- } else if (lang === 'python') {
113
- const py = line.match(/(?:from|import)\s+([\w.]+)/);
114
- if (py) {
115
- imports.push({ path: py[1], line: lineNum });
116
- }
117
- } else if (lang === 'go') {
118
- const go = line.match(/import\s+(?:\(\s*)?["']([^"']+)["']/);
119
- if (go) {
120
- imports.push({ path: go[1], line: lineNum });
121
- }
122
- }
123
- }
124
-
125
- return imports;
126
- }
127
-
128
- function sanitize(name) {
129
- // Ensure unique, valid mermaid ID
130
- const base = name.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[0-9]/, '_$&');
131
- // Add hash suffix to prevent collisions (using SHA-256)
132
- const hash = crypto.createHash('sha256').update(name).digest('hex').slice(0, 8);
133
- return `${base}_${hash}`;
134
- }
135
-
136
- function escapeMermaid(str) {
137
- if (!str) return '';
138
- return str
139
- .replace(/"/g, '\\"')
140
- .replace(/\[/g, '\\[')
141
- .replace(/\]/g, '\\]')
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
- }
151
-
152
- function normalizePath(inputPath) {
153
- // Always use forward slashes for consistency
154
- return inputPath.replace(/\\/g, '/');
155
- }
156
-
157
- const IMPORT_RESOLUTION_SUFFIXES = [
158
- '',
159
- '.ts',
160
- '.tsx',
161
- '.js',
162
- '.jsx',
163
- '.mjs',
164
- '.mts',
165
- '.cts',
166
- '/index.ts',
167
- '/index.tsx',
168
- '/index.js',
169
- '/index.jsx',
170
- '/index.mjs',
171
- '/index.mts',
172
- '/index.cts'
173
- ];
174
-
175
- function toComparablePath(p) {
176
- return normalizePath(String(p || '')).replace(/^\.\//, '');
177
- }
178
-
179
- function getImportPath(importInfo) {
180
- if (typeof importInfo === 'string') return importInfo;
181
- if (importInfo && typeof importInfo.path === 'string') return importInfo.path;
182
- return null;
183
- }
184
-
185
- function resolveInternalImport(fromFilePath, importPath, rootPath) {
186
- if (typeof fromFilePath !== 'string' || typeof importPath !== 'string') {
187
- return null;
188
- }
189
- if (!importPath.startsWith('.')) {
190
- return null;
191
- }
192
-
193
- const fromDir = path.dirname(fromFilePath);
194
-
195
- // In analysis mode we can enforce root boundaries with absolute paths
196
- if (rootPath) {
197
- const absoluteTarget = path.resolve(rootPath, fromDir, importPath);
198
- const relativeToRoot = toComparablePath(path.relative(rootPath, absoluteTarget));
199
- if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
200
- return null;
37
+ program
38
+ .name(CANONICAL_COMMAND_NAME)
39
+ .description('Generate architecture evidence for humans and AI agents')
40
+ .version(packageJson.version);
41
+
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;
201
91
  }
202
- return relativeToRoot;
203
92
  }
204
-
205
- // Fallback for precomputed data without root path
206
- const posixFromDir = normalizePath(fromDir);
207
- const posixImport = normalizePath(importPath);
208
- return toComparablePath(path.posix.normalize(path.posix.join(posixFromDir, posixImport)));
93
+ return path.basename(argv[1] || '');
209
94
  }
210
95
 
211
- function findComponentByResolvedPath(components, resolvedPath) {
212
- const comparablePath = toComparablePath(resolvedPath);
213
- const candidates = new Set(
214
- IMPORT_RESOLUTION_SUFFIXES.map(suffix => toComparablePath(comparablePath + suffix))
215
- );
216
- return components.find(c => candidates.has(toComparablePath(c.filePath)));
96
+ function isCompatibilityInvocation(argv, env = process.env) {
97
+ return getInvocationName(argv, env) === COMPATIBILITY_COMMAND_NAME;
217
98
  }
218
99
 
219
- function getExternalPackageName(importPath) {
220
- if (typeof importPath !== 'string') return null;
221
- if (!importPath) return null;
222
- if (importPath.startsWith('@')) {
223
- const [scope, pkg] = importPath.split('/');
224
- 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));
225
103
  }
226
- return importPath.split('/')[0] || null;
227
104
  }
228
105
 
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;
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);
288
126
  }
289
- const externalPackage = getExternalPackageName(importPath);
290
- if (externalPackage) {
291
- packages.add(externalPackage);
127
+ for (const subcommand of command.commands || []) {
128
+ stack.push(subcommand);
292
129
  }
293
130
  }
294
131
 
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
- }
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;
313
138
  }
314
139
  }
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));
140
+ return null;
334
141
  }
335
142
 
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
- }
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);
344
155
 
345
- function mapSafeNames(components) {
346
- const map = new Map();
347
- const used = new Set();
156
+ for (let i = 0; i < argv.length; i += 1) {
157
+ const arg = argv[i];
348
158
 
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);
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');
354
162
  continue;
355
163
  }
356
164
 
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
-
454
- // Analysis
455
- async function analyze(rootPath, options) {
456
- // Validate maxFiles with strict parsing
457
- let maxFiles = parseInt(options.maxFiles, 10);
458
- if (isNaN(maxFiles) || maxFiles < 1 || maxFiles > 10000) {
459
- maxFiles = 100;
460
- }
461
- // Extra safety: ensure within safe bounds
462
- maxFiles = Math.min(Math.max(maxFiles, 1), 10000);
463
-
464
- // Validate patterns type
465
- let patterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'];
466
- if (options.patterns) {
467
- if (typeof options.patterns !== 'string') {
468
- throw new TypeError('patterns must be a string');
469
- }
470
- patterns = options.patterns.split(',');
471
- }
472
-
473
- let exclude = ['node_modules/**', '.git/**', 'dist/**', 'build/**', '*.test.*', '*.spec.*'];
474
- if (options.exclude) {
475
- if (typeof options.exclude !== 'string') {
476
- throw new TypeError('exclude must be a string');
477
- }
478
- exclude = options.exclude.split(',');
479
- }
480
-
481
- const files = [];
482
- for (const pattern of patterns) {
483
- if (!pattern || pattern.trim() === '') continue;
484
- try {
485
- const matches = await glob(pattern.trim(), { cwd: rootPath, absolute: true, ignore: exclude });
486
- files.push(...matches);
487
- } catch (e) {
488
- 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;
489
169
  }
490
- }
491
-
492
- const uniqueFiles = [...new Set(files)].slice(0, maxFiles);
493
- const components = [];
494
- const languages = {};
495
- const directories = new Set();
496
- const entryPoints = [];
497
- const seenNames = new Set();
498
170
 
499
- for (const filePath of uniqueFiles) {
500
- try {
501
- // Security: Check file size before reading
502
- const stats = fs.statSync(filePath);
503
- if (stats.size > 10 * 1024 * 1024) { // 10MB limit
504
- 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;
505
176
  continue;
506
177
  }
507
- const content = fs.readFileSync(filePath, 'utf-8');
508
- const lang = detectLanguage(filePath);
509
- let rel = normalizePath(path.relative(rootPath, filePath));
510
- const dir = path.dirname(rel);
511
- if (dir === '.') {
512
- rel = './' + rel;
513
- }
514
-
515
- languages[lang] = (languages[lang] || 0) + 1;
516
- if (dir !== '.') directories.add(dir);
517
-
518
- // Support more entry point patterns (with escaped regex)
519
- const entryPattern = /\/(index|main|app|server)\.(ts|js|tsx|jsx|mts|mjs|py|go|rs)$/i;
520
- if (entryPattern.test(rel)) {
521
- entryPoints.push(rel);
522
- }
523
-
524
- // Handle duplicate names
525
- let baseName = path.basename(filePath, path.extname(filePath));
526
- let uniqueName = baseName;
527
- let counter = 1;
528
- while (seenNames.has(uniqueName)) {
529
- uniqueName = `${baseName}_${counter}`;
530
- counter++;
531
- }
532
- seenNames.add(uniqueName);
533
-
534
- const imports = extractImportsWithPositions(content, lang);
535
- const type = inferType(filePath, content);
536
-
537
- components.push({
538
- name: uniqueName,
539
- originalName: baseName,
540
- filePath: rel,
541
- type,
542
- imports,
543
- roleTags: inferRoleTags(rel, baseName, content, imports, type),
544
- directory: dir,
545
- });
546
- } catch (e) {
547
- if (process.env.DEBUG) {
548
- // Sanitize path to avoid info disclosure - show only basename
549
- const safePath = path.basename(filePath);
550
- console.error(chalk.gray(`Skipped ${safePath}: ${e.message}`));
551
- }
552
- }
553
- }
554
-
555
- // Resolve dependencies
556
- for (const comp of components) {
557
- comp.dependencies = [];
558
- for (const imp of comp.imports) {
559
- const importPath = getImportPath(imp);
560
- if (!importPath) continue;
561
- const resolved = resolveInternalImport(comp.filePath, importPath, rootPath);
562
- if (!resolved) continue;
563
- const dep = findComponentByResolvedPath(components, resolved);
564
- if (dep) comp.dependencies.push(dep.name);
565
- }
566
- }
567
-
568
- return { rootPath, components, entryPoints, languages, directories: [...directories].sort() };
569
- }
570
-
571
- // Diagram generators
572
- function generateArchitecture(data, focus) {
573
- if (!data || !Array.isArray(data.components)) {
574
- return 'graph TD\n Note["No data available"]';
575
- }
576
-
577
- const lines = ['graph TD'];
578
- const focusNorm = focus ? normalizePath(focus) : null;
579
- // Use exact path matching for focus
580
- const comps = focusNorm
581
- ? data.components.filter(c => {
582
- const normalizedFilePath = normalizePath(c.filePath || '');
583
- const normalizedName = c.name || '';
584
- // Check if focus is at path boundary
585
- return normalizedFilePath === focusNorm ||
586
- normalizedFilePath.startsWith(focusNorm + '/') ||
587
- normalizedName === focusNorm;
588
- })
589
- : data.components;
590
-
591
- if (comps.length === 0) {
592
- lines.push(' Note["No components found' + (focus ? ' for focus: ' + escapeMermaid(focus) : '') + '"]');
593
- return lines.join('\n');
594
- }
595
-
596
- const byDir = new Map();
597
- for (const c of comps) {
598
- const dir = c.directory || 'root';
599
- if (!byDir.has(dir)) byDir.set(dir, []);
600
- byDir.get(dir).push(c);
601
- }
602
-
603
- for (const [dir, items] of byDir) {
604
- if (items.length === 0) continue;
605
- lines.push(` subgraph ${sanitize(dir)}["${escapeMermaid(dir)}"]`);
606
- for (const c of items) {
607
- const shape = c.type === 'service' ? '[[' : '[';
608
- const end = c.type === 'service' ? ']]' : ']';
609
- lines.push(` ${sanitize(c.name)}${shape}"${escapeMermaid(c.originalName)}"${end}`);
610
- }
611
- lines.push(' end');
612
- }
613
-
614
- for (const c of comps) {
615
- for (const d of c.dependencies) {
616
- if (comps.find(x => x.name === d)) {
617
- lines.push(` ${sanitize(c.name)} --> ${sanitize(d)}`);
618
- }
619
- }
620
- }
621
-
622
- // Track styled nodes to avoid duplicates
623
- const styledNodes = new Set();
624
- for (const ep of data.entryPoints) {
625
- const epName = path.basename(ep, path.extname(ep));
626
- const comp = comps.find(c => c.originalName === epName);
627
- if (comp && !styledNodes.has(comp.name)) {
628
- lines.push(` style ${sanitize(comp.name)} fill:#4f46e5,color:#fff`);
629
- styledNodes.add(comp.name);
630
- }
631
- }
632
-
633
- return lines.join('\n');
634
- }
635
-
636
- function generateSequence(data) {
637
- if (!data || !Array.isArray(data.components)) {
638
- return 'sequenceDiagram\n Note over User,App: No data available';
639
- }
640
-
641
- const lines = ['sequenceDiagram'];
642
- // Use configurable limit with warning
643
- const MAX_SERVICES = 6;
644
- const services = data.components.filter(c => c.type === 'service' || c.name === 'index').slice(0, MAX_SERVICES);
645
- if (data.components.length > MAX_SERVICES) {
646
- console.warn(chalk.yellow(`⚠️ Sequence diagram limited to ${MAX_SERVICES} services`));
647
- }
648
-
649
- if (services.length === 0) {
650
- lines.push(' Note over User,App: No services detected');
651
- return lines.join('\n');
652
- }
653
-
654
- // Track used sanitized names to prevent collisions
655
- const usedNames = new Map();
656
- const getSafeName = (service) => {
657
- const base = sanitize(service.name);
658
- if (!usedNames.has(base)) {
659
- usedNames.set(base, service.name);
660
- return base;
661
- }
662
- // Collision - append number
663
- let i = 1;
664
- let newName = `${base}_${i}`;
665
- while (usedNames.has(newName)) {
666
- i++;
667
- newName = `${base}_${i}`;
668
- }
669
- usedNames.set(newName, service.name);
670
- return newName;
671
- };
672
-
673
- const safeNames = services.map(getSafeName);
674
-
675
- for (let i = 0; i < services.length; i++) {
676
- lines.push(` participant ${safeNames[i]} as ${escapeMermaid(services[i].originalName)}`);
677
- }
678
-
679
- for (let i = 0; i < services.length - 1; i++) {
680
- lines.push(` ${safeNames[i]}->>${safeNames[i+1]}: calls`);
681
- }
682
- return lines.join('\n');
683
- }
684
-
685
- function generateDependency(data, focus) {
686
- if (!data || !Array.isArray(data.components)) {
687
- return 'graph LR\n Note["No data available"]';
688
- }
689
-
690
- const lines = ['graph LR'];
691
- const focusNorm = focus ? normalizePath(focus) : null;
692
- const comps = focusNorm ? data.components.filter(c => {
693
- const normalizedPath = normalizePath(c.filePath || '');
694
- return normalizedPath === focusNorm || normalizedPath.startsWith(focusNorm + '/');
695
- }) : data.components;
696
-
697
- if (comps.length === 0) {
698
- lines.push(' Note["No components found"]');
699
- return lines.join('\n');
700
- }
701
-
702
- const external = new Set();
703
-
704
- for (const c of comps) {
705
- const imports = Array.isArray(c.imports) ? c.imports : [];
706
- for (const importInfo of imports) {
707
- const importPath = getImportPath(importInfo);
708
- if (!importPath) continue;
709
- if (!importPath.startsWith('.')) {
710
- const pkg = getExternalPackageName(importPath);
711
- if (pkg) {
712
- external.add(pkg);
713
- lines.push(` ${sanitize(pkg)}["${escapeMermaid(pkg)}"] --> ${sanitize(c.name)}`);
714
- }
715
- } else {
716
- const basePath = resolveInternalImport(c.filePath, importPath, data.rootPath);
717
- if (!basePath) continue;
718
- const resolved = findComponentByResolvedPath(comps, basePath);
719
- if (resolved) lines.push(` ${sanitize(c.name)} --> ${sanitize(resolved.name)}`);
720
- }
721
- }
722
- }
723
-
724
- for (const e of external) {
725
- lines.push(` style ${sanitize(e)} fill:#f59e0b,color:#fff`);
726
- }
727
- return lines.join('\n');
728
- }
729
-
730
- function generateClass(data) {
731
- if (!data || !Array.isArray(data.components)) {
732
- return 'classDiagram\n note "No data available"';
733
- }
734
-
735
- const lines = ['classDiagram'];
736
- const MAX_CLASSES = 20;
737
- const classes = data.components.filter(c => c.type === 'class' || c.type === 'component').slice(0, MAX_CLASSES);
738
- if (data.components.length > MAX_CLASSES) {
739
- console.warn(chalk.yellow(`⚠️ Class diagram limited to ${MAX_CLASSES} classes`));
740
- }
741
-
742
- if (classes.length === 0) {
743
- lines.push(' note "No classes found"');
744
- return lines.join('\n');
745
- }
746
-
747
- for (const c of classes) {
748
- lines.push(` class ${sanitize(c.name)} {`);
749
- lines.push(` +${escapeMermaid(c.filePath)}`);
750
- lines.push(' }');
751
- }
752
-
753
- for (const c of classes) {
754
- const deps = (c.dependencies || []).slice(0, 3);
755
- for (const d of deps) {
756
- if (classes.find(x => x.name === d)) {
757
- lines.push(` ${sanitize(c.name)} --> ${sanitize(d)}`);
758
- }
759
- }
760
- }
761
- return lines.join('\n');
762
- }
763
-
764
- function generateFlow(data) {
765
- if (!data || !Array.isArray(data.components)) {
766
- return 'flowchart TD\n Start(["Start"])\n End(["End"])\n Start --> End';
767
- }
768
-
769
- const lines = ['flowchart TD'];
770
- lines.push(' Start(["Start"])');
771
- const MAX_COMPONENTS = 8;
772
- const comps = data.components.slice(0, MAX_COMPONENTS);
773
- if (data.components.length > MAX_COMPONENTS) {
774
- console.warn(chalk.yellow(`⚠️ Flow diagram limited to ${MAX_COMPONENTS} components`));
775
- }
776
-
777
- if (comps.length === 0) {
778
- lines.push(' End(["End"])');
779
- lines.push(' Start --> End');
780
- return lines.join('\n');
781
- }
782
-
783
- let prev = 'Start';
784
- for (const c of comps) {
785
- const safeName = sanitize(c.name);
786
- lines.push(` ${safeName}["${escapeMermaid(c.originalName)}"]`);
787
- lines.push(` ${prev} --> ${safeName}`);
788
- prev = safeName;
789
- }
790
- lines.push(' End(["End"])');
791
- lines.push(` ${prev} --> End`);
792
- return lines.join('\n');
793
- }
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}`);
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;
946
183
  }
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}`);
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;
998
189
  }
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}`);
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;
1063
195
  }
196
+ commandFound = true;
1064
197
  }
1065
- }
1066
-
1067
- lines.push(' classDef securityNode fill:#dc2626,color:#fff');
1068
- return lines.join('\n');
1069
- }
1070
198
 
1071
- function generate(data, type, focus) {
1072
- switch (type) {
1073
- case 'architecture': return generateArchitecture(data, focus);
1074
- case 'sequence': return generateSequence(data);
1075
- case 'dependency': return generateDependency(data, focus);
1076
- case 'class': return generateClass(data);
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);
1083
- default:
1084
- console.warn(chalk.yellow(`⚠️ Unknown diagram type "${type}", using architecture`));
1085
- return generateArchitecture(data, focus);
199
+ resolvedArgs.push(arg);
1086
200
  }
1087
- }
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
201
 
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
- };
202
+ return resolvedArgs;
1115
203
  }
1116
204
 
1117
- function parseCommaSeparatedList(value) {
1118
- if (!value || typeof value !== 'string') return [];
1119
- return value.split(',').map((item) => item.trim()).filter(Boolean);
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);
1120
211
  }
1121
212
 
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,
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,
1148
225
  };
1149
226
  }
1150
-
1151
- // URL shortening for large diagrams
1152
- function createMermaidUrl(mermaidCode) {
1153
- // If diagram is very large, provide text file instead
1154
- if (mermaidCode.length > 5000) {
1155
- return { url: null, large: true };
1156
- }
1157
-
1158
- try {
1159
- const payload = JSON.stringify({ code: mermaidCode });
1160
- const compressed = zlib.deflateSync(payload);
1161
- const encoded = compressed
1162
- .toString('base64')
1163
- .replace(/\+/g, '-')
1164
- .replace(/\//g, '_')
1165
- .replace(/=+$/g, '');
1166
- const url = `https://mermaid.live/edit#pako:${encoded}`;
1167
-
1168
- // Check if URL is too long for browser
1169
- if (url.length > 8000) {
1170
- return { url: null, large: true };
1171
- }
1172
- return { url, large: false };
1173
- } catch (e) {
1174
- return { url: null, large: true };
1175
- }
1176
- }
1177
-
1178
- // Validate output path to prevent directory traversal
1179
- function validateOutputPath(outputPath, rootPath) {
1180
- if (typeof outputPath !== 'string' || outputPath.trim() === '') {
1181
- throw new Error('Invalid path: output path is required');
1182
- }
1183
-
1184
- // Security: Check for null bytes
1185
- if (outputPath.includes('\0')) {
1186
- throw new Error('Invalid path: null bytes detected');
1187
- }
1188
-
1189
- // Resolve symlinks to prevent symlink attacks
1190
- let realRoot;
1191
- try {
1192
- realRoot = fs.realpathSync(rootPath);
1193
- } catch (e) {
1194
- throw new Error(`Invalid project path: ${rootPath}`);
1195
- }
1196
- const resolved = path.isAbsolute(outputPath)
1197
- ? path.resolve(outputPath)
1198
- : path.resolve(realRoot, outputPath);
1199
-
1200
- const resolveViaExistingAncestor = (targetPath) => {
1201
- const pending = [];
1202
- let probe = targetPath;
1203
-
1204
- while (!fs.existsSync(probe)) {
1205
- pending.unshift(path.basename(probe));
1206
- const parent = path.dirname(probe);
1207
- if (parent === probe) {
1208
- break;
1209
- }
1210
- probe = parent;
1211
- }
1212
-
1213
- const canonicalBase = fs.realpathSync(probe);
1214
- return path.join(canonicalBase, ...pending);
1215
- };
1216
-
1217
- const canonicalResolved = resolveViaExistingAncestor(resolved);
1218
- const relative = path.relative(realRoot, canonicalResolved);
1219
-
1220
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
1221
- throw new Error(`Invalid path: directory traversal detected in "${outputPath}"`);
1222
- }
1223
-
1224
- return canonicalResolved;
1225
- }
1226
-
1227
- function resolveRootPathOrExit(targetPath) {
1228
- const root = path.resolve(targetPath || '.');
1229
- try {
1230
- const stats = fs.statSync(root);
1231
- if (!stats.isDirectory()) {
1232
- console.error(chalk.red('❌ Path error:'), `Target is not a directory: ${root}`);
1233
- process.exit(2);
1234
- }
1235
- } catch (error) {
1236
- console.error(chalk.red('❌ Path error:'), `Target directory not found: ${root}`);
1237
- process.exit(2);
1238
- }
1239
- return root;
1240
- }
1241
-
1242
- function openPreviewUrl(url) {
1243
- const { cmd, args } = getOpenCommand(url, process.platform);
1244
- try {
1245
- const child = spawn(cmd, args, {
1246
- stdio: 'ignore',
1247
- detached: true,
1248
- windowsHide: true
1249
- });
1250
- child.on('error', (err) => {
1251
- console.error(chalk.yellow('⚠️ Failed to open browser:'), err.message);
1252
- });
1253
- child.unref();
1254
- } catch (err) {
1255
- console.error(chalk.yellow('⚠️ Failed to open browser:'), err.message);
1256
- }
1257
- }
1258
-
1259
- function runMermaidCli(args) {
1260
- const candidates = getNpxCommandCandidates(process.platform);
1261
- let lastError = null;
1262
- for (const candidate of candidates) {
1263
- try {
1264
- execFileSync(candidate, args, { stdio: 'pipe', windowsHide: true });
1265
- return;
1266
- } catch (error) {
1267
- lastError = error;
1268
- }
1269
- }
1270
- if (lastError) {
1271
- throw lastError;
1272
- }
1273
- throw new Error('npx command not found');
1274
- }
1275
-
1276
- const ALLOWED_THEMES = ['default', 'dark', 'forest', 'neutral', 'light'];
1277
-
1278
- function normalizeThemeOption(theme, fallback = 'default') {
1279
- const normalized = String(theme || fallback).toLowerCase();
1280
- return ALLOWED_THEMES.includes(normalized) ? normalized : fallback;
1281
- }
1282
-
1283
- function validateExistingPathInRoot(targetPath, rootPath, label = 'path') {
1284
- const realRoot = fs.realpathSync(rootPath);
1285
- const realTarget = fs.realpathSync(targetPath);
1286
- const relative = path.relative(realRoot, realTarget);
1287
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
1288
- throw new Error(`Invalid ${label}: path escapes project root`);
1289
- }
1290
- return realTarget;
1291
- }
1292
-
1293
- // Commands
1294
- program
1295
- .name('diagram')
1296
- .description('Generate architecture diagrams from code')
1297
- .version('1.0.0');
1298
-
1299
- program
1300
- .command('analyze [path]')
1301
- .description('Analyze codebase structure')
1302
- .option('-p, --patterns <list>', 'File patterns (comma-separated)', '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs')
1303
- .option('-e, --exclude <list>', 'Exclude patterns', 'node_modules/**,.git/**,dist/**')
1304
- .option('-m, --max-files <n>', 'Max files to analyze', '100')
1305
- .option('-j, --json', 'Output as JSON')
1306
- .action(async (targetPath, options) => {
1307
- const root = resolveRootPathOrExit(targetPath);
1308
- if (!options.json) {
1309
- console.log(chalk.blue('Analyzing'), root);
1310
- }
1311
-
1312
- const data = await analyze(root, options);
1313
-
1314
- if (options.json) {
1315
- console.log(JSON.stringify(data, null, 2));
1316
- } else {
1317
- console.log(chalk.green('\n📊 Summary'));
1318
- console.log(` Files: ${data.components.length}`);
1319
- console.log(` Languages: ${Object.entries(data.languages).map(([k,v]) => `${k}(${v})`).join(', ') || 'none'}`);
1320
- console.log(` Entry points: ${data.entryPoints.join(', ') || 'none'}`);
1321
- console.log(`\n${chalk.yellow('Components:')}`);
1322
- data.components.slice(0, 15).forEach(c => {
1323
- const deps = c.dependencies.length > 0 ? ` → ${c.dependencies.slice(0, 3).join(', ')}` : '';
1324
- console.log(` ${c.originalName} (${c.type})${deps}`);
1325
- });
1326
- if (data.components.length > 15) {
1327
- console.log(chalk.gray(` ... and ${data.components.length - 15} more`));
1328
- }
1329
- }
1330
- });
1331
-
1332
- program
1333
- .command('generate [path]')
1334
- .description('Generate a diagram')
1335
- .option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow, database, user, events, auth, security', 'architecture')
1336
- .option('-f, --focus <module>', 'Focus on specific module')
1337
- .option('-o, --output <file>', 'Output file (SVG/PNG)')
1338
- .option('-m, --max-files <n>', 'Max files to analyze', '100')
1339
- .option('--theme <theme>', 'Theme: default, dark, forest, neutral', 'default')
1340
- .option('--open', 'Open in browser')
1341
- .action(async (targetPath, options) => {
1342
- const root = resolveRootPathOrExit(targetPath);
1343
- const requestedTheme = String(options.theme || 'default').toLowerCase();
1344
- const safeTheme = normalizeThemeOption(options.theme, 'default');
1345
- if (requestedTheme !== safeTheme) {
1346
- console.warn(chalk.yellow(`⚠️ Unknown theme "${options.theme}", using "${safeTheme}"`));
1347
- }
1348
- console.log(chalk.blue('Generating'), options.type, 'diagram for', root);
1349
-
1350
- const data = await analyze(root, options);
1351
- const mermaid = generate(data, options.type, options.focus);
1352
-
1353
- console.log(chalk.green('\n📐 Mermaid Diagram:\n'));
1354
- console.log('```mermaid');
1355
- console.log(mermaid);
1356
- console.log('```\n');
1357
-
1358
- // Preview URL
1359
- const { url, large } = createMermaidUrl(mermaid);
1360
-
1361
- if (large || !url) {
1362
- console.log(chalk.yellow('⚠️ Diagram is too large for preview URL.'));
1363
- console.log(chalk.cyan('💾 Save to file:'), 'diagram generate . --output diagram.svg');
1364
- } else {
1365
- console.log(chalk.cyan('🔗 Preview:'), url);
1366
- }
1367
-
1368
- // Save to file if requested
1369
- if (options.output) {
1370
- // Validate output path for security
1371
- let safeOutput;
1372
- try {
1373
- safeOutput = validateOutputPath(options.output, root);
1374
- } catch (err) {
1375
- console.error(chalk.red('❌ Output path error:'), err.message);
1376
- process.exit(2);
1377
- }
1378
-
1379
- // Ensure output directory exists
1380
- const outputDir = path.dirname(safeOutput);
1381
- if (!fs.existsSync(outputDir)) {
1382
- fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
1383
- }
1384
-
1385
- const ext = path.extname(options.output).toLowerCase();
1386
- if (ext === '.md' || ext === '.mmd') {
1387
- fs.writeFileSync(safeOutput, mermaid);
1388
- console.log(chalk.green('✅ Saved to'), options.output);
1389
- } else {
1390
- // Try to render
1391
- let tempFile = null;
1392
- try {
1393
- // Use crypto for secure random filename
1394
- const randomId = crypto.randomBytes(16).toString('hex');
1395
- tempFile = path.join(os.tmpdir(), `diagram-${Date.now()}-${randomId}.mmd`);
1396
- fs.writeFileSync(tempFile, `%%{init: {'theme': '${safeTheme}'}}%%\n${mermaid}`);
1397
- runMermaidCli(['-y', '@mermaid-js/mermaid-cli', 'mmdc', '-i', tempFile, '-o', safeOutput, '-b', 'transparent']);
1398
- fs.unlinkSync(tempFile);
1399
- console.log(chalk.green('✅ Rendered to'), options.output);
1400
- } catch (e) {
1401
- if (tempFile && fs.existsSync(tempFile)) {
1402
- try { fs.unlinkSync(tempFile); } catch (e2) {}
1403
- }
1404
- console.error(chalk.red('❌ Could not render output file. Install mermaid-cli: npm i -g @mermaid-js/mermaid-cli'));
1405
- if (process.env.DEBUG) console.error(chalk.gray(e.message));
1406
- process.exit(2);
1407
- }
1408
- }
1409
- }
1410
-
1411
- if (options.open && url) {
1412
- // Security: Validate URL protocol
1413
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
1414
- console.error(chalk.red('❌ Invalid URL protocol'));
1415
- } else {
1416
- openPreviewUrl(url);
1417
- }
1418
- }
1419
- });
1420
-
1421
- program
1422
- .command('all [path]')
1423
- .description('Generate all diagram types')
1424
- .option('-o, --output-dir <dir>', 'Output directory', './diagrams')
1425
- .option('-p, --patterns <list>', 'File patterns', '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs')
1426
- .option('-e, --exclude <list>', 'Exclude patterns', 'node_modules/**,.git/**,dist/**')
1427
- .option('-m, --max-files <n>', 'Max files to analyze', '100')
1428
- .action(async (targetPath, options) => {
1429
- const root = resolveRootPathOrExit(targetPath);
1430
- let outDir;
1431
- try {
1432
- outDir = validateOutputPath(options.outputDir, root);
1433
- } catch (err) {
1434
- console.error(chalk.red('❌ Output path error:'), err.message);
1435
- process.exit(2);
1436
- }
1437
-
1438
- console.log(chalk.blue('Analyzing'), root);
1439
- const data = await analyze(root, options);
1440
-
1441
- if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
1442
-
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
- };
1450
-
1451
- for (const type of types) {
1452
- const mermaid = generate(data, type);
1453
- const file = path.join(outDir, `${type}.mmd`);
1454
- fs.writeFileSync(file, mermaid);
1455
- manifest.diagrams.push(toManifestEntry(type, file, mermaid, root));
1456
- console.log(chalk.green('✅'), type, '→', file);
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);
1462
-
1463
- console.log(chalk.cyan('\n🔗 Preview all at: https://mermaid.live'));
1464
- });
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(
1473
- '--fail-on-placeholder',
1474
- 'Fail if any required diagram was a placeholder (or any placeholder if no required types are set)'
1475
- )
1476
- .action(async (targetPath, options) => {
1477
- const root = resolveRootPathOrExit(targetPath);
1478
- const manifestDir = path.join(root, options.manifestDir || '.diagram');
1479
- const manifestPath = path.join(manifestDir, 'manifest.json');
1480
-
1481
- let safeManifestPath;
1482
- try {
1483
- safeManifestPath = validateExistingPathInRoot(manifestPath, root, 'manifest path');
1484
- } catch (err) {
1485
- console.error(chalk.red('❌ Manifest error:'), err.message);
1486
- process.exit(2);
1487
- }
1488
-
1489
- let manifestRaw;
1490
- try {
1491
- manifestRaw = fs.readFileSync(safeManifestPath, 'utf8');
1492
- } catch (err) {
1493
- console.error(chalk.red('❌ Manifest read failed:'), err.message);
1494
- process.exit(2);
1495
- }
1496
-
1497
- let parsedManifest;
1498
- try {
1499
- parsedManifest = JSON.parse(manifestRaw);
1500
- } catch (err) {
1501
- console.error(chalk.red('❌ Manifest parse failed:'), err.message);
1502
- process.exit(2);
1503
- }
1504
-
1505
- const summary = buildManifestSummary(parsedManifest);
1506
- if (!summary) {
1507
- console.error(chalk.red('❌ Invalid manifest format'));
1508
- process.exit(2);
1509
- }
1510
-
1511
- const required = parseCommaSeparatedList(options.requireTypes);
1512
- const missingRequired = required.filter((type) => !summary.diagrams.some((d) => d.type === type));
1513
- summary.required = {
1514
- requested: required,
1515
- missing: missingRequired,
1516
- };
1517
-
1518
- if (required.length > 0 && missingRequired.length > 0) {
1519
- console.error(chalk.red(`❌ Manifest missing required diagram types: ${missingRequired.join(', ')}`));
1520
- process.exit(2);
1521
- }
1522
-
1523
- const placeholderTypesToCheck = required.length > 0
1524
- ? summary.placeholderTypes.filter((type) => required.includes(type))
1525
- : summary.placeholderTypes;
1526
-
1527
- if (options.failOnPlaceholder && placeholderTypesToCheck.length > 0) {
1528
- console.error(
1529
- chalk.yellow(
1530
- `⚠️ Manifest includes ${placeholderTypesToCheck.length} required placeholder diagram(s): ${placeholderTypesToCheck.join(', ')}`
1531
- )
1532
- );
1533
- process.exit(2);
1534
- }
1535
-
1536
- if (options.output) {
1537
- let safeOutput;
1538
- try {
1539
- safeOutput = validateOutputPath(options.output, root);
1540
- } catch (err) {
1541
- console.error(chalk.red('❌ Output path error:'), err.message);
1542
- process.exit(2);
1543
- }
1544
-
1545
- const outputDir = path.dirname(safeOutput);
1546
- if (!fs.existsSync(outputDir)) {
1547
- fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
1548
- }
1549
-
1550
- fs.writeFileSync(safeOutput, `${JSON.stringify(summary, null, 2)}\n`);
1551
- console.log(chalk.green('✅ manifest summary'), '→', safeOutput);
1552
- return;
1553
- }
1554
-
1555
- console.log(chalk.blue('\n📘 Manifest summary for'), safeManifestPath);
1556
- console.log(` Total: ${summary.totalDiagrams}`);
1557
- console.log(` Placeholder: ${summary.placeholders}`);
1558
- if (summary.missingTypes.length > 0) {
1559
- console.log(chalk.yellow(` Missing expected (all supported): ${summary.missingTypes.join(', ')}`));
1560
- }
1561
- if (summary.placeholderTypes.length > 0) {
1562
- console.log(chalk.yellow(` Placeholder types: ${summary.placeholderTypes.join(', ')}`));
1563
- }
1564
- console.log('');
1565
- for (const entry of summary.diagrams) {
1566
- const status = entry.isPlaceholder ? chalk.yellow('placeholder') : chalk.green('ok');
1567
- console.log(` ${status} ${entry.type} -> ${entry.file}`);
1568
- }
1569
- });
1570
-
1571
- program
1572
- .command('video [path]')
1573
- .description('Generate an animated video of the diagram')
1574
- .option('-t, --type <type>', 'Diagram type', 'architecture')
1575
- .option('-o, --output <file>', 'Output file (.mp4, .webm, .mov)', 'diagram.mp4')
1576
- .option('-d, --duration <sec>', 'Video duration in seconds', '5')
1577
- .option('-f, --fps <n>', 'Frames per second', '30')
1578
- .option('--width <n>', 'Video width', '1280')
1579
- .option('--height <n>', 'Video height', '720')
1580
- .option('--theme <theme>', 'Theme: default, dark, forest, neutral', 'dark')
1581
- .option('-m, --max-files <n>', 'Max files to analyze', '100')
1582
- .action(async (targetPath, options) => {
1583
- const root = resolveRootPathOrExit(targetPath);
1584
- const safeTheme = normalizeThemeOption(options.theme, 'dark');
1585
-
1586
- // Validate output path
1587
- let safeOutput;
1588
- try {
1589
- safeOutput = validateOutputPath(options.output, root);
1590
- } catch (err) {
1591
- console.error(chalk.red('❌ Output path error:'), err.message);
1592
- process.exit(2);
1593
- }
1594
-
1595
- const outputDir = path.dirname(safeOutput);
1596
- if (!fs.existsSync(outputDir)) {
1597
- fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
1598
- }
1599
-
1600
- console.log(chalk.blue('🎬 Generating video for'), root);
1601
-
1602
- const data = await analyze(root, options);
1603
- const mermaid = generate(data, options.type);
1604
-
1605
- const { generateVideo } = getVideoModule();
1606
-
1607
- await generateVideo(mermaid, safeOutput, {
1608
- duration: parseInt(options.duration) || 5,
1609
- fps: parseInt(options.fps) || 30,
1610
- width: parseInt(options.width) || 1280,
1611
- height: parseInt(options.height) || 720,
1612
- theme: safeTheme
1613
- });
1614
- });
1615
-
1616
- program
1617
- .command('animate [path]')
1618
- .description('Generate animated SVG with CSS animations')
1619
- .option('-t, --type <type>', 'Diagram type', 'architecture')
1620
- .option('-o, --output <file>', 'Output file', 'diagram-animated.svg')
1621
- .option('--theme <theme>', 'Theme', 'dark')
1622
- .option('-m, --max-files <n>', 'Max files to analyze', '100')
1623
- .action(async (targetPath, options) => {
1624
- const root = resolveRootPathOrExit(targetPath);
1625
- const safeTheme = normalizeThemeOption(options.theme, 'dark');
1626
-
1627
- // Validate output path
1628
- let safeOutput;
1629
- try {
1630
- safeOutput = validateOutputPath(options.output, root);
1631
- } catch (err) {
1632
- console.error(chalk.red('❌ Output path error:'), err.message);
1633
- process.exit(2);
1634
- }
1635
-
1636
- const outputDir = path.dirname(safeOutput);
1637
- if (!fs.existsSync(outputDir)) {
1638
- fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
1639
- }
1640
-
1641
- console.log(chalk.blue('✨ Generating animated SVG for'), root);
1642
-
1643
- const data = await analyze(root, options);
1644
- const mermaid = generate(data, options.type);
1645
-
1646
- const { generateAnimatedSVG } = getVideoModule();
1647
-
1648
- await generateAnimatedSVG(mermaid, safeOutput, {
1649
- theme: safeTheme
1650
- });
1651
- });
1652
-
1653
- program
1654
- .command('test [path]')
1655
- .description('Validate architecture against .architecture.yml rules')
1656
- .option('-c, --config <file>', 'Config file path', '.architecture.yml')
1657
- .option('-f, --format <format>', 'Output format: console, json, junit', 'console')
1658
- .option('-o, --output <file>', 'Output file (for json/junit formats)')
1659
- .option('-p, --patterns <list>', 'File patterns', '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs')
1660
- .option('-e, --exclude <list>', 'Exclude patterns', 'node_modules/**,.git/**,dist/**')
1661
- .option('-m, --max-files <n>', 'Max files to analyze', '100')
1662
- .option('--dry-run', 'Preview file matching without validation', false)
1663
- .option('--verbose', 'Show detailed output', false)
1664
- .option('--init', 'Generate starter configuration file', false)
1665
- .option('--force', 'Overwrite existing configuration with --init', false)
1666
- .action(async (targetPath, options) => {
1667
- const { RulesEngine } = require('./rules');
1668
- const { ComponentGraph } = require('./graph');
1669
- const { RuleFactory } = require('./rules/factory');
1670
- const { formatResults } = require('./formatters/index');
1671
- const { validateConfig, getDefaultConfig } = require('./schema/rules-schema');
1672
- const YAML = require('yaml');
1673
-
1674
- const root = resolveRootPathOrExit(targetPath);
1675
- const engine = new RulesEngine();
1676
- const startTime = Date.now();
1677
- const outputsMachineFormat =
1678
- !options.output && (options.format === 'json' || options.format === 'junit');
1679
- const quietMachineOutput = outputsMachineFormat && !options.verbose;
1680
-
1681
- // Init mode - generate starter config
1682
- if (options.init) {
1683
- const configPath = path.join(root, '.architecture.yml');
1684
-
1685
- if (fs.existsSync(configPath) && !options.force) {
1686
- console.error(chalk.yellow('⚠️ Configuration already exists:'), configPath);
1687
- console.log(chalk.gray(' Use --force to overwrite'));
1688
- process.exit(2);
1689
- }
1690
-
1691
- const defaultConfig = getDefaultConfig();
1692
- const yaml = YAML.stringify(defaultConfig, {
1693
- indent: 2,
1694
- lineWidth: 0
1695
- });
1696
-
1697
- fs.writeFileSync(configPath, yaml);
1698
- console.log(chalk.green('✅ Created configuration:'), configPath);
1699
- console.log(chalk.gray('\nEdit the file to define your architecture rules, then run:'));
1700
- console.log(chalk.cyan(' diagram test'));
1701
- process.exit(0);
1702
- }
1703
-
1704
- // Find or use specified config
1705
- let configPath = options.config;
1706
- if (!path.isAbsolute(configPath)) {
1707
- configPath = path.join(root, configPath);
1708
- }
1709
-
1710
- // Validate config path is within project root (security check)
1711
- const relativeConfigPath = path.relative(root, configPath);
1712
- if (relativeConfigPath.startsWith('..') || path.isAbsolute(relativeConfigPath)) {
1713
- console.error(chalk.red('❌ Invalid config path: directory traversal detected'));
1714
- process.exit(2);
1715
- }
1716
-
1717
- if (!fs.existsSync(configPath)) {
1718
- // Try to find config in root
1719
- const found = engine.findConfig(root);
1720
- if (!found) {
1721
- console.error(chalk.red('❌ No .architecture.yml found. Run: diagram test --init'));
1722
- process.exit(2);
1723
- }
1724
- configPath = found;
1725
- }
1726
-
1727
- // Load config
1728
- let config;
1729
- try {
1730
- config = engine.loadConfig(configPath);
1731
- } catch (error) {
1732
- console.error(chalk.red('❌ Config error:'), error.message);
1733
- process.exit(2);
1734
- }
1735
-
1736
- // Validate config against schema
1737
- const validation = validateConfig(config);
1738
- if (!validation.valid) {
1739
- console.error(chalk.red('❌ Schema validation failed:'));
1740
- for (const err of validation.errors) {
1741
- console.error(chalk.red(` • ${err.path}: ${err.message}`));
1742
- }
1743
- process.exit(2);
1744
- }
1745
-
1746
- // Analyze codebase
1747
- if (!quietMachineOutput) {
1748
- console.log(chalk.blue('🔍 Analyzing'), root);
1749
- }
1750
- const data = await analyze(root, options);
1751
- const graph = new ComponentGraph(data);
1752
-
1753
- // Create rules
1754
- let rules;
1755
- try {
1756
- rules = RuleFactory.createRules(config);
1757
- } catch (error) {
1758
- console.error(chalk.red('❌ Rule error:'), error.message);
1759
- process.exit(2);
1760
- }
1761
-
1762
- // Dry run mode - just show file matching
1763
- if (options.dryRun) {
1764
- const preview = engine.previewMatches(rules, graph);
1765
- console.log(chalk.cyan('\n📋 Dry Run - File Matching Preview\n'));
1766
- for (const rule of preview.rules) {
1767
- console.log(chalk.bold(rule.name));
1768
- console.log(' Layer:', chalk.gray(Array.isArray(rule.layer) ? rule.layer.join(', ') : rule.layer));
1769
- console.log(' Matched files:', rule.matchedFiles.length);
1770
- if (options.verbose) {
1771
- for (const file of rule.matchedFiles) {
1772
- console.log(' -', file);
1773
- }
1774
- }
1775
- console.log();
1776
- }
1777
- process.exit(0);
1778
- }
1779
-
1780
- // Run validation
1781
- if (!quietMachineOutput) {
1782
- console.log(chalk.blue('🧪 Validating'), rules.length, 'rules...\n');
1783
- }
1784
- const results = engine.validate(rules, graph);
1785
-
1786
- // Validate output path if specified
1787
- let safeOutput = options.output;
1788
- if (safeOutput) {
1789
- try {
1790
- safeOutput = validateOutputPath(safeOutput, root);
1791
- } catch (err) {
1792
- console.error(chalk.red('❌ Output path error:'), err.message);
1793
- process.exit(2);
1794
- }
1795
- }
1796
-
1797
- // Output results
1798
- const exitCode = formatResults(results, options.format, {
1799
- output: safeOutput,
1800
- verbose: options.verbose
1801
- }, startTime);
1802
-
1803
- process.exit(exitCode);
1804
- });
1805
-
1806
- program.parse();