project-graph-mcp 1.5.0 → 2.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 (125) hide show
  1. package/README.md +171 -31
  2. package/docs/img/explorer-compact.jpg +0 -0
  3. package/docs/img/explorer-expanded.jpg +0 -0
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -1
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/.project-graph-cache.json +1 -0
  23. package/src/compact/ai-context.js +7 -0
  24. package/src/compact/compact-migrate.js +17 -0
  25. package/src/compact/compact.js +18 -0
  26. package/src/compact/compress.js +14 -0
  27. package/src/compact/ctx-to-jsdoc.js +29 -0
  28. package/src/compact/doc-dialect.js +30 -0
  29. package/src/compact/expand.js +37 -0
  30. package/src/compact/framework-references.js +5 -0
  31. package/src/compact/instructions.js +3 -0
  32. package/src/compact/mode-config.js +8 -0
  33. package/src/compact/validate-pipeline.js +9 -0
  34. package/src/core/event-bus.js +9 -0
  35. package/src/core/filters.js +14 -0
  36. package/src/core/graph-builder.js +12 -0
  37. package/src/core/parser.js +31 -0
  38. package/src/core/workspace.js +8 -0
  39. package/src/lang/lang-go.js +17 -0
  40. package/src/lang/lang-python.js +12 -0
  41. package/src/lang/lang-sql.js +23 -0
  42. package/src/lang/lang-typescript.js +9 -0
  43. package/src/lang/lang-utils.js +4 -0
  44. package/src/mcp/mcp-server.js +17 -0
  45. package/src/mcp/tool-defs.js +3 -0
  46. package/src/mcp/tools.js +25 -0
  47. package/src/network/backend-lifecycle.js +19 -0
  48. package/src/network/backend.js +5 -0
  49. package/src/network/local-gateway.js +23 -0
  50. package/src/network/mdns.js +13 -0
  51. package/src/network/server.js +10 -0
  52. package/src/network/web-server.js +34 -0
  53. package/web/.project-graph-cache.json +1 -0
  54. package/web/app.js +17 -0
  55. package/web/components/code-block.js +3 -0
  56. package/web/components/quick-open.js +5 -0
  57. package/web/dashboard-state.js +3 -0
  58. package/web/dashboard.html +27 -0
  59. package/web/dashboard.js +8 -0
  60. package/web/highlight.js +13 -0
  61. package/web/index.html +35 -0
  62. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  63. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  64. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  65. package/web/panels/EventItem/EventItem.css.js +1 -0
  66. package/web/panels/EventItem/EventItem.js +4 -0
  67. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  69. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  70. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  72. package/web/panels/ProjectList/ProjectList.js +4 -0
  73. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  74. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  77. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  78. package/web/panels/code-viewer.js +5 -0
  79. package/web/panels/ctx-panel.js +4 -0
  80. package/web/panels/dep-graph.js +6 -0
  81. package/web/panels/file-tree.js +188 -0
  82. package/web/panels/health-panel.js +3 -0
  83. package/web/panels/live-monitor.js +3 -0
  84. package/web/state.js +17 -0
  85. package/web/style.css +157 -0
  86. package/references/symbiote-3x.md +0 -834
  87. package/src/ai-context.js +0 -113
  88. package/src/analysis-cache.js +0 -155
  89. package/src/cli-handlers.js +0 -271
  90. package/src/cli.js +0 -95
  91. package/src/compact.js +0 -207
  92. package/src/complexity.js +0 -237
  93. package/src/compress.js +0 -319
  94. package/src/ctx-to-jsdoc.js +0 -514
  95. package/src/custom-rules.js +0 -584
  96. package/src/db-analysis.js +0 -194
  97. package/src/dead-code.js +0 -468
  98. package/src/doc-dialect.js +0 -716
  99. package/src/filters.js +0 -227
  100. package/src/framework-references.js +0 -177
  101. package/src/full-analysis.js +0 -470
  102. package/src/graph-builder.js +0 -299
  103. package/src/instructions.js +0 -73
  104. package/src/jsdoc-checker.js +0 -351
  105. package/src/jsdoc-generator.js +0 -203
  106. package/src/lang-go.js +0 -285
  107. package/src/lang-python.js +0 -197
  108. package/src/lang-sql.js +0 -309
  109. package/src/lang-typescript.js +0 -190
  110. package/src/lang-utils.js +0 -124
  111. package/src/large-files.js +0 -163
  112. package/src/mcp-server.js +0 -675
  113. package/src/mode-config.js +0 -127
  114. package/src/outdated-patterns.js +0 -296
  115. package/src/parser.js +0 -662
  116. package/src/server.js +0 -28
  117. package/src/similar-functions.js +0 -279
  118. package/src/test-annotations.js +0 -323
  119. package/src/tool-defs.js +0 -793
  120. package/src/tools.js +0 -470
  121. package/src/type-checker.js +0 -188
  122. package/src/undocumented.js +0 -259
  123. package/src/workspace.js +0 -70
  124. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  125. /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
package/src/tools.js DELETED
@@ -1,470 +0,0 @@
1
- /**
2
- * MCP Tools for Project Graph
3
- */
4
-
5
- import { parseProject, parseFile, findJSFiles } from './parser.js';
6
- import { buildGraph, createSkeleton } from './graph-builder.js';
7
- import { readFileSync, statSync, writeFileSync, existsSync, unlinkSync } from 'fs';
8
- import { execSync } from 'child_process';
9
- import { join } from 'path';
10
-
11
- /** @type {import('./graph-builder.js').Graph|null} */
12
- let cachedGraph = null;
13
-
14
- /** @type {string|null} */
15
- let cachedPath = null;
16
-
17
- /** @type {Map<string, number>} file path -> mtimeMs */
18
- let cachedMtimes = new Map();
19
-
20
- /**
21
- * Save cache to disk
22
- * @param {string} path
23
- * @param {import('./graph-builder.js').Graph} graph
24
- */
25
- function saveDiskCache(path, graph) {
26
- try {
27
- const cachePath = join(path, '.project-graph-cache.json');
28
- const cacheData = {
29
- version: 1,
30
- path: path,
31
- mtimes: Object.fromEntries(cachedMtimes),
32
- graph: graph
33
- };
34
- writeFileSync(cachePath, JSON.stringify(cacheData), 'utf-8');
35
- } catch (e) {
36
- // Ignore cache save errors
37
- }
38
- }
39
-
40
- /**
41
- * Load cache from disk
42
- * @param {string} path
43
- * @returns {boolean} true if cache was successfully loaded and is valid
44
- */
45
- function loadDiskCache(path) {
46
- try {
47
- const cachePath = join(path, '.project-graph-cache.json');
48
- if (!existsSync(cachePath)) return false;
49
-
50
- const content = readFileSync(cachePath, 'utf-8');
51
- const data = JSON.parse(content);
52
-
53
- if (data.version !== 1 || data.path !== path) return false;
54
-
55
- cachedMtimes.clear();
56
- for (const [file, mtime] of Object.entries(data.mtimes)) {
57
- cachedMtimes.set(file, mtime);
58
- }
59
-
60
- cachedGraph = data.graph;
61
- cachedPath = path;
62
-
63
- const changed = detectChanges(path);
64
- if (changed) {
65
- cachedGraph = null;
66
- cachedPath = null;
67
- cachedMtimes.clear();
68
- return false;
69
- }
70
-
71
- return true;
72
- } catch (e) {
73
- return false;
74
- }
75
- }
76
-
77
- /**
78
- * Get or build graph with smart mtime-based caching.
79
- * On first call: full parse + build.
80
- * On subsequent calls: check file mtimes, rebuild only if changes detected.
81
- * @param {string} path
82
- * @returns {Promise<import('./graph-builder.js').Graph>}
83
- */
84
- export async function getGraph(path) {
85
- // Different path = full rebuild
86
- if (cachedGraph && cachedPath === path) {
87
- // Check for file changes via mtime
88
- const changed = detectChanges(path);
89
- if (!changed) {
90
- return cachedGraph;
91
- }
92
- // Files changed - full rebuild (incremental would need graph-builder changes)
93
- } else if (!cachedGraph) {
94
- if (loadDiskCache(path)) {
95
- return cachedGraph;
96
- }
97
- }
98
-
99
- const parsed = await parseProject(path);
100
- cachedGraph = buildGraph(parsed);
101
- cachedPath = path;
102
-
103
- // Snapshot mtimes for all parsed files
104
- snapshotMtimes(path);
105
- saveDiskCache(path, cachedGraph);
106
-
107
- return cachedGraph;
108
- }
109
-
110
- /**
111
- * Detect if any JS files changed since last snapshot.
112
- * Checks: new files, deleted files, modified files (via mtimeMs).
113
- * @param {string} path
114
- * @returns {boolean} true if changes detected
115
- */
116
- function detectChanges(path) {
117
- if (cachedMtimes.size === 0) return true;
118
-
119
- try {
120
- const currentFiles = findJSFiles(path);
121
- const currentSet = new Set(currentFiles);
122
- const cachedSet = new Set(cachedMtimes.keys());
123
-
124
- // New or deleted files
125
- if (currentFiles.length !== cachedMtimes.size) return true;
126
- for (const f of currentFiles) {
127
- if (!cachedSet.has(f)) return true;
128
- }
129
- for (const f of cachedSet) {
130
- if (!currentSet.has(f)) return true;
131
- }
132
-
133
- // Check mtimes
134
- for (const file of currentFiles) {
135
- try {
136
- const mtime = statSync(file).mtimeMs;
137
- if (mtime !== cachedMtimes.get(file)) return true;
138
- } catch {
139
- return true; // File gone or unreadable
140
- }
141
- }
142
-
143
- return false;
144
- } catch {
145
- return true; // Safety: rebuild on error
146
- }
147
- }
148
-
149
- /**
150
- * Snapshot current mtimes for all JS files in path.
151
- * @param {string} path
152
- */
153
- function snapshotMtimes(path) {
154
- cachedMtimes.clear();
155
- try {
156
- const files = findJSFiles(path);
157
- for (const file of files) {
158
- try {
159
- cachedMtimes.set(file, statSync(file).mtimeMs);
160
- } catch {
161
- // Skip unreadable files
162
- }
163
- }
164
- } catch {
165
- // Ignore errors
166
- }
167
- }
168
-
169
- /**
170
- * Get compact project skeleton
171
- * @param {string} path
172
- * @returns {Promise<Object>}
173
- */
174
- export async function getSkeleton(path) {
175
- const graph = await getGraph(path);
176
- return createSkeleton(graph);
177
- }
178
-
179
- /**
180
- * Get enriched focus zone based on recent activity
181
- * @param {Object} options
182
- * @param {string[]} [options.recentFiles]
183
- * @param {boolean} [options.useGitDiff]
184
- * @returns {Promise<Object>}
185
- */
186
- export async function getFocusZone(options = {}) {
187
- const path = options.path || 'src/components';
188
- const graph = await getGraph(path);
189
-
190
- let focusFiles = options.recentFiles || [];
191
-
192
- // Auto-detect from git diff
193
- if (options.useGitDiff) {
194
- try {
195
- const diff = execSync('git diff --name-only HEAD~5', { encoding: 'utf-8' });
196
- focusFiles = diff.split('\n').filter(f => f.endsWith('.js'));
197
- } catch (e) {
198
- // Git not available or not a repo
199
- }
200
- }
201
-
202
- const expanded = {};
203
-
204
- for (const file of focusFiles) {
205
- // Find classes in this file
206
- const content = readFileSync(file, 'utf-8');
207
- const parsed = await parseFile(content, file);
208
-
209
- for (const cls of parsed.classes) {
210
- const shortName = graph.legend[cls.name];
211
- if (shortName && graph.nodes[shortName]) {
212
- expanded[shortName] = {
213
- ...graph.nodes[shortName],
214
- methods: cls.methods,
215
- properties: cls.properties,
216
- file: cls.file,
217
- line: cls.line,
218
- };
219
- }
220
- }
221
- }
222
-
223
- return {
224
- focusFiles,
225
- expanded,
226
- expandable: Object.keys(graph.nodes).filter(k => !expanded[k]),
227
- };
228
- }
229
-
230
- /**
231
- * Expand a symbol to full details
232
- * @param {string} symbol - Minified symbol like 'SN' or 'SN.tP'
233
- * @returns {Promise<Object>}
234
- */
235
- export async function expand(symbol) {
236
- const path = cachedPath || 'src/components';
237
- const graph = await getGraph(path);
238
-
239
- const [nodeKey, methodKey] = symbol.split('.');
240
- const fullName = graph.reverseLegend[nodeKey];
241
-
242
- if (!fullName) {
243
- return { error: `Unknown symbol: ${symbol}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.` };
244
- }
245
-
246
- // Find the source file
247
- const parsed = await parseProject(path);
248
- const cls = parsed.classes.find(c => c.name === fullName);
249
- const fn = parsed.functions.find(f => f.name === fullName);
250
-
251
- if (!cls && !fn) {
252
- return { error: `Symbol not found: ${fullName}` };
253
- }
254
-
255
- if (fn && !methodKey) {
256
- return {
257
- symbol,
258
- fullName,
259
- type: 'function',
260
- file: fn.file,
261
- line: fn.line,
262
- exported: fn.exported,
263
- calls: fn.calls,
264
- };
265
- }
266
-
267
- // If method specified, extract method code
268
- if (methodKey && cls) {
269
- const methodName = graph.reverseLegend[methodKey] || methodKey;
270
- const content = readFileSync(cls.file, 'utf-8');
271
- const methodCode = extractMethod(content, methodName);
272
-
273
- return {
274
- symbol,
275
- fullName: `${fullName}.${methodName}`,
276
- file: cls.file,
277
- line: cls.line, // TODO: get actual method line
278
- code: methodCode,
279
- };
280
- }
281
-
282
- // Return full class info
283
- return {
284
- symbol,
285
- fullName,
286
- file: cls.file,
287
- line: cls.line,
288
- extends: cls.extends,
289
- methods: cls.methods,
290
- properties: cls.properties,
291
- calls: cls.calls,
292
- };
293
- }
294
-
295
- /**
296
- * Get dependency tree for a symbol
297
- * @param {string} symbol
298
- * @returns {Promise<Object>}
299
- */
300
- export async function deps(symbol) {
301
- const path = cachedPath || 'src/components';
302
- const graph = await getGraph(path);
303
-
304
- const node = graph.nodes[symbol];
305
- if (!node) {
306
- return { error: `Unknown symbol: ${symbol}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.` };
307
- }
308
-
309
- // Find incoming edges (usedBy)
310
- const usedBy = graph.edges
311
- .filter(e => e[2].startsWith(symbol))
312
- .map(e => e[0]);
313
-
314
- // Find outgoing edges (calls)
315
- const calls = graph.edges
316
- .filter(e => e[0] === symbol)
317
- .map(e => e[2]);
318
-
319
- return {
320
- symbol,
321
- imports: node.i || [],
322
- usedBy: [...new Set(usedBy)],
323
- calls: [...new Set(calls)],
324
- };
325
- }
326
-
327
- /**
328
- * Find all usages of a symbol
329
- * @param {string} symbol
330
- * @returns {Promise<Array>}
331
- */
332
- export async function usages(symbol) {
333
- const path = cachedPath || 'src/components';
334
- const graph = await getGraph(path);
335
- const parsed = await parseProject(path);
336
-
337
- const fullName = graph.reverseLegend[symbol] || symbol;
338
- const results = [];
339
-
340
- for (const cls of parsed.classes) {
341
- if (cls.calls?.includes(fullName) || cls.calls?.some(c => c.includes(fullName))) {
342
- results.push({
343
- file: cls.file,
344
- line: cls.line,
345
- context: `${cls.name} calls ${fullName}`,
346
- });
347
- }
348
- }
349
-
350
- return results;
351
- }
352
-
353
- /**
354
- * Extract method code from file content
355
- * @param {string} content
356
- * @param {string} methodName
357
- * @returns {string}
358
- */
359
- function extractMethod(content, methodName) {
360
- const regex = new RegExp(`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)(?:async\\s+)?${methodName}\\s*\\([^)]*\\)\\s*{`, 'g');
361
- const match = regex.exec(content);
362
-
363
- if (!match) return '';
364
-
365
- const start = match.index;
366
- let depth = 0;
367
- let i = match.index + match[0].length - 1;
368
-
369
- while (i < content.length) {
370
- if (content[i] === '{') depth++;
371
- else if (content[i] === '}') {
372
- depth--;
373
- if (depth === 0) {
374
- return content.slice(start, i + 1);
375
- }
376
- }
377
- i++;
378
- }
379
-
380
- return content.slice(start);
381
- }
382
-
383
- /**
384
- * Find call chain from one symbol to another
385
- * @param {Object} options
386
- * @param {string} options.from - Starting symbol (full or minified)
387
- * @param {string} options.to - Target symbol (full or minified)
388
- * @param {string} [options.path] - Project path
389
- * @returns {Promise<string[]|Object>}
390
- */
391
- export async function getCallChain(options = {}) {
392
- const { from, to, path } = options;
393
- if (!from || !to) {
394
- return { error: 'Both "from" and "to" parameters are required' };
395
- }
396
-
397
- const projectPath = path || cachedPath || 'src/components';
398
- const graph = await getGraph(projectPath);
399
-
400
- const fromSym = graph.legend[from] || from;
401
- const toSym = graph.legend[to] || to;
402
-
403
- // Build adjacency list for fast lookup
404
- const adj = {};
405
- for (const [caller, _, target] of graph.edges) {
406
- if (!adj[caller]) adj[caller] = [];
407
- adj[caller].push(target);
408
- }
409
-
410
- // Queue stores { current: string, path: string[] }
411
- const queue = [{ current: fromSym, path: [fromSym] }];
412
- const visitedNodes = new Set();
413
- const expandedBases = new Set();
414
- visitedNodes.add(fromSym);
415
-
416
- while (queue.length > 0) {
417
- const { current, path: currentPath } = queue.shift();
418
-
419
- const currentBase = current.split('.')[0];
420
- const currentMethod = current.split('.')[1];
421
-
422
- if (current === toSym || currentBase === toSym || currentMethod === toSym) {
423
- const fullPath = currentPath.map(sym => {
424
- const parts = sym.split('.');
425
- const base = graph.reverseLegend[parts[0]] || parts[0];
426
- if (parts.length === 2) {
427
- const method = graph.reverseLegend[parts[1]] || parts[1];
428
- return `${base}.${method}`;
429
- }
430
- return base;
431
- });
432
- return fullPath;
433
- }
434
-
435
- if (expandedBases.has(currentBase)) {
436
- continue;
437
- }
438
- expandedBases.add(currentBase);
439
-
440
- const neighbors = adj[currentBase] || [];
441
- for (const neighbor of neighbors) {
442
- if (!visitedNodes.has(neighbor)) {
443
- visitedNodes.add(neighbor);
444
- queue.push({
445
- current: neighbor,
446
- path: [...currentPath, neighbor]
447
- });
448
- }
449
- }
450
- }
451
-
452
- return { error: `No call path found from "${from}" to "${to}"` };
453
- }
454
-
455
- /**
456
- * Invalidate cache
457
- */
458
- export function invalidateCache() {
459
- if (cachedPath) {
460
- try {
461
- const cachePath = join(cachedPath, '.project-graph-cache.json');
462
- if (existsSync(cachePath)) {
463
- unlinkSync(cachePath);
464
- }
465
- } catch (e) {}
466
- }
467
- cachedGraph = null;
468
- cachedPath = null;
469
- cachedMtimes.clear();
470
- }
@@ -1,188 +0,0 @@
1
- /**
2
- * Optional Type Checker (tsc wrapper)
3
- * Provides JSDoc type validation via TypeScript compiler
4
- *
5
- * Requires `tsc` in PATH (npm i -g typescript)
6
- * Graceful fallback if not available
7
- */
8
-
9
- import { execSync, spawn } from 'child_process';
10
- import { existsSync } from 'fs';
11
- import { resolve, join } from 'path';
12
-
13
- /**
14
- * @typedef {Object} TypeDiagnostic
15
- * @property {string} file
16
- * @property {number} line
17
- * @property {number} column
18
- * @property {'error'|'warning'} severity
19
- * @property {string} message
20
- * @property {string} code - TS error code (e.g. "TS2345")
21
- */
22
-
23
- /**
24
- * Check if tsc is available
25
- * @returns {{ available: boolean, version: string|null, path: string|null }}
26
- */
27
- function detectTsc() {
28
- try {
29
- const version = execSync('tsc --version', { encoding: 'utf-8', timeout: 5000 }).trim();
30
- const tscPath = execSync('which tsc', { encoding: 'utf-8', timeout: 5000 }).trim();
31
- return { available: true, version, path: tscPath };
32
- } catch (e) {
33
- // Try npx
34
- try {
35
- const version = execSync('npx tsc --version', { encoding: 'utf-8', timeout: 15000 }).trim();
36
- return { available: true, version, path: 'npx tsc' };
37
- } catch (e2) {
38
- return { available: false, version: null, path: null };
39
- }
40
- }
41
- }
42
-
43
- /**
44
- * Parse tsc output line into structured diagnostic
45
- * @param {string} line
46
- * @param {string} baseDir
47
- * @returns {TypeDiagnostic|null}
48
- */
49
- function parseDiagnosticLine(line, baseDir) {
50
- // Format: file.js(line,col): error TS1234: message
51
- const match = line.match(/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/);
52
- if (!match) return null;
53
-
54
- return {
55
- file: match[1],
56
- line: parseInt(match[2]),
57
- column: parseInt(match[3]),
58
- severity: match[4],
59
- message: match[6],
60
- code: match[5],
61
- };
62
- }
63
-
64
- /**
65
- * Build tsc arguments
66
- * @param {string} dir
67
- * @param {Object} options
68
- * @returns {string[]}
69
- */
70
- function buildArgs(dir, options = {}) {
71
- const args = ['--noEmit'];
72
-
73
- // Check for existing config
74
- const tsconfig = join(dir, 'tsconfig.json');
75
- const jsconfig = join(dir, 'jsconfig.json');
76
-
77
- if (existsSync(tsconfig)) {
78
- args.push('--project', tsconfig);
79
- } else if (existsSync(jsconfig)) {
80
- args.push('--project', jsconfig);
81
- } else {
82
- // No config — use sensible defaults for JS projects
83
- args.push('--allowJs', '--checkJs');
84
- args.push('--target', 'ESNext');
85
- args.push('--module', 'NodeNext');
86
- args.push('--moduleResolution', 'NodeNext');
87
- args.push('--skipLibCheck');
88
-
89
- // Include the directory
90
- if (options.files?.length) {
91
- args.push(...options.files);
92
- } else {
93
- args.push('--rootDir', dir);
94
- }
95
- }
96
-
97
- return args;
98
- }
99
-
100
- /**
101
- * Run type checking on a directory
102
- * @param {string} dir - Directory to check
103
- * @param {Object} [options]
104
- * @param {string[]} [options.files] - Specific files to check
105
- * @param {number} [options.maxDiagnostics=50] - Max diagnostics to return
106
- * @returns {Promise<{ available: boolean, version: string|null, diagnostics: TypeDiagnostic[], summary: Object, hint: string|null }>}
107
- */
108
- export async function checkTypes(dir, options = {}) {
109
- const maxDiagnostics = options.maxDiagnostics || 50;
110
- const resolvedDir = resolve(dir);
111
-
112
- // Detect tsc
113
- const tsc = detectTsc();
114
- if (!tsc.available) {
115
- return {
116
- available: false,
117
- version: null,
118
- diagnostics: [],
119
- summary: { total: 0, errors: 0, warnings: 0 },
120
- hint: 'TypeScript not found. Install: npm i -g typescript',
121
- };
122
- }
123
-
124
- // Build and run command
125
- const args = buildArgs(resolvedDir, options);
126
- const cmd = tsc.path.includes('npx') ? 'npx' : 'tsc';
127
- const cmdArgs = tsc.path.includes('npx') ? ['tsc', ...args] : args;
128
-
129
- const result = await new Promise((res) => {
130
- const child = spawn(cmd, cmdArgs, {
131
- cwd: resolvedDir,
132
- stdio: ['ignore', 'pipe', 'pipe'],
133
- });
134
-
135
- let stdout = '';
136
- let stderr = '';
137
- child.stdout.on('data', (d) => { stdout += d; });
138
- child.stderr.on('data', (d) => { stderr += d; });
139
-
140
- const timer = setTimeout(() => {
141
- child.kill('SIGTERM');
142
- res({ stdout, stderr, killed: true });
143
- }, 60000);
144
-
145
- child.on('close', () => {
146
- clearTimeout(timer);
147
- res({ stdout, stderr, killed: false });
148
- });
149
-
150
- child.on('error', (e) => {
151
- clearTimeout(timer);
152
- res({ stdout: '', stderr: e.message, killed: false });
153
- });
154
- });
155
-
156
- // Parse output (tsc exits with 1 on errors, stdout has diagnostics)
157
- const output = (result.stdout || '') + (result.stderr || '');
158
- const lines = output.split('\n').filter(l => l.trim());
159
-
160
- const diagnostics = [];
161
- for (const line of lines) {
162
- const diag = parseDiagnosticLine(line, resolvedDir);
163
- if (diag && diagnostics.length < maxDiagnostics) {
164
- diagnostics.push(diag);
165
- }
166
- }
167
-
168
- const errors = diagnostics.filter(d => d.severity === 'error').length;
169
- const warnings = diagnostics.filter(d => d.severity === 'warning').length;
170
-
171
- const byFile = {};
172
- for (const d of diagnostics) {
173
- byFile[d.file] = (byFile[d.file] || 0) + 1;
174
- }
175
-
176
- return {
177
- available: true,
178
- version: tsc.version,
179
- diagnostics,
180
- summary: {
181
- total: diagnostics.length,
182
- errors,
183
- warnings,
184
- byFile,
185
- },
186
- hint: null,
187
- };
188
- }