@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.
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 +178 -1761
  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,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 { 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
- // Read version from package.json
5
+ const path = require('path');
15
6
  const packageJson = require('../package.json');
16
-
17
- // Video generation (lazy loaded)
18
- let videoModule;
19
- function getVideoModule() {
20
- if (!videoModule) {
21
- try {
22
- videoModule = require('./video.js');
23
- } catch (e) {
24
- console.error(chalk.red('❌ Video generation requires Playwright. Install with: npm install playwright'));
25
- process.exit(1);
26
- }
27
- }
28
- return videoModule;
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
- // Utility functions
34
- function detectLanguage(filePath) {
35
- if (typeof filePath !== 'string') return 'unknown';
36
- const ext = path.extname(filePath).toLowerCase();
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
- // In analysis mode we can enforce root boundaries with absolute paths
199
- if (rootPath) {
200
- const absoluteTarget = path.resolve(rootPath, fromDir, importPath);
201
- const relativeToRoot = toComparablePath(path.relative(rootPath, absoluteTarget));
202
- if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
203
- return null;
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 findComponentByResolvedPath(components, resolvedPath) {
215
- const comparablePath = toComparablePath(resolvedPath);
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 getExternalPackageName(importPath) {
223
- if (typeof importPath !== 'string') return null;
224
- if (!importPath) return null;
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
- const ROLE_PATTERNS = {
233
- user: [
234
- 'route', 'routes', 'controller', 'controllers', 'handler', 'handlers',
235
- 'api', 'middleware', 'page', 'pages', 'ui', 'frontend', 'web', 'client', 'request'
236
- ],
237
- auth: [
238
- 'auth', 'authentication', 'authorization', 'session', 'signin', 'login',
239
- 'signup', 'token', 'jwt', 'oauth', 'sso', 'passport', 'identity', 'acl',
240
- 'guard', 'permission', 'password', 'mfa', 'security'
241
- ],
242
- database: [
243
- 'db', 'database', 'data', 'datastore', 'repository', 'repo', 'model',
244
- 'schema', 'migration', 'query', 'querybuilder', 'prisma', 'typeorm',
245
- 'sequelize', 'mongoose', 'knex', 'drizzle', 'redis', 'postgres', 'mysql',
246
- 'sqlite', 'mongo', 'dynamodb', 'd1'
247
- ],
248
- events: [
249
- 'event', 'events', 'queue', 'worker', 'cron', 'scheduler', 'webhook',
250
- 'pubsub', 'bus', 'publish', 'subscriber', 'consumer', 'producer',
251
- 'listener', 'trigger'
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 externalPackage = getExternalPackageName(importPath);
293
- if (externalPackage) {
294
- packages.add(externalPackage);
127
+ for (const subcommand of command.commands || []) {
128
+ stack.push(subcommand);
295
129
  }
296
130
  }
297
131
 
298
- return [...packages];
299
- }
300
-
301
- function inferRoleTags(filePath, originalName, fileContent, importEntries, type) {
302
- const content = (fileContent || '').toLowerCase();
303
- const pathText = normalizePath(filePath || '').toLowerCase();
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
- function getExternalPackageList(importEntries) {
340
- const packages = collectExternalImports(importEntries);
341
- if (!packages.length) return [];
342
- return packages.map((pkg) => ({
343
- name: pkg,
344
- label: pkg,
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
- function mapSafeNames(components) {
349
- const map = new Map();
350
- const used = new Set();
156
+ for (let i = 0; i < argv.length; i += 1) {
157
+ const arg = argv[i];
351
158
 
352
- for (const component of components) {
353
- const rawName = sanitize(component.name || component.originalName || 'node');
354
- if (!used.has(rawName)) {
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
- let i = 1;
361
- let candidate = `${rawName}_${i}`;
362
- while (used.has(candidate)) {
363
- i += 1;
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
- for (const filePath of uniqueFiles) {
503
- try {
504
- // Security: Check file size before reading
505
- const stats = fs.statSync(filePath);
506
- if (stats.size > 10 * 1024 * 1024) { // 10MB limit
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
- const content = fs.readFileSync(filePath, 'utf-8');
511
- const lang = detectLanguage(filePath);
512
- let rel = normalizePath(path.relative(rootPath, filePath));
513
- const dir = path.dirname(rel);
514
- if (dir === '.') {
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
- lines.push(' classDef eventNode fill:#db2777,color:#fff');
954
- return lines.join('\n');
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
- const providerSet = new Set();
1006
- for (const seed of seeds) {
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
- function generate(data, type, focus) {
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
- function toManifestEntry(type, filePath, mermaidCode, rootPath) {
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
- function parseCommaSeparatedList(value) {
1121
- if (!value || typeof value !== 'string') return [];
1122
- 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);
1123
211
  }
1124
212
 
1125
- function buildManifestSummary(manifest) {
1126
- if (!manifest || !Array.isArray(manifest.diagrams)) {
1127
- return null;
1128
- }
1129
-
1130
- const diagrams = manifest.diagrams
1131
- .map((diagram) => ({
1132
- ...diagram,
1133
- isPlaceholder: Boolean(diagram.isPlaceholder),
1134
- }))
1135
- .filter((entry) => entry && typeof entry.type === 'string' && entry.file);
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();