project-graph-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * CLI Entry Point for Project Graph MCP
3
+ */
4
+
5
+ import { CLI_HANDLERS } from './cli-handlers.js';
6
+
7
+ /**
8
+ * Print CLI help
9
+ */
10
+ export function printHelp() {
11
+ console.log(`
12
+ project-graph-mcp - MCP server for AI agents
13
+
14
+ Usage:
15
+ node src/server.js Start MCP stdio server
16
+ node src/server.js <command> [args] Run CLI command
17
+
18
+ Commands:
19
+ skeleton <path> Get compact project overview
20
+ expand <symbol> Expand minified symbol (e.g., SN, SN.togglePin)
21
+ deps <symbol> Get dependency tree
22
+ usages <symbol> Find all usages
23
+ pending <path> List pending @test/@expect tests
24
+ summary <path> Get test progress summary
25
+ undocumented <path> Find missing JSDoc (--level=tests|params|all)
26
+ deadcode <path> Find unused functions/classes
27
+ jsdoc <file> Generate JSDoc for file
28
+ similar <path> Find similar functions (--threshold=60)
29
+ complexity <path> Analyze cyclomatic complexity (--min=1)
30
+ largefiles <path> Find files needing split (--problematic)
31
+ outdated <path> Find legacy patterns & redundant deps
32
+ analyze <path> Run ALL checks with Health Score
33
+ filters Show current filter configuration
34
+ instructions Show agent guidelines (JSDoc, Arch)
35
+ help Show this help
36
+
37
+ Examples:
38
+ node src/server.js skeleton src/components
39
+ node src/server.js expand SN
40
+ node src/server.js pending src/
41
+ `);
42
+ }
43
+
44
+ /**
45
+ * Run CLI command
46
+ * @param {string} command
47
+ * @param {string[]} args
48
+ */
49
+ export async function runCLI(command, args) {
50
+ // Handle help commands
51
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
52
+ printHelp();
53
+ return;
54
+ }
55
+
56
+ // Look up handler
57
+ const def = CLI_HANDLERS[command];
58
+ if (!def) {
59
+ console.error(`Unknown command: ${command}`);
60
+ console.error('Run with "help" for usage information');
61
+ process.exit(1);
62
+ }
63
+
64
+ // Validate required arg
65
+ if (def.requiresArg && !args[0]) {
66
+ console.error(def.argError || `Argument required for: ${command}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ try {
71
+ const result = await def.handler(args);
72
+
73
+ // Handle raw output (like instructions)
74
+ if (def.rawOutput) {
75
+ console.log(result);
76
+ } else {
77
+ console.log(JSON.stringify(result, null, 2));
78
+ }
79
+ } catch (error) {
80
+ console.error('Error:', error.message);
81
+ process.exit(1);
82
+ }
83
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Cyclomatic Complexity Analyzer
3
+ * Measures function complexity based on decision points
4
+ */
5
+
6
+ import { readFileSync, readdirSync, statSync } from 'fs';
7
+ import { join, relative, resolve } from 'path';
8
+ import { parse } from '../vendor/acorn.mjs';
9
+ import * as walk from '../vendor/walk.mjs';
10
+ import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
11
+
12
+ /**
13
+ * @typedef {Object} ComplexityItem
14
+ * @property {string} name
15
+ * @property {string} type - 'function' | 'method'
16
+ * @property {string} file
17
+ * @property {number} line
18
+ * @property {number} complexity - Cyclomatic complexity score
19
+ * @property {string} rating - 'low' | 'moderate' | 'high' | 'critical'
20
+ */
21
+
22
+ /**
23
+ * Find all JS files
24
+ * @param {string} dir
25
+ * @param {string} rootDir
26
+ * @returns {string[]}
27
+ */
28
+ function findJSFiles(dir, rootDir = dir) {
29
+ if (dir === rootDir) parseGitignore(rootDir);
30
+ const files = [];
31
+
32
+ try {
33
+ for (const entry of readdirSync(dir)) {
34
+ const fullPath = join(dir, entry);
35
+ const relativePath = relative(rootDir, fullPath);
36
+ const stat = statSync(fullPath);
37
+
38
+ if (stat.isDirectory()) {
39
+ if (!shouldExcludeDir(entry, relativePath)) {
40
+ files.push(...findJSFiles(fullPath, rootDir));
41
+ }
42
+ } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
43
+ if (!shouldExcludeFile(entry, relativePath)) {
44
+ files.push(fullPath);
45
+ }
46
+ }
47
+ }
48
+ } catch (e) { }
49
+
50
+ return files;
51
+ }
52
+
53
+ /**
54
+ * Calculate complexity of a function body
55
+ * @param {Object} body
56
+ * @returns {number}
57
+ */
58
+ function calculateComplexity(body) {
59
+ let complexity = 1; // Base complexity
60
+
61
+ walk.simple(body, {
62
+ // Branching
63
+ IfStatement() { complexity++; },
64
+ ConditionalExpression() { complexity++; }, // ternary
65
+
66
+ // Loops
67
+ ForStatement() { complexity++; },
68
+ ForOfStatement() { complexity++; },
69
+ ForInStatement() { complexity++; },
70
+ WhileStatement() { complexity++; },
71
+ DoWhileStatement() { complexity++; },
72
+
73
+ // Switch cases
74
+ SwitchCase(node) {
75
+ if (node.test) complexity++; // Skip default case
76
+ },
77
+
78
+ // Logical operators
79
+ LogicalExpression(node) {
80
+ if (node.operator === '&&' || node.operator === '||') {
81
+ complexity++;
82
+ }
83
+ },
84
+
85
+ // Nullish coalescing
86
+ BinaryExpression(node) {
87
+ if (node.operator === '??') {
88
+ complexity++;
89
+ }
90
+ },
91
+
92
+ // Error handling
93
+ CatchClause() { complexity++; },
94
+ });
95
+
96
+ return complexity;
97
+ }
98
+
99
+ /**
100
+ * Get rating from complexity score
101
+ * @param {number} complexity
102
+ * @returns {string}
103
+ */
104
+ function getRating(complexity) {
105
+ if (complexity <= 5) return 'low';
106
+ if (complexity <= 10) return 'moderate';
107
+ if (complexity <= 20) return 'high';
108
+ return 'critical';
109
+ }
110
+
111
+ /**
112
+ * Analyze complexity of file
113
+ * @param {string} filePath
114
+ * @returns {ComplexityItem[]}
115
+ */
116
+ function analyzeFile(filePath, rootDir) {
117
+ const code = readFileSync(filePath, 'utf-8');
118
+ const relPath = relative(rootDir, filePath);
119
+ const items = [];
120
+
121
+ let ast;
122
+ try {
123
+ ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
124
+ } catch (e) {
125
+ return items;
126
+ }
127
+
128
+ walk.simple(ast, {
129
+ FunctionDeclaration(node) {
130
+ if (!node.id) return;
131
+ const complexity = calculateComplexity(node.body);
132
+ items.push({
133
+ name: node.id.name,
134
+ type: 'function',
135
+ file: relPath,
136
+ line: node.loc.start.line,
137
+ complexity,
138
+ rating: getRating(complexity),
139
+ });
140
+ },
141
+
142
+ ArrowFunctionExpression(node) {
143
+ // Skip small arrow functions
144
+ if (node.body.type !== 'BlockStatement') return;
145
+ const complexity = calculateComplexity(node.body);
146
+ if (complexity > 5) {
147
+ // Only report complex arrow functions
148
+ items.push({
149
+ name: '(arrow)',
150
+ type: 'function',
151
+ file: relPath,
152
+ line: node.loc.start.line,
153
+ complexity,
154
+ rating: getRating(complexity),
155
+ });
156
+ }
157
+ },
158
+
159
+ MethodDefinition(node) {
160
+ if (node.kind !== 'method') return;
161
+ const name = node.key.name || node.key.value;
162
+ const complexity = calculateComplexity(node.value.body);
163
+ items.push({
164
+ name,
165
+ type: 'method',
166
+ file: relPath,
167
+ line: node.loc.start.line,
168
+ complexity,
169
+ rating: getRating(complexity),
170
+ });
171
+ },
172
+ });
173
+
174
+ return items;
175
+ }
176
+
177
+ /**
178
+ * Get complexity analysis for directory
179
+ * @param {string} dir
180
+ * @param {Object} [options]
181
+ * @param {number} [options.minComplexity=1] - Minimum complexity to include
182
+ * @param {boolean} [options.onlyProblematic=false] - Only show high/critical
183
+ * @returns {Promise<{total: number, stats: Object, items: ComplexityItem[]}>}
184
+ */
185
+ export async function getComplexity(dir, options = {}) {
186
+ const minComplexity = options.minComplexity || 1;
187
+ const onlyProblematic = options.onlyProblematic || false;
188
+ const resolvedDir = resolve(dir);
189
+
190
+ const files = findJSFiles(dir);
191
+ let allItems = [];
192
+
193
+ for (const file of files) {
194
+ allItems.push(...analyzeFile(file, resolvedDir));
195
+ }
196
+
197
+ // Filter
198
+ allItems = allItems.filter(item => {
199
+ if (item.complexity < minComplexity) return false;
200
+ if (onlyProblematic && (item.rating === 'low' || item.rating === 'moderate')) return false;
201
+ return true;
202
+ });
203
+
204
+ // Sort by complexity descending
205
+ allItems.sort((a, b) => b.complexity - a.complexity);
206
+
207
+ // Calculate stats
208
+ const stats = {
209
+ low: allItems.filter(i => i.rating === 'low').length,
210
+ moderate: allItems.filter(i => i.rating === 'moderate').length,
211
+ high: allItems.filter(i => i.rating === 'high').length,
212
+ critical: allItems.filter(i => i.rating === 'critical').length,
213
+ average: allItems.length > 0
214
+ ? Math.round(allItems.reduce((s, i) => s + i.complexity, 0) / allItems.length * 10) / 10
215
+ : 0,
216
+ };
217
+
218
+ return {
219
+ total: allItems.length,
220
+ stats,
221
+ items: allItems.slice(0, 30),
222
+ };
223
+ }