typegraph-mcp 0.9.46 → 0.9.48
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/README.md +12 -3
- package/benchmark.ts +12 -3
- package/biome-config.ts +76 -0
- package/check.ts +67 -83
- package/cli.ts +43 -62
- package/commands/check.md +1 -1
- package/dist/benchmark.js +56 -19
- package/dist/check.js +213 -153
- package/dist/cli.js +1373 -1228
- package/dist/module-graph.js +22 -9
- package/dist/server.js +82 -38
- package/dist/smoke-test.js +128 -33
- package/dist/tsserver-client.js +35 -10
- package/module-graph.ts +29 -9
- package/package.json +5 -4
- package/scripts/self-install.ts +78 -0
- package/server.ts +26 -19
- package/smoke-test.ts +118 -17
- package/tsserver-client.ts +49 -11
- package/engine-sync-test.ts +0 -262
- package/export-surface-test.ts +0 -202
- package/install-oxlint-test.ts +0 -116
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ First query takes ~2s (tsserver warmup). Subsequent queries: 1-60ms.
|
|
|
87
87
|
## Requirements
|
|
88
88
|
|
|
89
89
|
- **Node.js** >= 22
|
|
90
|
-
-
|
|
90
|
+
- A project containing TypeScript source files. `tsconfig.json` is supported but optional; without one, semantic tools use an inferred TypeScript project.
|
|
91
91
|
- **npm** for dependency installation
|
|
92
92
|
|
|
93
93
|
## Tools
|
|
@@ -146,9 +146,9 @@ npx typegraph-mcp check
|
|
|
146
146
|
| Symptom | Fix |
|
|
147
147
|
|---|---|
|
|
148
148
|
| Server won't start | `cd plugins/typegraph-mcp && npm install --include=optional` |
|
|
149
|
-
| "
|
|
149
|
+
| "Compatible tsserver not found" | Reinstall the plugin dependencies; TypeGraph uses the project's legacy tsserver when available and its bundled TypeScript 5 runtime for TypeScript 7 projects |
|
|
150
150
|
| Tools return empty results | Check `TYPEGRAPH_TSCONFIG` points to the right tsconfig |
|
|
151
|
-
|
|
|
151
|
+
| Broad project checks include plugins/ | Narrow the check command's input or ignore `plugins/`; setup does not edit `tsconfig.json` |
|
|
152
152
|
| `@esbuild/*` or `@rollup/*` package missing | Reinstall with Node 22: `npm install --include=optional` |
|
|
153
153
|
| "npm warn Unknown project config" | Safe to ignore — caused by pnpm settings in your `.npmrc` that npm doesn't recognize |
|
|
154
154
|
|
|
@@ -224,6 +224,15 @@ nvm use
|
|
|
224
224
|
npm install --include=optional
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
To install the current checkout into itself for Claude Code and Codex CLI:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npm run setup:self
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The wrapper invokes the local `cli.ts`. In a Git worktree it first adds every generated
|
|
234
|
+
plugin, skill, MCP configuration, and agent-instruction path to `.gitignore`.
|
|
235
|
+
|
|
227
236
|
### Run locally against a project
|
|
228
237
|
|
|
229
238
|
```bash
|
package/benchmark.ts
CHANGED
|
@@ -666,8 +666,17 @@ async function benchmarkAccuracy(
|
|
|
666
666
|
|
|
667
667
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
668
668
|
|
|
669
|
-
export async function main(config?: {
|
|
670
|
-
|
|
669
|
+
export async function main(config?: {
|
|
670
|
+
projectRoot: string;
|
|
671
|
+
tsconfigPath: string;
|
|
672
|
+
toolDir?: string;
|
|
673
|
+
toolIsEmbedded?: boolean;
|
|
674
|
+
}) {
|
|
675
|
+
const resolvedConfig = config ?? resolveConfig(import.meta.dirname);
|
|
676
|
+
({ projectRoot, tsconfigPath } = resolvedConfig);
|
|
677
|
+
const excludedPaths = resolvedConfig.toolIsEmbedded && resolvedConfig.toolDir
|
|
678
|
+
? [resolvedConfig.toolDir]
|
|
679
|
+
: [];
|
|
671
680
|
console.log("");
|
|
672
681
|
console.log("typegraph-mcp Benchmark");
|
|
673
682
|
console.log("=======================");
|
|
@@ -676,7 +685,7 @@ export async function main(config?: { projectRoot: string; tsconfigPath: string
|
|
|
676
685
|
|
|
677
686
|
// Build graph
|
|
678
687
|
const graphStart = performance.now();
|
|
679
|
-
const { graph } = await buildGraph(projectRoot, tsconfigPath);
|
|
688
|
+
const { graph } = await buildGraph(projectRoot, tsconfigPath, excludedPaths);
|
|
680
689
|
const graphMs = performance.now() - graphStart;
|
|
681
690
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
682
691
|
console.log(`Module graph: ${graph.files.size} files, ${edgeCount} edges [${graphMs.toFixed(0)}ms]`);
|
package/biome-config.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const BIOME_CONFIG_NAMES = [
|
|
2
|
+
"biome.json",
|
|
3
|
+
"biome.jsonc",
|
|
4
|
+
".biome.json",
|
|
5
|
+
".biome.jsonc",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function filesObjectMatch(raw: string): RegExpMatchArray | null {
|
|
9
|
+
return raw.match(/(["']files["']\s*:\s*\{)([\s\S]*?)(\n?\s*\})/);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function filesIncludes(raw: string): string[] | null {
|
|
13
|
+
const filesObject = filesObjectMatch(raw);
|
|
14
|
+
if (!filesObject) return null;
|
|
15
|
+
const includes = filesObject[2].match(/["']includes["']\s*:\s*\[([\s\S]*?)\]/);
|
|
16
|
+
if (!includes) return null;
|
|
17
|
+
return [...includes[1].matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function biomeScopeExcludes(raw: string, parentDir: string): boolean {
|
|
21
|
+
const escaped = parentDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
const explicitIgnore = new RegExp(
|
|
23
|
+
`["']!{1,2}(?:\\*\\*/)?${escaped}(?:/\\*\\*)?["']`
|
|
24
|
+
);
|
|
25
|
+
if (explicitIgnore.test(raw)) return true;
|
|
26
|
+
|
|
27
|
+
const includes = filesIncludes(raw);
|
|
28
|
+
if (!includes) return false;
|
|
29
|
+
const positivePatterns = includes.filter((pattern) => !pattern.startsWith("!"));
|
|
30
|
+
|
|
31
|
+
return !positivePatterns.some((pattern) => {
|
|
32
|
+
const normalized = pattern.replace(/^\.\//, "");
|
|
33
|
+
return (
|
|
34
|
+
normalized === "*" ||
|
|
35
|
+
normalized === "**" ||
|
|
36
|
+
normalized.startsWith("**/") ||
|
|
37
|
+
normalized.startsWith("*/") ||
|
|
38
|
+
normalized === parentDir ||
|
|
39
|
+
normalized.startsWith(`${parentDir}/`)
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function appendToIncludes(body: string, forceIgnore: string): string | null {
|
|
45
|
+
const pattern = /(["']includes["']\s*:\s*\[)([\s\S]*?)(\])/;
|
|
46
|
+
if (!pattern.test(body)) return null;
|
|
47
|
+
return body.replace(pattern, (_match, open, items, close) => {
|
|
48
|
+
const trimmed = items.trimEnd();
|
|
49
|
+
const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
|
|
50
|
+
return `${open}${trimmed}${needsComma ? "," : ""} ${forceIgnore}${close}`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function patchBiomeConfig(raw: string, parentDir: string): string | null {
|
|
55
|
+
const forceIgnore = `"!!${parentDir}"`;
|
|
56
|
+
const filesObjectRe = /(["']files["']\s*:\s*\{)([\s\S]*?)(\n?\s*\})/;
|
|
57
|
+
const filesObject = filesObjectMatch(raw);
|
|
58
|
+
|
|
59
|
+
if (filesObject) {
|
|
60
|
+
const updatedBody = appendToIncludes(filesObject[2], forceIgnore);
|
|
61
|
+
if (updatedBody) {
|
|
62
|
+
return raw.replace(filesObjectRe, `${filesObject[1]}${updatedBody}${filesObject[3]}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = filesObject[2].trimEnd();
|
|
66
|
+
const needsComma = body.trim().length > 0 && !body.trimEnd().endsWith(",");
|
|
67
|
+
const updatedFiles = `${filesObject[1]}${body}${needsComma ? "," : ""}\n "includes": ["**", ${forceIgnore}]${filesObject[3]}`;
|
|
68
|
+
return raw.replace(filesObjectRe, updatedFiles);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lastBrace = raw.lastIndexOf("}");
|
|
72
|
+
if (lastBrace === -1) return null;
|
|
73
|
+
const before = raw.slice(0, lastBrace).trimEnd();
|
|
74
|
+
const needsComma = !before.endsWith(",") && !before.endsWith("{");
|
|
75
|
+
return `${before}${needsComma ? "," : ""}\n "files": { "includes": ["**", ${forceIgnore}] }\n}\n`;
|
|
76
|
+
}
|
package/check.ts
CHANGED
|
@@ -14,6 +14,8 @@ import * as path from "node:path";
|
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
16
|
import { resolveConfig, type TypegraphConfig } from "./config.js";
|
|
17
|
+
import { resolveTsServer, type TsServerResolution } from "./tsserver-client.js";
|
|
18
|
+
import { BIOME_CONFIG_NAMES, biomeScopeExcludes } from "./biome-config.js";
|
|
17
19
|
|
|
18
20
|
// ─── Result Type ─────────────────────────────────────────────────────────────
|
|
19
21
|
|
|
@@ -26,7 +28,7 @@ export interface CheckResult {
|
|
|
26
28
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
27
29
|
|
|
28
30
|
/** Find first .ts file in the project (for resolver smoke test) */
|
|
29
|
-
function findFirstTsFile(dir: string): string | null {
|
|
31
|
+
function findFirstTsFile(dir: string, excludedPaths: string[] = []): string | null {
|
|
30
32
|
const skipDirs = new Set(["node_modules", "dist", ".git", ".wrangler", "coverage"]);
|
|
31
33
|
try {
|
|
32
34
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -36,7 +38,14 @@ function findFirstTsFile(dir: string): string | null {
|
|
|
36
38
|
}
|
|
37
39
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
40
|
if (entry.isDirectory() && !skipDirs.has(entry.name) && !entry.name.startsWith(".")) {
|
|
39
|
-
const
|
|
41
|
+
const childDir = path.join(dir, entry.name);
|
|
42
|
+
if (
|
|
43
|
+
excludedPaths.some(
|
|
44
|
+
(excludedPath) =>
|
|
45
|
+
childDir === excludedPath || childDir.startsWith(excludedPath + path.sep)
|
|
46
|
+
)
|
|
47
|
+
) continue;
|
|
48
|
+
const found = findFirstTsFile(childDir, excludedPaths);
|
|
40
49
|
if (found) return found;
|
|
41
50
|
}
|
|
42
51
|
}
|
|
@@ -47,18 +56,9 @@ function findFirstTsFile(dir: string): string | null {
|
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/** Spawn tsserver, send configure, verify response, shut down */
|
|
50
|
-
function testTsserver(projectRoot: string): Promise<boolean> {
|
|
59
|
+
function testTsserver(projectRoot: string, resolution: TsServerResolution): Promise<boolean> {
|
|
51
60
|
return new Promise((resolve) => {
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const require = createRequire(path.resolve(projectRoot, "package.json"));
|
|
55
|
-
tsserverPath = require.resolve("typescript/lib/tsserver.js");
|
|
56
|
-
} catch {
|
|
57
|
-
resolve(false);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
61
|
+
const child = spawn("node", [resolution.path, "--disableAutomaticTypingAcquisition"], {
|
|
62
62
|
cwd: projectRoot,
|
|
63
63
|
stdio: ["pipe", "pipe", "pipe"],
|
|
64
64
|
});
|
|
@@ -228,45 +228,6 @@ function findAntigravityRegistration(projectRoot: string): string | null {
|
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
function readProjectPackageJson(projectRoot: string): Record<string, unknown> | null {
|
|
232
|
-
const packageJsonPath = path.resolve(projectRoot, "package.json");
|
|
233
|
-
if (!fs.existsSync(packageJsonPath)) return null;
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as Record<string, unknown>;
|
|
237
|
-
} catch {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function getProjectInstallCommand(projectRoot: string, packageJson: Record<string, unknown> | null): string {
|
|
243
|
-
const packageManager = typeof packageJson?.["packageManager"] === "string"
|
|
244
|
-
? packageJson["packageManager"]
|
|
245
|
-
: "";
|
|
246
|
-
|
|
247
|
-
if (packageManager.startsWith("pnpm@") || fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
248
|
-
return "pnpm install";
|
|
249
|
-
}
|
|
250
|
-
if (packageManager.startsWith("yarn@") || fs.existsSync(path.join(projectRoot, "yarn.lock"))) {
|
|
251
|
-
return "yarn install";
|
|
252
|
-
}
|
|
253
|
-
return "npm install";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function hasDeclaredDependency(packageJson: Record<string, unknown> | null, packageName: string): boolean {
|
|
257
|
-
const depKeys = [
|
|
258
|
-
"dependencies",
|
|
259
|
-
"devDependencies",
|
|
260
|
-
"peerDependencies",
|
|
261
|
-
"optionalDependencies",
|
|
262
|
-
] as const;
|
|
263
|
-
|
|
264
|
-
return depKeys.some((key) => {
|
|
265
|
-
const deps = packageJson?.[key];
|
|
266
|
-
return typeof deps === "object" && deps !== null && packageName in deps;
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
231
|
const ESLINT_CONFIG_NAMES = [
|
|
271
232
|
"eslint.config.mjs",
|
|
272
233
|
"eslint.config.js",
|
|
@@ -283,7 +244,8 @@ const OXLINT_CONFIG_NAMES = [
|
|
|
283
244
|
|
|
284
245
|
type LintConfigCheck =
|
|
285
246
|
| { tool: "ESLint"; fileName: string; fullPath: string; propertyName: "ignores" }
|
|
286
|
-
| { tool: "Oxlint"; fileName: string; fullPath: string; propertyName: "ignorePatterns" }
|
|
247
|
+
| { tool: "Oxlint"; fileName: string; fullPath: string; propertyName: "ignorePatterns" }
|
|
248
|
+
| { tool: "Biome"; fileName: string; fullPath: string; propertyName: "files.includes" };
|
|
287
249
|
|
|
288
250
|
function findLintConfigs(projectRoot: string): LintConfigCheck[] {
|
|
289
251
|
const configs: LintConfigCheck[] = [];
|
|
@@ -302,6 +264,14 @@ function findLintConfigs(projectRoot: string): LintConfigCheck[] {
|
|
|
302
264
|
}
|
|
303
265
|
}
|
|
304
266
|
|
|
267
|
+
for (const fileName of BIOME_CONFIG_NAMES) {
|
|
268
|
+
const fullPath = path.resolve(projectRoot, fileName);
|
|
269
|
+
if (fs.existsSync(fullPath)) {
|
|
270
|
+
configs.push({ tool: "Biome", fileName, fullPath, propertyName: "files.includes" });
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
305
275
|
return configs;
|
|
306
276
|
}
|
|
307
277
|
|
|
@@ -310,12 +280,11 @@ function findLintConfigs(projectRoot: string): LintConfigCheck[] {
|
|
|
310
280
|
export async function main(configOverride?: TypegraphConfig): Promise<CheckResult> {
|
|
311
281
|
const { projectRoot, tsconfigPath, toolDir, toolIsEmbedded, toolRelPath } =
|
|
312
282
|
configOverride ?? resolveConfig(import.meta.dirname);
|
|
283
|
+
const excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
313
284
|
|
|
314
285
|
let passed = 0;
|
|
315
286
|
let failed = 0;
|
|
316
287
|
let warned = 0;
|
|
317
|
-
const projectPackageJson = readProjectPackageJson(projectRoot);
|
|
318
|
-
const installCommand = getProjectInstallCommand(projectRoot, projectPackageJson);
|
|
319
288
|
|
|
320
289
|
function pass(msg: string): void {
|
|
321
290
|
console.log(` \u2713 ${msg}`);
|
|
@@ -363,33 +332,32 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
363
332
|
pass("tsx available (via npx/global)");
|
|
364
333
|
}
|
|
365
334
|
|
|
366
|
-
// 3.
|
|
367
|
-
let
|
|
335
|
+
// 3. Compatible tsserver runtime
|
|
336
|
+
let tsResolution: TsServerResolution | null = null;
|
|
368
337
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
338
|
+
tsResolution = resolveTsServer(projectRoot);
|
|
339
|
+
if (tsResolution.source === "project") {
|
|
340
|
+
pass(`TypeScript found (v${tsResolution.version})`);
|
|
341
|
+
} else if (tsResolution.projectVersion) {
|
|
342
|
+
pass(
|
|
343
|
+
`Compatible tsserver found (TypeScript v${tsResolution.version} tool runtime; project uses v${tsResolution.projectVersion})`
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
pass(`TypeScript found in typegraph-mcp (v${tsResolution.version})`);
|
|
347
|
+
}
|
|
375
348
|
} catch {
|
|
376
|
-
const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
|
|
377
349
|
fail(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
: "TypeScript not found in project",
|
|
381
|
-
hasDeclaredTs
|
|
382
|
-
? `Run \`${installCommand}\` to install project dependencies`
|
|
383
|
-
: `Add \`typescript\` to devDependencies and run \`${installCommand}\``
|
|
350
|
+
"Compatible tsserver runtime not found",
|
|
351
|
+
`Run \`cd ${toolRelPath} && npm install --include=optional\``
|
|
384
352
|
);
|
|
385
353
|
}
|
|
386
354
|
|
|
387
|
-
// 4.
|
|
355
|
+
// 4. TypeScript project mode
|
|
388
356
|
const tsconfigAbs = path.resolve(projectRoot, tsconfigPath);
|
|
389
357
|
if (fs.existsSync(tsconfigAbs)) {
|
|
390
358
|
pass(`tsconfig.json exists at ${tsconfigPath}`);
|
|
391
359
|
} else {
|
|
392
|
-
|
|
360
|
+
pass("No tsconfig.json; semantic tools will use an inferred TypeScript project");
|
|
393
361
|
}
|
|
394
362
|
|
|
395
363
|
// 5. MCP registration
|
|
@@ -501,7 +469,13 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
501
469
|
// 6. typegraph-mcp dependencies installed
|
|
502
470
|
const toolNodeModules = path.join(toolDir, "node_modules");
|
|
503
471
|
if (fs.existsSync(toolNodeModules)) {
|
|
504
|
-
const requiredPkgs = [
|
|
472
|
+
const requiredPkgs = [
|
|
473
|
+
"@modelcontextprotocol/sdk",
|
|
474
|
+
"oxc-parser",
|
|
475
|
+
"oxc-resolver",
|
|
476
|
+
"typescript",
|
|
477
|
+
"zod",
|
|
478
|
+
];
|
|
505
479
|
const missing = requiredPkgs.filter(
|
|
506
480
|
(pkg) => !fs.existsSync(path.join(toolNodeModules, ...pkg.split("/")))
|
|
507
481
|
);
|
|
@@ -539,13 +513,15 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
539
513
|
const oxcResolverReq = createRequire(path.join(toolDir, "package.json"));
|
|
540
514
|
const { ResolverFactory } = await import(oxcResolverReq.resolve("oxc-resolver"));
|
|
541
515
|
const resolver = new ResolverFactory({
|
|
542
|
-
|
|
516
|
+
...(fs.existsSync(tsconfigAbs)
|
|
517
|
+
? { tsconfig: { configFile: tsconfigAbs, references: "auto" as const } }
|
|
518
|
+
: {}),
|
|
543
519
|
extensions: [".ts", ".tsx", ".js"],
|
|
544
520
|
extensionAlias: { ".js": [".ts", ".tsx", ".js"] },
|
|
545
521
|
});
|
|
546
522
|
// Find any .ts file in the project to test resolution
|
|
547
523
|
let resolveOk = false;
|
|
548
|
-
const testFile = findFirstTsFile(projectRoot);
|
|
524
|
+
const testFile = findFirstTsFile(projectRoot, excludedPaths);
|
|
549
525
|
if (testFile) {
|
|
550
526
|
const dir = path.dirname(testFile);
|
|
551
527
|
const base = "./" + path.basename(testFile);
|
|
@@ -569,9 +545,9 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
569
545
|
}
|
|
570
546
|
|
|
571
547
|
// 9. tsserver startup test
|
|
572
|
-
if (
|
|
548
|
+
if (tsResolution) {
|
|
573
549
|
try {
|
|
574
|
-
const ok = await testTsserver(projectRoot);
|
|
550
|
+
const ok = await testTsserver(projectRoot, tsResolution);
|
|
575
551
|
if (ok) {
|
|
576
552
|
pass("tsserver responds to configure");
|
|
577
553
|
} else {
|
|
@@ -592,7 +568,7 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
592
568
|
|
|
593
569
|
// 10. Module graph build test
|
|
594
570
|
try {
|
|
595
|
-
let buildGraph: (root: string, tsconfig: string) => Promise<{ graph: { files: Set<string>; forward: Map<string, unknown[]> } }>;
|
|
571
|
+
let buildGraph: (root: string, tsconfig: string, excluded?: string[]) => Promise<{ graph: { files: Set<string>; forward: Map<string, unknown[]> } }>;
|
|
596
572
|
try {
|
|
597
573
|
({ buildGraph } = await import(path.resolve(toolDir, "module-graph.js")));
|
|
598
574
|
} catch {
|
|
@@ -600,7 +576,11 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
600
576
|
({ buildGraph } = await import("./module-graph.js"));
|
|
601
577
|
}
|
|
602
578
|
const start = performance.now();
|
|
603
|
-
const { graph } = await buildGraph(
|
|
579
|
+
const { graph } = await buildGraph(
|
|
580
|
+
projectRoot,
|
|
581
|
+
tsconfigPath,
|
|
582
|
+
excludedPaths
|
|
583
|
+
);
|
|
604
584
|
const elapsed = (performance.now() - start).toFixed(0);
|
|
605
585
|
const edgeCount = [...graph.forward.values()].reduce(
|
|
606
586
|
(s: number, e: unknown[]) => s + e.length,
|
|
@@ -636,19 +616,23 @@ export async function main(configOverride?: TypegraphConfig): Promise<CheckResul
|
|
|
636
616
|
|
|
637
617
|
for (const config of lintConfigs) {
|
|
638
618
|
const content = fs.readFileSync(config.fullPath, "utf-8");
|
|
639
|
-
const hasParentIgnore =
|
|
619
|
+
const hasParentIgnore =
|
|
620
|
+
config.tool === "Biome"
|
|
621
|
+
? biomeScopeExcludes(content, parentDir)
|
|
622
|
+
: parentIgnorePattern.test(content);
|
|
640
623
|
|
|
641
624
|
if (hasParentIgnore) {
|
|
642
625
|
pass(`${config.tool} ignores ${parentDir}/ (${config.fileName})`);
|
|
643
626
|
} else {
|
|
627
|
+
const expectedPattern = config.tool === "Biome" ? `!!${parentDir}` : `${parentDir}/**`;
|
|
644
628
|
fail(
|
|
645
|
-
`${config.tool} missing ignore: "${
|
|
646
|
-
`Add to ${config.propertyName} in ${config.fileName}:\n "${
|
|
629
|
+
`${config.tool} missing ignore: "${expectedPattern}" (${config.fileName})`,
|
|
630
|
+
`Add to ${config.propertyName} in ${config.fileName}:\n "${expectedPattern}",`
|
|
647
631
|
);
|
|
648
632
|
}
|
|
649
633
|
}
|
|
650
634
|
} else {
|
|
651
|
-
skip("Lint config check (no ESLint or
|
|
635
|
+
skip("Lint config check (no ESLint, Oxlint, or Biome config found)");
|
|
652
636
|
}
|
|
653
637
|
} else {
|
|
654
638
|
skip("Lint config check (typegraph-mcp is external to project)");
|
package/cli.ts
CHANGED
|
@@ -18,6 +18,7 @@ import * as fs from "node:fs";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { execSync } from "node:child_process";
|
|
20
20
|
import * as p from "@clack/prompts";
|
|
21
|
+
import { BIOME_CONFIG_NAMES, biomeScopeExcludes, patchBiomeConfig } from "./biome-config.js";
|
|
21
22
|
import { resolveConfig } from "./config.js";
|
|
22
23
|
|
|
23
24
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -141,6 +142,7 @@ const AGENTS: Record<AgentId, AgentDef> = {
|
|
|
141
142
|
/** Core files always installed (server, modules, config, package manifest) */
|
|
142
143
|
const CORE_FILES = [
|
|
143
144
|
"server.ts",
|
|
145
|
+
"biome-config.ts",
|
|
144
146
|
"module-graph.ts",
|
|
145
147
|
"tsserver-client.ts",
|
|
146
148
|
"graph-queries.ts",
|
|
@@ -700,49 +702,6 @@ function deregisterAntigravityMcp(projectRoot: string): void {
|
|
|
700
702
|
}
|
|
701
703
|
}
|
|
702
704
|
|
|
703
|
-
// ─── TSConfig Exclude ─────────────────────────────────────────────────────────
|
|
704
|
-
|
|
705
|
-
function ensureTsconfigExclude(projectRoot: string): void {
|
|
706
|
-
const tsconfigPath = path.resolve(projectRoot, "tsconfig.json");
|
|
707
|
-
if (!fs.existsSync(tsconfigPath)) return;
|
|
708
|
-
|
|
709
|
-
try {
|
|
710
|
-
const raw = fs.readFileSync(tsconfigPath, "utf-8");
|
|
711
|
-
const excludeArrayMatch = raw.match(/("exclude"\s*:\s*\[)([\s\S]*?)(\])/);
|
|
712
|
-
if (excludeArrayMatch && /["']plugins(?:\/\*\*|\/\*|)["']/.test(excludeArrayMatch[2])) {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Insert "plugins/**" into the exclude array in the original file
|
|
717
|
-
if (excludeArrayMatch) {
|
|
718
|
-
// Existing exclude array — append to it
|
|
719
|
-
const updated = raw.replace(
|
|
720
|
-
/("exclude"\s*:\s*\[)([\s\S]*?)(\])/,
|
|
721
|
-
(_match, open, items, close) => {
|
|
722
|
-
const trimmed = items.trimEnd();
|
|
723
|
-
const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
|
|
724
|
-
return `${open}${items.trimEnd()}${needsComma ? "," : ""}\n "plugins/**"${close}`;
|
|
725
|
-
}
|
|
726
|
-
);
|
|
727
|
-
fs.writeFileSync(tsconfigPath, updated);
|
|
728
|
-
} else {
|
|
729
|
-
// No exclude field — add one before the closing brace
|
|
730
|
-
const lastBrace = raw.lastIndexOf("}");
|
|
731
|
-
if (lastBrace !== -1) {
|
|
732
|
-
const before = raw.slice(0, lastBrace).trimEnd();
|
|
733
|
-
const needsComma = !before.endsWith(",") && !before.endsWith("{");
|
|
734
|
-
const patched = `${before}${needsComma ? "," : ""}\n "exclude": ["plugins/**"]\n}\n`;
|
|
735
|
-
fs.writeFileSync(tsconfigPath, patched);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
p.log.success('Added "plugins/**" to tsconfig.json exclude (prevents build errors)');
|
|
740
|
-
} catch {
|
|
741
|
-
// Don't fail setup over tsconfig parsing issues
|
|
742
|
-
p.log.warn('Could not update tsconfig.json — manually add "plugins/**" to the exclude array to prevent build errors');
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
705
|
// ─── Lint Ignore ─────────────────────────────────────────────────────────────
|
|
747
706
|
|
|
748
707
|
const ESLINT_CONFIG_NAMES = [
|
|
@@ -761,7 +720,8 @@ const OXLINT_CONFIG_NAMES = [
|
|
|
761
720
|
|
|
762
721
|
type LintConfig =
|
|
763
722
|
| { tool: "ESLint"; fileName: string; fullPath: string; format: "flat" }
|
|
764
|
-
| { tool: "Oxlint"; fileName: string; fullPath: string; format: "json" | "module" }
|
|
723
|
+
| { tool: "Oxlint"; fileName: string; fullPath: string; format: "json" | "module" }
|
|
724
|
+
| { tool: "Biome"; fileName: string; fullPath: string; format: "json" };
|
|
765
725
|
|
|
766
726
|
function findLintConfigs(projectRoot: string): LintConfig[] {
|
|
767
727
|
const configs: LintConfig[] = [];
|
|
@@ -785,6 +745,14 @@ function findLintConfigs(projectRoot: string): LintConfig[] {
|
|
|
785
745
|
}
|
|
786
746
|
}
|
|
787
747
|
|
|
748
|
+
for (const fileName of BIOME_CONFIG_NAMES) {
|
|
749
|
+
const fullPath = path.resolve(projectRoot, fileName);
|
|
750
|
+
if (fs.existsSync(fullPath)) {
|
|
751
|
+
configs.push({ tool: "Biome", fileName, fullPath, format: "json" });
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
788
756
|
return configs;
|
|
789
757
|
}
|
|
790
758
|
|
|
@@ -862,34 +830,51 @@ function patchOxlintModuleConfig(raw: string): string | null {
|
|
|
862
830
|
return null;
|
|
863
831
|
}
|
|
864
832
|
|
|
833
|
+
function lintPropertyName(config: LintConfig): string {
|
|
834
|
+
if (config.tool === "ESLint") return "ignores";
|
|
835
|
+
if (config.tool === "Oxlint") return "ignorePatterns";
|
|
836
|
+
return "files.includes";
|
|
837
|
+
}
|
|
838
|
+
|
|
865
839
|
function ensureLintIgnores(projectRoot: string): void {
|
|
866
840
|
const configs = findLintConfigs(projectRoot);
|
|
867
841
|
for (const config of configs) {
|
|
868
842
|
try {
|
|
869
843
|
const raw = fs.readFileSync(config.fullPath, "utf-8");
|
|
870
|
-
if (
|
|
844
|
+
if (
|
|
845
|
+
config.tool === "Biome"
|
|
846
|
+
? biomeScopeExcludes(raw, "plugins")
|
|
847
|
+
: /["']plugins\/\*\*["']/.test(raw)
|
|
848
|
+
) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
871
851
|
|
|
872
852
|
const updated =
|
|
873
853
|
config.tool === "ESLint"
|
|
874
854
|
? patchEslintConfig(raw)
|
|
855
|
+
: config.tool === "Biome"
|
|
856
|
+
? patchBiomeConfig(raw, "plugins")
|
|
875
857
|
: config.format === "json"
|
|
876
858
|
? patchOxlintJsonConfig(raw)
|
|
877
859
|
: patchOxlintModuleConfig(raw);
|
|
878
860
|
|
|
879
861
|
if (updated) {
|
|
880
862
|
fs.writeFileSync(config.fullPath, updated);
|
|
881
|
-
const propertyName = config
|
|
882
|
-
|
|
863
|
+
const propertyName = lintPropertyName(config);
|
|
864
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
865
|
+
p.log.success(`Added "${ignorePattern}" to ${config.fileName} ${propertyName}`);
|
|
883
866
|
} else {
|
|
884
|
-
const propertyName = config
|
|
867
|
+
const propertyName = lintPropertyName(config);
|
|
868
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
885
869
|
p.log.warn(
|
|
886
|
-
`Could not patch ${config.fileName} — manually add "
|
|
870
|
+
`Could not patch ${config.fileName} — manually add "${ignorePattern}" to ${propertyName}`
|
|
887
871
|
);
|
|
888
872
|
}
|
|
889
873
|
} catch {
|
|
890
|
-
const propertyName = config
|
|
874
|
+
const propertyName = lintPropertyName(config);
|
|
875
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
891
876
|
p.log.warn(
|
|
892
|
-
`Could not update ${config.fileName} — manually add "
|
|
877
|
+
`Could not update ${config.fileName} — manually add "${ignorePattern}" to ${propertyName}`
|
|
893
878
|
);
|
|
894
879
|
}
|
|
895
880
|
}
|
|
@@ -957,13 +942,13 @@ async function setup(yes: boolean): Promise<void> {
|
|
|
957
942
|
process.exit(1);
|
|
958
943
|
}
|
|
959
944
|
|
|
960
|
-
if (
|
|
961
|
-
p.
|
|
962
|
-
|
|
945
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
946
|
+
p.log.success("Found package.json and tsconfig.json");
|
|
947
|
+
} else {
|
|
948
|
+
p.log.success("Found package.json");
|
|
949
|
+
p.log.info("No tsconfig.json found; semantic tools will use an inferred TypeScript project");
|
|
963
950
|
}
|
|
964
951
|
|
|
965
|
-
p.log.success("Found package.json and tsconfig.json");
|
|
966
|
-
|
|
967
952
|
// 2. Check for existing installation
|
|
968
953
|
const targetDir = path.resolve(projectRoot, PLUGIN_DIR_NAME);
|
|
969
954
|
const isUpdate = fs.existsSync(targetDir);
|
|
@@ -1125,13 +1110,10 @@ async function setup(yes: boolean): Promise<void> {
|
|
|
1125
1110
|
// 7. Register MCP server in agent-specific configs
|
|
1126
1111
|
registerMcpServers(projectRoot, selectedAgents);
|
|
1127
1112
|
|
|
1128
|
-
// 8. Ensure plugins/ is
|
|
1129
|
-
ensureTsconfigExclude(projectRoot);
|
|
1130
|
-
|
|
1131
|
-
// 9. Ensure plugins/ is ignored by supported lint configs
|
|
1113
|
+
// 8. Ensure plugins/ is ignored by supported lint configs
|
|
1132
1114
|
ensureLintIgnores(projectRoot);
|
|
1133
1115
|
|
|
1134
|
-
//
|
|
1116
|
+
// 9. Verification
|
|
1135
1117
|
await runVerification(targetDir, selectedAgents);
|
|
1136
1118
|
}
|
|
1137
1119
|
|
|
@@ -1383,7 +1365,6 @@ const args = process.argv.slice(2);
|
|
|
1383
1365
|
const command = args.find((a) => !a.startsWith("-"));
|
|
1384
1366
|
const yes = args.includes("--yes") || args.includes("-y");
|
|
1385
1367
|
const help = args.includes("--help") || args.includes("-h");
|
|
1386
|
-
|
|
1387
1368
|
// Clear npx download noise (warnings, "Ok to proceed?" prompt)
|
|
1388
1369
|
process.stdout.write("\x1Bc");
|
|
1389
1370
|
|
package/commands/check.md
CHANGED
|
@@ -20,4 +20,4 @@ Run health checks to verify typegraph-mcp is correctly set up for this project.
|
|
|
20
20
|
- For any failures, highlight the issue and the suggested fix
|
|
21
21
|
- If all checks pass, confirm typegraph-mcp is ready
|
|
22
22
|
|
|
23
|
-
3. The check verifies: Node.js version, tsx availability, TypeScript
|
|
23
|
+
3. The check verifies: Node.js version, tsx availability, compatible TypeScript runtime, tsconfig.json, MCP registration, dependencies, oxc-parser, oxc-resolver, tsserver, module graph, ESLint/Oxlint/Biome ignores, and .gitignore configuration.
|