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/dist/cli.js
CHANGED
|
@@ -9,1232 +9,1326 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// biome-config.ts
|
|
13
|
+
function filesObjectMatch(raw) {
|
|
14
|
+
return raw.match(/(["']files["']\s*:\s*\{)([\s\S]*?)(\n?\s*\})/);
|
|
15
|
+
}
|
|
16
|
+
function filesIncludes(raw) {
|
|
17
|
+
const filesObject = filesObjectMatch(raw);
|
|
18
|
+
if (!filesObject) return null;
|
|
19
|
+
const includes = filesObject[2].match(/["']includes["']\s*:\s*\[([\s\S]*?)\]/);
|
|
20
|
+
if (!includes) return null;
|
|
21
|
+
return [...includes[1].matchAll(/["']([^"']+)["']/g)].map((match) => match[1]);
|
|
22
|
+
}
|
|
23
|
+
function biomeScopeExcludes(raw, parentDir) {
|
|
24
|
+
const escaped = parentDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
const explicitIgnore = new RegExp(
|
|
26
|
+
`["']!{1,2}(?:\\*\\*/)?${escaped}(?:/\\*\\*)?["']`
|
|
27
|
+
);
|
|
28
|
+
if (explicitIgnore.test(raw)) return true;
|
|
29
|
+
const includes = filesIncludes(raw);
|
|
30
|
+
if (!includes) return false;
|
|
31
|
+
const positivePatterns = includes.filter((pattern) => !pattern.startsWith("!"));
|
|
32
|
+
return !positivePatterns.some((pattern) => {
|
|
33
|
+
const normalized = pattern.replace(/^\.\//, "");
|
|
34
|
+
return normalized === "*" || normalized === "**" || normalized.startsWith("**/") || normalized.startsWith("*/") || normalized === parentDir || normalized.startsWith(`${parentDir}/`);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function appendToIncludes(body, forceIgnore) {
|
|
38
|
+
const pattern = /(["']includes["']\s*:\s*\[)([\s\S]*?)(\])/;
|
|
39
|
+
if (!pattern.test(body)) return null;
|
|
40
|
+
return body.replace(pattern, (_match, open, items, close) => {
|
|
41
|
+
const trimmed = items.trimEnd();
|
|
42
|
+
const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
|
|
43
|
+
return `${open}${trimmed}${needsComma ? "," : ""} ${forceIgnore}${close}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function patchBiomeConfig(raw, parentDir) {
|
|
47
|
+
const forceIgnore = `"!!${parentDir}"`;
|
|
48
|
+
const filesObjectRe = /(["']files["']\s*:\s*\{)([\s\S]*?)(\n?\s*\})/;
|
|
49
|
+
const filesObject = filesObjectMatch(raw);
|
|
50
|
+
if (filesObject) {
|
|
51
|
+
const updatedBody = appendToIncludes(filesObject[2], forceIgnore);
|
|
52
|
+
if (updatedBody) {
|
|
53
|
+
return raw.replace(filesObjectRe, `${filesObject[1]}${updatedBody}${filesObject[3]}`);
|
|
54
|
+
}
|
|
55
|
+
const body = filesObject[2].trimEnd();
|
|
56
|
+
const needsComma2 = body.trim().length > 0 && !body.trimEnd().endsWith(",");
|
|
57
|
+
const updatedFiles = `${filesObject[1]}${body}${needsComma2 ? "," : ""}
|
|
58
|
+
"includes": ["**", ${forceIgnore}]${filesObject[3]}`;
|
|
59
|
+
return raw.replace(filesObjectRe, updatedFiles);
|
|
60
|
+
}
|
|
61
|
+
const lastBrace = raw.lastIndexOf("}");
|
|
62
|
+
if (lastBrace === -1) return null;
|
|
63
|
+
const before = raw.slice(0, lastBrace).trimEnd();
|
|
64
|
+
const needsComma = !before.endsWith(",") && !before.endsWith("{");
|
|
65
|
+
return `${before}${needsComma ? "," : ""}
|
|
66
|
+
"files": { "includes": ["**", ${forceIgnore}] }
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
var BIOME_CONFIG_NAMES;
|
|
71
|
+
var init_biome_config = __esm({
|
|
72
|
+
"biome-config.ts"() {
|
|
73
|
+
BIOME_CONFIG_NAMES = [
|
|
74
|
+
"biome.json",
|
|
75
|
+
"biome.jsonc",
|
|
76
|
+
".biome.json",
|
|
77
|
+
".biome.jsonc"
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
12
82
|
// config.ts
|
|
13
83
|
import * as path from "path";
|
|
14
|
-
function resolveConfig(
|
|
84
|
+
function resolveConfig(toolDir2) {
|
|
15
85
|
const cwd = process.cwd();
|
|
16
|
-
const projectRoot3 = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path.basename(path.dirname(
|
|
86
|
+
const projectRoot3 = process.env["TYPEGRAPH_PROJECT_ROOT"] ? path.resolve(cwd, process.env["TYPEGRAPH_PROJECT_ROOT"]) : path.basename(path.dirname(toolDir2)) === "plugins" ? path.resolve(toolDir2, "../..") : cwd;
|
|
17
87
|
const tsconfigPath3 = process.env["TYPEGRAPH_TSCONFIG"] || "./tsconfig.json";
|
|
18
|
-
const
|
|
19
|
-
const toolRelPath =
|
|
20
|
-
return { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir, toolIsEmbedded, toolRelPath };
|
|
88
|
+
const toolIsEmbedded2 = toolDir2.startsWith(projectRoot3 + path.sep);
|
|
89
|
+
const toolRelPath = toolIsEmbedded2 ? path.relative(projectRoot3, toolDir2) : toolDir2;
|
|
90
|
+
return { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir: toolDir2, toolIsEmbedded: toolIsEmbedded2, toolRelPath };
|
|
21
91
|
}
|
|
22
92
|
var init_config = __esm({
|
|
23
93
|
"config.ts"() {
|
|
24
94
|
}
|
|
25
95
|
});
|
|
26
96
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
__export(module_graph_exports, {
|
|
30
|
-
buildGraph: () => buildGraph,
|
|
31
|
-
createResolver: () => createResolver,
|
|
32
|
-
discoverFiles: () => discoverFiles,
|
|
33
|
-
removeFile: () => removeFile,
|
|
34
|
-
resolveProjectImport: () => resolveProjectImport,
|
|
35
|
-
startWatcher: () => startWatcher,
|
|
36
|
-
updateFile: () => updateFile
|
|
37
|
-
});
|
|
38
|
-
import { parseSync } from "oxc-parser";
|
|
39
|
-
import { ResolverFactory } from "oxc-resolver";
|
|
40
|
-
import * as fs from "fs";
|
|
97
|
+
// tsserver-client.ts
|
|
98
|
+
import { spawn } from "child_process";
|
|
41
99
|
import * as path2 from "path";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
-
} catch {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (entry.isDirectory()) {
|
|
53
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
54
|
-
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
55
|
-
walk(path2.join(dir, entry.name));
|
|
56
|
-
} else if (entry.isFile()) {
|
|
57
|
-
const name = entry.name;
|
|
58
|
-
if (SKIP_FILES.has(name)) continue;
|
|
59
|
-
if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts")) continue;
|
|
60
|
-
const ext = path2.extname(name);
|
|
61
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
62
|
-
files.push(path2.join(dir, name));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
walk(rootDir);
|
|
68
|
-
return files;
|
|
69
|
-
}
|
|
70
|
-
function parseFileImports(filePath, source) {
|
|
71
|
-
const result = parseSync(filePath, source);
|
|
72
|
-
const imports = [];
|
|
73
|
-
for (const imp of result.module.staticImports) {
|
|
74
|
-
const specifier = imp.moduleRequest.value;
|
|
75
|
-
const names = [];
|
|
76
|
-
let allTypeOnly = true;
|
|
77
|
-
for (const entry of imp.entries) {
|
|
78
|
-
const kind = entry.importName.kind;
|
|
79
|
-
const name = kind === "Default" ? "default" : kind === "All" || kind === "AllButDefault" || kind === "NamespaceObject" ? "*" : entry.importName.name ?? entry.localName.value;
|
|
80
|
-
names.push(name);
|
|
81
|
-
if (!entry.isType) allTypeOnly = false;
|
|
82
|
-
}
|
|
83
|
-
if (names.length === 0) {
|
|
84
|
-
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: false });
|
|
85
|
-
} else {
|
|
86
|
-
imports.push({ specifier, names, isTypeOnly: allTypeOnly, isDynamic: false });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
for (const exp of result.module.staticExports) {
|
|
90
|
-
for (const entry of exp.entries) {
|
|
91
|
-
const moduleRequest = entry.moduleRequest;
|
|
92
|
-
if (!moduleRequest) continue;
|
|
93
|
-
const specifier = moduleRequest.value;
|
|
94
|
-
const entryKind = entry.importName.kind;
|
|
95
|
-
const name = entryKind === "AllButDefault" || entryKind === "All" || entryKind === "NamespaceObject" ? "*" : entry.importName.name ?? "*";
|
|
96
|
-
const existing = imports.find((i) => i.specifier === specifier && !i.isDynamic);
|
|
97
|
-
if (existing) {
|
|
98
|
-
if (!existing.names.includes(name)) existing.names.push(name);
|
|
99
|
-
} else {
|
|
100
|
-
imports.push({ specifier, names: [name], isTypeOnly: false, isDynamic: false });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
for (const di of result.module.dynamicImports) {
|
|
105
|
-
if (di.moduleRequest) {
|
|
106
|
-
const sliced = source.slice(di.moduleRequest.start, di.moduleRequest.end);
|
|
107
|
-
if (sliced.startsWith("'") || sliced.startsWith('"')) {
|
|
108
|
-
const specifier = sliced.slice(1, -1);
|
|
109
|
-
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: true });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return imports;
|
|
114
|
-
}
|
|
115
|
-
function distToSource(resolvedPath, projectRoot3) {
|
|
116
|
-
if (!resolvedPath.startsWith(projectRoot3)) return resolvedPath;
|
|
117
|
-
const rel2 = path2.relative(projectRoot3, resolvedPath);
|
|
118
|
-
const distIdx = rel2.indexOf("dist" + path2.sep);
|
|
119
|
-
if (distIdx === -1) return resolvedPath;
|
|
120
|
-
const prefix = rel2.slice(0, distIdx);
|
|
121
|
-
const afterDist = rel2.slice(distIdx + 5);
|
|
122
|
-
const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
|
|
123
|
-
for (const ext of SOURCE_EXTS) {
|
|
124
|
-
const candidate = path2.resolve(projectRoot3, prefix, "src", withoutExt + ext);
|
|
125
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
126
|
-
}
|
|
127
|
-
for (const ext of SOURCE_EXTS) {
|
|
128
|
-
const candidate = path2.resolve(projectRoot3, prefix, withoutExt + ext);
|
|
129
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
130
|
-
}
|
|
131
|
-
if (withoutExt.endsWith("/index")) {
|
|
132
|
-
const dirPath = withoutExt.slice(0, -6);
|
|
133
|
-
for (const ext of SOURCE_EXTS) {
|
|
134
|
-
const candidate = path2.resolve(projectRoot3, prefix, "src", dirPath + ext);
|
|
135
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return resolvedPath;
|
|
100
|
+
import * as fs from "fs";
|
|
101
|
+
import { createRequire } from "module";
|
|
102
|
+
function readPackageVersion(packagePath) {
|
|
103
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
104
|
+
return pkg.version ?? "unknown";
|
|
139
105
|
}
|
|
140
|
-
function
|
|
106
|
+
function resolveTsServer(projectRoot3) {
|
|
107
|
+
const projectRequire = createRequire(path2.resolve(projectRoot3, "package.json"));
|
|
108
|
+
let projectVersion;
|
|
141
109
|
try {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!TS_EXTENSIONS.has(ext)) return null;
|
|
147
|
-
if (SKIP_FILES.has(path2.basename(mapped))) return null;
|
|
148
|
-
return mapped;
|
|
149
|
-
}
|
|
110
|
+
const packagePath2 = projectRequire.resolve("typescript/package.json");
|
|
111
|
+
projectVersion = readPackageVersion(packagePath2);
|
|
112
|
+
const serverPath2 = projectRequire.resolve("typescript/lib/tsserver.js");
|
|
113
|
+
return { path: serverPath2, version: projectVersion, source: "project" };
|
|
150
114
|
} catch {
|
|
151
115
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
extensionAlias: {
|
|
162
|
-
".js": [".ts", ".tsx", ".js"],
|
|
163
|
-
".jsx": [".tsx", ".jsx"],
|
|
164
|
-
".mjs": [".mts", ".mjs"],
|
|
165
|
-
".cjs": [".cts", ".cjs"]
|
|
166
|
-
},
|
|
167
|
-
conditionNames: ["import", "require"],
|
|
168
|
-
mainFields: ["module", "main"]
|
|
169
|
-
});
|
|
116
|
+
const toolRequire = createRequire(import.meta.url);
|
|
117
|
+
const packagePath = toolRequire.resolve("typescript/package.json");
|
|
118
|
+
const serverPath = toolRequire.resolve("typescript/lib/tsserver.js");
|
|
119
|
+
return {
|
|
120
|
+
path: serverPath,
|
|
121
|
+
version: readPackageVersion(packagePath),
|
|
122
|
+
source: "typegraph",
|
|
123
|
+
projectVersion
|
|
124
|
+
};
|
|
170
125
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
126
|
+
var log, REQUEST_TIMEOUT_MS, TsServerClient;
|
|
127
|
+
var init_tsserver_client = __esm({
|
|
128
|
+
"tsserver-client.ts"() {
|
|
129
|
+
log = (...args2) => console.error("[typegraph/tsserver]", ...args2);
|
|
130
|
+
REQUEST_TIMEOUT_MS = 1e4;
|
|
131
|
+
TsServerClient = class {
|
|
132
|
+
constructor(projectRoot3, tsconfigPath3 = "./tsconfig.json") {
|
|
133
|
+
this.projectRoot = projectRoot3;
|
|
134
|
+
this.tsconfigPath = tsconfigPath3;
|
|
135
|
+
}
|
|
136
|
+
child = null;
|
|
137
|
+
seq = 0;
|
|
138
|
+
pending = /* @__PURE__ */ new Map();
|
|
139
|
+
openFiles = /* @__PURE__ */ new Set();
|
|
140
|
+
buffer = Buffer.alloc(0);
|
|
141
|
+
ready = false;
|
|
142
|
+
shuttingDown = false;
|
|
143
|
+
restartCount = 0;
|
|
144
|
+
maxRestarts = 3;
|
|
145
|
+
projectsDirty = false;
|
|
146
|
+
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
147
|
+
resolvePath(file) {
|
|
148
|
+
return path2.isAbsolute(file) ? file : path2.resolve(this.projectRoot, file);
|
|
149
|
+
}
|
|
150
|
+
relativePath(file) {
|
|
151
|
+
return path2.relative(this.projectRoot, file);
|
|
152
|
+
}
|
|
153
|
+
/** Read a line from a file (1-based line number). Returns trimmed content. */
|
|
154
|
+
readLine(file, line) {
|
|
155
|
+
try {
|
|
156
|
+
const absPath2 = this.resolvePath(file);
|
|
157
|
+
const content = fs.readFileSync(absPath2, "utf-8");
|
|
158
|
+
const lines = content.split("\n");
|
|
159
|
+
return lines[line - 1]?.trim() ?? "";
|
|
160
|
+
} catch {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
165
|
+
async start() {
|
|
166
|
+
if (this.child) return;
|
|
167
|
+
const resolution = resolveTsServer(this.projectRoot);
|
|
168
|
+
const tsserverPath = resolution.path;
|
|
169
|
+
const fallbackNote = resolution.projectVersion ? `; project TypeScript ${resolution.projectVersion} has no legacy tsserver` : "";
|
|
170
|
+
log(
|
|
171
|
+
`Spawning tsserver: ${tsserverPath} (TypeScript ${resolution.version}, ${resolution.source}${fallbackNote})`
|
|
172
|
+
);
|
|
173
|
+
log(`Project root: ${this.projectRoot}`);
|
|
174
|
+
log(`tsconfig: ${this.tsconfigPath}`);
|
|
175
|
+
this.child = spawn("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
176
|
+
cwd: this.projectRoot,
|
|
177
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
178
|
+
env: { ...process.env, TSS_LOG: void 0 }
|
|
179
|
+
});
|
|
180
|
+
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
181
|
+
this.child.stderr.on("data", (chunk) => {
|
|
182
|
+
const text = chunk.toString().trim();
|
|
183
|
+
if (text) log(`[stderr] ${text}`);
|
|
184
|
+
});
|
|
185
|
+
this.child.on("close", (code) => {
|
|
186
|
+
log(`tsserver exited with code ${code}`);
|
|
187
|
+
this.child = null;
|
|
188
|
+
this.rejectAllPending(new Error(`tsserver exited with code ${code}`));
|
|
189
|
+
this.tryRestart();
|
|
190
|
+
});
|
|
191
|
+
this.child.on("error", (err) => {
|
|
192
|
+
log(`tsserver error: ${err.message}`);
|
|
193
|
+
this.rejectAllPending(err);
|
|
194
|
+
});
|
|
195
|
+
await this.sendRequest("configure", {
|
|
196
|
+
preferences: {
|
|
197
|
+
disableSuggestions: true
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const warmStart = performance.now();
|
|
201
|
+
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
202
|
+
options: { allowJs: true, checkJs: false }
|
|
198
203
|
});
|
|
204
|
+
this.ready = true;
|
|
205
|
+
log(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
199
206
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
for (const [source, edges] of forward) {
|
|
208
|
-
for (const edge of edges) {
|
|
209
|
-
let revEdges = reverse.get(edge.target);
|
|
210
|
-
if (!revEdges) {
|
|
211
|
-
revEdges = [];
|
|
212
|
-
reverse.set(edge.target, revEdges);
|
|
207
|
+
shutdown() {
|
|
208
|
+
this.shuttingDown = true;
|
|
209
|
+
if (this.child) {
|
|
210
|
+
this.child.kill("SIGTERM");
|
|
211
|
+
this.child = null;
|
|
212
|
+
}
|
|
213
|
+
this.rejectAllPending(new Error("Client shutdown"));
|
|
213
214
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
isTypeOnly: edge.isTypeOnly,
|
|
219
|
-
isDynamic: edge.isDynamic
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return reverse;
|
|
224
|
-
}
|
|
225
|
-
async function buildGraph(projectRoot3, tsconfigPath3) {
|
|
226
|
-
const startTime = performance.now();
|
|
227
|
-
const resolver = createResolver(projectRoot3, tsconfigPath3);
|
|
228
|
-
const fileList = discoverFiles(projectRoot3);
|
|
229
|
-
log(`Discovered ${fileList.length} source files`);
|
|
230
|
-
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot3);
|
|
231
|
-
const reverse = buildReverseMap(forward);
|
|
232
|
-
const files = new Set(fileList);
|
|
233
|
-
const edgeCount = [...forward.values()].reduce((sum, edges) => sum + edges.length, 0);
|
|
234
|
-
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
235
|
-
log(`Graph built: ${files.size} files, ${edgeCount} edges [${elapsed}ms]`);
|
|
236
|
-
if (parseFailures.length > 0) {
|
|
237
|
-
log(`Parse failures: ${parseFailures.length} files`);
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
graph: { forward, reverse, files },
|
|
241
|
-
resolver
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function updateFile(graph, filePath, resolver, projectRoot3) {
|
|
245
|
-
const oldEdges = graph.forward.get(filePath) ?? [];
|
|
246
|
-
for (const edge of oldEdges) {
|
|
247
|
-
const revEdges = graph.reverse.get(edge.target);
|
|
248
|
-
if (revEdges) {
|
|
249
|
-
const idx = revEdges.findIndex((r) => r.target === filePath);
|
|
250
|
-
if (idx !== -1) revEdges.splice(idx, 1);
|
|
251
|
-
if (revEdges.length === 0) graph.reverse.delete(edge.target);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
let source;
|
|
255
|
-
try {
|
|
256
|
-
source = fs.readFileSync(filePath, "utf-8");
|
|
257
|
-
} catch {
|
|
258
|
-
removeFile(graph, filePath);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
let rawImports;
|
|
262
|
-
try {
|
|
263
|
-
rawImports = parseFileImports(filePath, source);
|
|
264
|
-
} catch {
|
|
265
|
-
log(`Parse error on update: ${filePath}`);
|
|
266
|
-
graph.forward.set(filePath, []);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
const fromDir = path2.dirname(filePath);
|
|
270
|
-
const newEdges = [];
|
|
271
|
-
for (const raw of rawImports) {
|
|
272
|
-
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot3);
|
|
273
|
-
if (target) {
|
|
274
|
-
newEdges.push({
|
|
275
|
-
target,
|
|
276
|
-
specifiers: raw.names,
|
|
277
|
-
isTypeOnly: raw.isTypeOnly,
|
|
278
|
-
isDynamic: raw.isDynamic
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
graph.forward.set(filePath, newEdges);
|
|
283
|
-
graph.files.add(filePath);
|
|
284
|
-
for (const edge of newEdges) {
|
|
285
|
-
let revEdges = graph.reverse.get(edge.target);
|
|
286
|
-
if (!revEdges) {
|
|
287
|
-
revEdges = [];
|
|
288
|
-
graph.reverse.set(edge.target, revEdges);
|
|
289
|
-
}
|
|
290
|
-
revEdges.push({
|
|
291
|
-
target: filePath,
|
|
292
|
-
specifiers: edge.specifiers,
|
|
293
|
-
isTypeOnly: edge.isTypeOnly,
|
|
294
|
-
isDynamic: edge.isDynamic
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
function removeFile(graph, filePath) {
|
|
299
|
-
const edges = graph.forward.get(filePath) ?? [];
|
|
300
|
-
for (const edge of edges) {
|
|
301
|
-
const revEdges2 = graph.reverse.get(edge.target);
|
|
302
|
-
if (revEdges2) {
|
|
303
|
-
const idx = revEdges2.findIndex((r) => r.target === filePath);
|
|
304
|
-
if (idx !== -1) revEdges2.splice(idx, 1);
|
|
305
|
-
if (revEdges2.length === 0) graph.reverse.delete(edge.target);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
const revEdges = graph.reverse.get(filePath) ?? [];
|
|
309
|
-
for (const revEdge of revEdges) {
|
|
310
|
-
const fwdEdges = graph.forward.get(revEdge.target);
|
|
311
|
-
if (fwdEdges) {
|
|
312
|
-
const idx = fwdEdges.findIndex((e) => e.target === filePath);
|
|
313
|
-
if (idx !== -1) fwdEdges.splice(idx, 1);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
graph.forward.delete(filePath);
|
|
317
|
-
graph.reverse.delete(filePath);
|
|
318
|
-
graph.files.delete(filePath);
|
|
319
|
-
}
|
|
320
|
-
function startWatcher(projectRoot3, graph, resolver, hooks) {
|
|
321
|
-
try {
|
|
322
|
-
const watcher = fs.watch(
|
|
323
|
-
projectRoot3,
|
|
324
|
-
{ recursive: true },
|
|
325
|
-
(_eventType, filename) => {
|
|
326
|
-
if (!filename) return;
|
|
327
|
-
const ext = path2.extname(filename);
|
|
328
|
-
if (!TS_EXTENSIONS.has(ext)) return;
|
|
329
|
-
const parts = filename.split(path2.sep);
|
|
330
|
-
if (parts.some((p2) => SKIP_DIRS.has(p2))) return;
|
|
331
|
-
if (SKIP_FILES.has(path2.basename(filename))) return;
|
|
332
|
-
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
215
|
+
tryRestart() {
|
|
216
|
+
if (this.shuttingDown) return;
|
|
217
|
+
if (this.restartCount >= this.maxRestarts) {
|
|
218
|
+
log(`Max restarts (${this.maxRestarts}) reached, not restarting`);
|
|
333
219
|
return;
|
|
334
|
-
const absPath2 = path2.resolve(projectRoot3, filename);
|
|
335
|
-
if (fs.existsSync(absPath2)) {
|
|
336
|
-
updateFile(graph, absPath2, resolver, projectRoot3);
|
|
337
|
-
void hooks?.onFileUpdated?.(absPath2);
|
|
338
|
-
} else {
|
|
339
|
-
removeFile(graph, absPath2);
|
|
340
|
-
void hooks?.onFileDeleted?.(absPath2);
|
|
341
220
|
}
|
|
221
|
+
this.restartCount++;
|
|
222
|
+
log(`Restarting tsserver (attempt ${this.restartCount})...`);
|
|
223
|
+
this.buffer = Buffer.alloc(0);
|
|
224
|
+
const filesToReopen = [...this.openFiles];
|
|
225
|
+
this.openFiles.clear();
|
|
226
|
+
this.start().then(async () => {
|
|
227
|
+
for (const file of filesToReopen) {
|
|
228
|
+
await this.ensureOpen(file).catch(() => {
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
342
232
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
var log, TS_EXTENSIONS, SKIP_DIRS, SKIP_FILES, SOURCE_EXTS;
|
|
352
|
-
var init_module_graph = __esm({
|
|
353
|
-
"module-graph.ts"() {
|
|
354
|
-
log = (...args2) => console.error("[typegraph/graph]", ...args2);
|
|
355
|
-
TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
356
|
-
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
357
|
-
"node_modules",
|
|
358
|
-
"dist",
|
|
359
|
-
"build",
|
|
360
|
-
"out",
|
|
361
|
-
".wrangler",
|
|
362
|
-
".mf",
|
|
363
|
-
".git",
|
|
364
|
-
".next",
|
|
365
|
-
".turbo",
|
|
366
|
-
"coverage"
|
|
367
|
-
]);
|
|
368
|
-
SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
369
|
-
SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts"];
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// check.ts
|
|
374
|
-
var check_exports = {};
|
|
375
|
-
__export(check_exports, {
|
|
376
|
-
main: () => main
|
|
377
|
-
});
|
|
378
|
-
import * as fs2 from "fs";
|
|
379
|
-
import * as path3 from "path";
|
|
380
|
-
import { createRequire } from "module";
|
|
381
|
-
import { spawn, spawnSync } from "child_process";
|
|
382
|
-
function findFirstTsFile(dir) {
|
|
383
|
-
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".wrangler", "coverage"]);
|
|
384
|
-
try {
|
|
385
|
-
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
386
|
-
if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
387
|
-
return path3.join(dir, entry.name);
|
|
233
|
+
rejectAllPending(err) {
|
|
234
|
+
for (const [seq, pending] of this.pending) {
|
|
235
|
+
clearTimeout(pending.timer);
|
|
236
|
+
pending.reject(err);
|
|
237
|
+
}
|
|
238
|
+
this.pending.clear();
|
|
388
239
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (found) return found;
|
|
240
|
+
// ─── Protocol: Parsing ──────────────────────────────────────────────────
|
|
241
|
+
onData(chunk) {
|
|
242
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
243
|
+
this.processBuffer();
|
|
394
244
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
child.stdout.on("data", (chunk) => {
|
|
420
|
-
buffer += chunk.toString();
|
|
421
|
-
if (buffer.includes('"success":true')) {
|
|
422
|
-
clearTimeout(timeout);
|
|
423
|
-
child.kill();
|
|
424
|
-
resolve9(true);
|
|
245
|
+
processBuffer() {
|
|
246
|
+
while (true) {
|
|
247
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
248
|
+
if (headerEnd === -1) return;
|
|
249
|
+
const header = this.buffer.subarray(0, headerEnd).toString("utf-8");
|
|
250
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
251
|
+
if (!match) {
|
|
252
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const contentLength = parseInt(match[1], 10);
|
|
256
|
+
const bodyStart = headerEnd + 4;
|
|
257
|
+
if (this.buffer.length < bodyStart + contentLength) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const bodyBytes = this.buffer.subarray(bodyStart, bodyStart + contentLength);
|
|
261
|
+
this.buffer = this.buffer.subarray(bodyStart + contentLength);
|
|
262
|
+
try {
|
|
263
|
+
const message = JSON.parse(bodyBytes.toString("utf-8"));
|
|
264
|
+
this.onMessage(message);
|
|
265
|
+
} catch {
|
|
266
|
+
log("Failed to parse tsserver message");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
425
269
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
270
|
+
onMessage(message) {
|
|
271
|
+
if (message.type === "response" && message.request_seq !== void 0) {
|
|
272
|
+
const pending = this.pending.get(message.request_seq);
|
|
273
|
+
if (pending) {
|
|
274
|
+
clearTimeout(pending.timer);
|
|
275
|
+
this.pending.delete(message.request_seq);
|
|
276
|
+
if (message.success) {
|
|
277
|
+
pending.resolve(message.body);
|
|
278
|
+
} else {
|
|
279
|
+
pending.reject(
|
|
280
|
+
new Error(`tsserver ${pending.command} failed: ${message.message ?? "unknown error"}`)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
440
285
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
286
|
+
// ─── Protocol: Sending ──────────────────────────────────────────────────
|
|
287
|
+
sendRequest(command2, args2) {
|
|
288
|
+
if (!this.child?.stdin?.writable) {
|
|
289
|
+
return Promise.reject(new Error("tsserver not running"));
|
|
290
|
+
}
|
|
291
|
+
const seq = ++this.seq;
|
|
292
|
+
const request = {
|
|
293
|
+
seq,
|
|
294
|
+
type: "request",
|
|
295
|
+
command: command2,
|
|
296
|
+
arguments: args2
|
|
297
|
+
};
|
|
298
|
+
return new Promise((resolve10, reject) => {
|
|
299
|
+
const timer = setTimeout(() => {
|
|
300
|
+
this.pending.delete(seq);
|
|
301
|
+
reject(new Error(`tsserver ${command2} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
302
|
+
}, REQUEST_TIMEOUT_MS);
|
|
303
|
+
this.pending.set(seq, { resolve: resolve10, reject, timer, command: command2 });
|
|
304
|
+
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// Fire-and-forget — for commands like `open` that may not send a response
|
|
308
|
+
sendNotification(command2, args2) {
|
|
309
|
+
if (!this.child?.stdin?.writable) return;
|
|
310
|
+
const seq = ++this.seq;
|
|
311
|
+
const request = { seq, type: "request", command: command2, arguments: args2 };
|
|
312
|
+
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
313
|
+
}
|
|
314
|
+
// ─── File Management ───────────────────────────────────────────────────
|
|
315
|
+
async ensureOpen(file) {
|
|
316
|
+
const absPath2 = this.resolvePath(file);
|
|
317
|
+
if (this.openFiles.has(absPath2)) return;
|
|
318
|
+
this.openFiles.add(absPath2);
|
|
319
|
+
this.sendNotification("open", { file: absPath2 });
|
|
320
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
324
|
+
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
325
|
+
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
326
|
+
* load time and new files unassigned (they land in single-file inferred
|
|
327
|
+
* projects, producing definition-only reference results). The next query
|
|
328
|
+
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
329
|
+
* from disk. Open files are unaffected by design — their content is
|
|
330
|
+
* protocol-owned and refreshed via reloadOpenFile().
|
|
331
|
+
*/
|
|
332
|
+
markProjectsDirty() {
|
|
333
|
+
this.projectsDirty = true;
|
|
334
|
+
}
|
|
335
|
+
refreshIfDirty() {
|
|
336
|
+
if (!this.projectsDirty) return;
|
|
337
|
+
this.projectsDirty = false;
|
|
338
|
+
this.sendNotification("reloadProjects");
|
|
339
|
+
}
|
|
340
|
+
async reloadOpenFile(file) {
|
|
341
|
+
const absPath2 = this.resolvePath(file);
|
|
342
|
+
if (!this.openFiles.has(absPath2)) return false;
|
|
343
|
+
await this.sendRequest("reload", {
|
|
344
|
+
file: absPath2,
|
|
345
|
+
tmpfile: absPath2
|
|
346
|
+
});
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
closeFile(file) {
|
|
350
|
+
const absPath2 = this.resolvePath(file);
|
|
351
|
+
if (!this.openFiles.delete(absPath2)) return false;
|
|
352
|
+
this.sendNotification("close", { file: absPath2 });
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
// ─── Public API ────────────────────────────────────────────────────────
|
|
356
|
+
async definition(file, line, offset) {
|
|
357
|
+
this.refreshIfDirty();
|
|
358
|
+
const absPath2 = this.resolvePath(file);
|
|
359
|
+
await this.ensureOpen(absPath2);
|
|
360
|
+
const body = await this.sendRequest("definition", {
|
|
361
|
+
file: absPath2,
|
|
362
|
+
line,
|
|
363
|
+
offset
|
|
364
|
+
});
|
|
365
|
+
if (!body || !Array.isArray(body)) return [];
|
|
366
|
+
return body.map((d) => ({
|
|
367
|
+
...d,
|
|
368
|
+
file: this.relativePath(d.file)
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
async references(file, line, offset) {
|
|
372
|
+
this.refreshIfDirty();
|
|
373
|
+
const absPath2 = this.resolvePath(file);
|
|
374
|
+
await this.ensureOpen(absPath2);
|
|
375
|
+
const body = await this.sendRequest("references", {
|
|
376
|
+
file: absPath2,
|
|
377
|
+
line,
|
|
378
|
+
offset
|
|
379
|
+
});
|
|
380
|
+
if (!body?.refs) return [];
|
|
381
|
+
return body.refs.map((r) => ({
|
|
382
|
+
...r,
|
|
383
|
+
file: this.relativePath(r.file)
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
async quickinfo(file, line, offset) {
|
|
387
|
+
this.refreshIfDirty();
|
|
388
|
+
const absPath2 = this.resolvePath(file);
|
|
389
|
+
await this.ensureOpen(absPath2);
|
|
390
|
+
try {
|
|
391
|
+
const body = await this.sendRequest("quickinfo", {
|
|
392
|
+
file: absPath2,
|
|
393
|
+
line,
|
|
394
|
+
offset
|
|
395
|
+
});
|
|
396
|
+
return body ?? null;
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async navto(searchValue, maxResults = 10, file) {
|
|
402
|
+
this.refreshIfDirty();
|
|
403
|
+
if (file) await this.ensureOpen(file);
|
|
404
|
+
const args2 = {
|
|
405
|
+
searchValue,
|
|
406
|
+
maxResultCount: maxResults
|
|
407
|
+
};
|
|
408
|
+
if (file) args2["file"] = this.resolvePath(file);
|
|
409
|
+
const body = await this.sendRequest("navto", args2);
|
|
410
|
+
if (!body || !Array.isArray(body)) return [];
|
|
411
|
+
return body.map((item) => ({
|
|
412
|
+
...item,
|
|
413
|
+
file: this.relativePath(item.file)
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
async navbar(file) {
|
|
417
|
+
this.refreshIfDirty();
|
|
418
|
+
const absPath2 = this.resolvePath(file);
|
|
419
|
+
await this.ensureOpen(absPath2);
|
|
420
|
+
const body = await this.sendRequest("navbar", {
|
|
421
|
+
file: absPath2
|
|
422
|
+
});
|
|
423
|
+
return body ?? [];
|
|
424
|
+
}
|
|
425
|
+
};
|
|
542
426
|
}
|
|
543
|
-
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// module-graph.ts
|
|
430
|
+
var module_graph_exports = {};
|
|
431
|
+
__export(module_graph_exports, {
|
|
432
|
+
buildGraph: () => buildGraph,
|
|
433
|
+
createResolver: () => createResolver,
|
|
434
|
+
discoverFiles: () => discoverFiles,
|
|
435
|
+
removeFile: () => removeFile,
|
|
436
|
+
resolveProjectImport: () => resolveProjectImport,
|
|
437
|
+
startWatcher: () => startWatcher,
|
|
438
|
+
updateFile: () => updateFile
|
|
439
|
+
});
|
|
440
|
+
import { parseSync } from "oxc-parser";
|
|
441
|
+
import { ResolverFactory } from "oxc-resolver";
|
|
442
|
+
import * as fs2 from "fs";
|
|
443
|
+
import * as path3 from "path";
|
|
444
|
+
function normalizeExcludedPaths(rootDir, excludedPaths2) {
|
|
445
|
+
return excludedPaths2.map(
|
|
446
|
+
(excludedPath) => path3.resolve(rootDir, excludedPath)
|
|
447
|
+
);
|
|
544
448
|
}
|
|
545
|
-
function
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
"peerDependencies",
|
|
550
|
-
"optionalDependencies"
|
|
551
|
-
];
|
|
552
|
-
return depKeys.some((key) => {
|
|
553
|
-
const deps = packageJson?.[key];
|
|
554
|
-
return typeof deps === "object" && deps !== null && packageName in deps;
|
|
555
|
-
});
|
|
449
|
+
function isExcluded(filePath, excludedPaths2) {
|
|
450
|
+
return excludedPaths2.some(
|
|
451
|
+
(excludedPath) => filePath === excludedPath || filePath.startsWith(excludedPath + path3.sep)
|
|
452
|
+
);
|
|
556
453
|
}
|
|
557
|
-
function
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
454
|
+
function discoverFiles(rootDir, excludedPaths2 = []) {
|
|
455
|
+
const files = [];
|
|
456
|
+
const normalizedExclusions = normalizeExcludedPaths(rootDir, excludedPaths2);
|
|
457
|
+
function walk(dir) {
|
|
458
|
+
let entries;
|
|
459
|
+
try {
|
|
460
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
461
|
+
} catch {
|
|
462
|
+
return;
|
|
563
463
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
if (entry.isDirectory()) {
|
|
466
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
467
|
+
if (entry.name.startsWith(".") && dir !== rootDir) continue;
|
|
468
|
+
const childDir = path3.join(dir, entry.name);
|
|
469
|
+
if (isExcluded(childDir, normalizedExclusions)) continue;
|
|
470
|
+
walk(childDir);
|
|
471
|
+
} else if (entry.isFile()) {
|
|
472
|
+
const name = entry.name;
|
|
473
|
+
if (SKIP_FILES.has(name)) continue;
|
|
474
|
+
if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts")) continue;
|
|
475
|
+
const ext = path3.extname(name);
|
|
476
|
+
if (TS_EXTENSIONS.has(ext)) {
|
|
477
|
+
files.push(path3.join(dir, name));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
569
480
|
}
|
|
570
481
|
}
|
|
571
|
-
|
|
482
|
+
walk(rootDir);
|
|
483
|
+
return files;
|
|
572
484
|
}
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
485
|
+
function parseFileImports(filePath, source) {
|
|
486
|
+
const result = parseSync(filePath, source);
|
|
487
|
+
const imports = [];
|
|
488
|
+
for (const imp of result.module.staticImports) {
|
|
489
|
+
const specifier = imp.moduleRequest.value;
|
|
490
|
+
const names = [];
|
|
491
|
+
let allTypeOnly = true;
|
|
492
|
+
for (const entry of imp.entries) {
|
|
493
|
+
const kind = entry.importName.kind;
|
|
494
|
+
const name = kind === "Default" ? "default" : kind === "All" || kind === "AllButDefault" || kind === "NamespaceObject" ? "*" : entry.importName.name ?? entry.localName.value;
|
|
495
|
+
names.push(name);
|
|
496
|
+
if (!entry.isType) allTypeOnly = false;
|
|
497
|
+
}
|
|
498
|
+
if (names.length === 0) {
|
|
499
|
+
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: false });
|
|
500
|
+
} else {
|
|
501
|
+
imports.push({ specifier, names, isTypeOnly: allTypeOnly, isDynamic: false });
|
|
502
|
+
}
|
|
583
503
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
504
|
+
for (const exp of result.module.staticExports) {
|
|
505
|
+
for (const entry of exp.entries) {
|
|
506
|
+
const moduleRequest = entry.moduleRequest;
|
|
507
|
+
if (!moduleRequest) continue;
|
|
508
|
+
const specifier = moduleRequest.value;
|
|
509
|
+
const entryKind = entry.importName.kind;
|
|
510
|
+
const name = entryKind === "AllButDefault" || entryKind === "All" || entryKind === "NamespaceObject" ? "*" : entry.importName.name ?? "*";
|
|
511
|
+
const existing = imports.find((i) => i.specifier === specifier && !i.isDynamic);
|
|
512
|
+
if (existing) {
|
|
513
|
+
if (!existing.names.includes(name)) existing.names.push(name);
|
|
514
|
+
} else {
|
|
515
|
+
imports.push({ specifier, names: [name], isTypeOnly: false, isDynamic: false });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
588
518
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
519
|
+
for (const di of result.module.dynamicImports) {
|
|
520
|
+
if (di.moduleRequest) {
|
|
521
|
+
const sliced = source.slice(di.moduleRequest.start, di.moduleRequest.end);
|
|
522
|
+
if (sliced.startsWith("'") || sliced.startsWith('"')) {
|
|
523
|
+
const specifier = sliced.slice(1, -1);
|
|
524
|
+
imports.push({ specifier, names: ["*"], isTypeOnly: false, isDynamic: true });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
593
527
|
}
|
|
594
|
-
|
|
595
|
-
|
|
528
|
+
return imports;
|
|
529
|
+
}
|
|
530
|
+
function distToSource(resolvedPath, projectRoot3) {
|
|
531
|
+
if (!resolvedPath.startsWith(projectRoot3)) return resolvedPath;
|
|
532
|
+
const rel2 = path3.relative(projectRoot3, resolvedPath);
|
|
533
|
+
const distIdx = rel2.indexOf("dist" + path3.sep);
|
|
534
|
+
if (distIdx === -1) return resolvedPath;
|
|
535
|
+
const prefix = rel2.slice(0, distIdx);
|
|
536
|
+
const afterDist = rel2.slice(distIdx + 5);
|
|
537
|
+
const withoutExt = afterDist.replace(/\.(m?j|c)s$/, "");
|
|
538
|
+
for (const ext of SOURCE_EXTS) {
|
|
539
|
+
const candidate = path3.resolve(projectRoot3, prefix, "src", withoutExt + ext);
|
|
540
|
+
if (fs2.existsSync(candidate)) return candidate;
|
|
596
541
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
console.log(`Project root: ${projectRoot3}`);
|
|
601
|
-
console.log("");
|
|
602
|
-
const nodeVersion = process.version;
|
|
603
|
-
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
604
|
-
if (nodeMajor >= 22) {
|
|
605
|
-
pass(`Node.js ${nodeVersion} (>= 22 required)`);
|
|
606
|
-
} else {
|
|
607
|
-
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
|
|
542
|
+
for (const ext of SOURCE_EXTS) {
|
|
543
|
+
const candidate = path3.resolve(projectRoot3, prefix, withoutExt + ext);
|
|
544
|
+
if (fs2.existsSync(candidate)) return candidate;
|
|
608
545
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
546
|
+
if (withoutExt.endsWith("/index")) {
|
|
547
|
+
const dirPath = withoutExt.slice(0, -6);
|
|
548
|
+
for (const ext of SOURCE_EXTS) {
|
|
549
|
+
const candidate = path3.resolve(projectRoot3, prefix, "src", dirPath + ext);
|
|
550
|
+
if (fs2.existsSync(candidate)) return candidate;
|
|
551
|
+
}
|
|
615
552
|
}
|
|
616
|
-
|
|
553
|
+
return resolvedPath;
|
|
554
|
+
}
|
|
555
|
+
function resolveProjectImport(resolver, fromDir, specifier, projectRoot3) {
|
|
617
556
|
try {
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
557
|
+
const result = resolver.sync(fromDir, specifier);
|
|
558
|
+
if (result.path && !result.path.includes("node_modules")) {
|
|
559
|
+
const mapped = distToSource(result.path, projectRoot3);
|
|
560
|
+
const ext = path3.extname(mapped);
|
|
561
|
+
if (!TS_EXTENSIONS.has(ext)) return null;
|
|
562
|
+
if (SKIP_FILES.has(path3.basename(mapped))) return null;
|
|
563
|
+
return mapped;
|
|
564
|
+
}
|
|
624
565
|
} catch {
|
|
625
|
-
const hasDeclaredTs = hasDeclaredDependency(projectPackageJson, "typescript");
|
|
626
|
-
fail(
|
|
627
|
-
hasDeclaredTs ? "TypeScript is declared but not installed in project" : "TypeScript not found in project",
|
|
628
|
-
hasDeclaredTs ? `Run \`${installCommand}\` to install project dependencies` : `Add \`typescript\` to devDependencies and run \`${installCommand}\``
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
const tsconfigAbs = path3.resolve(projectRoot3, tsconfigPath3);
|
|
632
|
-
if (fs2.existsSync(tsconfigAbs)) {
|
|
633
|
-
pass(`tsconfig.json exists at ${tsconfigPath3}`);
|
|
634
|
-
} else {
|
|
635
|
-
fail(`tsconfig.json not found at ${tsconfigPath3}`, `Create a tsconfig.json at ${tsconfigPath3}`);
|
|
636
566
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
function createResolver(projectRoot3, tsconfigPath3) {
|
|
570
|
+
const configFile = path3.resolve(projectRoot3, tsconfigPath3);
|
|
571
|
+
return new ResolverFactory({
|
|
572
|
+
...fs2.existsSync(configFile) ? { tsconfig: { configFile, references: "auto" } } : {},
|
|
573
|
+
extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
574
|
+
extensionAlias: {
|
|
575
|
+
".js": [".ts", ".tsx", ".js"],
|
|
576
|
+
".jsx": [".tsx", ".jsx"],
|
|
577
|
+
".mjs": [".mts", ".mjs"],
|
|
578
|
+
".cjs": [".cts", ".cjs"]
|
|
579
|
+
},
|
|
580
|
+
conditionNames: ["import", "require"],
|
|
581
|
+
mainFields: ["module", "main"]
|
|
644
582
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
|
|
656
|
-
const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
|
|
657
|
-
const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
|
|
658
|
-
const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
|
|
659
|
-
if (hasProjectCodexRegistration) {
|
|
660
|
-
pass("MCP registered in project .codex/config.toml");
|
|
661
|
-
const trusted = hasTrustedCodexProject(projectRoot3);
|
|
662
|
-
if (trusted === false) {
|
|
663
|
-
warn(
|
|
664
|
-
"Project Codex config may be ignored",
|
|
665
|
-
'Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = "trusted"'
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
} else {
|
|
669
|
-
const issues = [];
|
|
670
|
-
if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
|
|
671
|
-
if (!hasCommand) issues.push("command is missing");
|
|
672
|
-
if (!hasArgs) issues.push("args are missing");
|
|
673
|
-
if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
|
|
674
|
-
if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
|
|
675
|
-
if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
|
|
676
|
-
fail(
|
|
677
|
-
`Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
|
|
678
|
-
`Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
|
|
679
|
-
);
|
|
583
|
+
}
|
|
584
|
+
function buildForwardEdges(files, resolver, projectRoot3) {
|
|
585
|
+
const forward = /* @__PURE__ */ new Map();
|
|
586
|
+
const parseFailures = [];
|
|
587
|
+
for (const filePath of files) {
|
|
588
|
+
let source;
|
|
589
|
+
try {
|
|
590
|
+
source = fs2.readFileSync(filePath, "utf-8");
|
|
591
|
+
} catch {
|
|
592
|
+
continue;
|
|
680
593
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const issues = [];
|
|
700
|
-
if (!hasCommand) issues.push("command should be 'npx'");
|
|
701
|
-
if (!hasArgs) issues.push("args should include 'tsx'");
|
|
702
|
-
if (!hasEnv) issues.push("env should set TYPEGRAPH_PROJECT_ROOT and TYPEGRAPH_TSCONFIG");
|
|
703
|
-
fail(
|
|
704
|
-
`MCP registration incomplete: ${issues.join(", ")}`,
|
|
705
|
-
"See README for correct .claude/mcp.json format"
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
} else {
|
|
709
|
-
const serverPath = toolIsEmbedded ? `./${toolRelPath}/server.ts` : path3.resolve(toolDir, "server.ts");
|
|
710
|
-
fail(
|
|
711
|
-
"MCP entry 'typegraph' not found in .claude/mcp.json",
|
|
712
|
-
`Add to .claude/mcp.json:
|
|
713
|
-
{
|
|
714
|
-
"mcpServers": {
|
|
715
|
-
"typegraph": {
|
|
716
|
-
"command": "npx",
|
|
717
|
-
"args": ["tsx", "${serverPath}"],
|
|
718
|
-
"env": { "TYPEGRAPH_PROJECT_ROOT": ".", "TYPEGRAPH_TSCONFIG": "./tsconfig.json" }
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}`
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
} catch (err) {
|
|
725
|
-
fail(
|
|
726
|
-
"Failed to parse .claude/mcp.json",
|
|
727
|
-
`Check JSON syntax: ${err instanceof Error ? err.message : String(err)}`
|
|
728
|
-
);
|
|
594
|
+
let rawImports;
|
|
595
|
+
try {
|
|
596
|
+
rawImports = parseFileImports(filePath, source);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
parseFailures.push(filePath);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const edges = [];
|
|
602
|
+
const fromDir = path3.dirname(filePath);
|
|
603
|
+
for (const raw of rawImports) {
|
|
604
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot3);
|
|
605
|
+
if (target) {
|
|
606
|
+
edges.push({
|
|
607
|
+
target,
|
|
608
|
+
specifiers: raw.names,
|
|
609
|
+
isTypeOnly: raw.isTypeOnly,
|
|
610
|
+
isDynamic: raw.isDynamic
|
|
611
|
+
});
|
|
729
612
|
}
|
|
730
|
-
} else {
|
|
731
|
-
fail(
|
|
732
|
-
"No MCP registration found",
|
|
733
|
-
`Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
|
|
734
|
-
);
|
|
735
613
|
}
|
|
614
|
+
forward.set(filePath, edges);
|
|
736
615
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
616
|
+
return { forward, parseFailures };
|
|
617
|
+
}
|
|
618
|
+
function buildReverseMap(forward) {
|
|
619
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
620
|
+
for (const [source, edges] of forward) {
|
|
621
|
+
for (const edge of edges) {
|
|
622
|
+
let revEdges = reverse.get(edge.target);
|
|
623
|
+
if (!revEdges) {
|
|
624
|
+
revEdges = [];
|
|
625
|
+
reverse.set(edge.target, revEdges);
|
|
626
|
+
}
|
|
627
|
+
revEdges.push({
|
|
628
|
+
target: source,
|
|
629
|
+
// reverse: the "target" is the file that imports
|
|
630
|
+
specifiers: edge.specifiers,
|
|
631
|
+
isTypeOnly: edge.isTypeOnly,
|
|
632
|
+
isDynamic: edge.isDynamic
|
|
633
|
+
});
|
|
747
634
|
}
|
|
748
|
-
} else {
|
|
749
|
-
fail("typegraph-mcp dependencies not installed", `Run \`cd ${toolRelPath} && npm install\``);
|
|
750
635
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
636
|
+
return reverse;
|
|
637
|
+
}
|
|
638
|
+
async function buildGraph(projectRoot3, tsconfigPath3, excludedPaths2 = []) {
|
|
639
|
+
const startTime = performance.now();
|
|
640
|
+
const resolver = createResolver(projectRoot3, tsconfigPath3);
|
|
641
|
+
const fileList = discoverFiles(projectRoot3, excludedPaths2);
|
|
642
|
+
log2(`Discovered ${fileList.length} source files`);
|
|
643
|
+
const { forward, parseFailures } = buildForwardEdges(fileList, resolver, projectRoot3);
|
|
644
|
+
const reverse = buildReverseMap(forward);
|
|
645
|
+
const files = new Set(fileList);
|
|
646
|
+
const edgeCount = [...forward.values()].reduce((sum, edges) => sum + edges.length, 0);
|
|
647
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
648
|
+
log2(`Graph built: ${files.size} files, ${edgeCount} edges [${elapsed}ms]`);
|
|
649
|
+
if (parseFailures.length > 0) {
|
|
650
|
+
log2(`Parse failures: ${parseFailures.length} files`);
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
graph: { forward, reverse, files },
|
|
654
|
+
resolver
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function updateFile(graph, filePath, resolver, projectRoot3) {
|
|
658
|
+
const oldEdges = graph.forward.get(filePath) ?? [];
|
|
659
|
+
for (const edge of oldEdges) {
|
|
660
|
+
const revEdges = graph.reverse.get(edge.target);
|
|
661
|
+
if (revEdges) {
|
|
662
|
+
const idx = revEdges.findIndex((r) => r.target === filePath);
|
|
663
|
+
if (idx !== -1) revEdges.splice(idx, 1);
|
|
664
|
+
if (revEdges.length === 0) graph.reverse.delete(edge.target);
|
|
762
665
|
}
|
|
763
|
-
} catch (err) {
|
|
764
|
-
fail(
|
|
765
|
-
`oxc-parser failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
766
|
-
`Reinstall: \`cd ${toolRelPath} && rm -rf node_modules && npm install\``
|
|
767
|
-
);
|
|
768
666
|
}
|
|
667
|
+
let source;
|
|
769
668
|
try {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
669
|
+
source = fs2.readFileSync(filePath, "utf-8");
|
|
670
|
+
} catch {
|
|
671
|
+
removeFile(graph, filePath);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
let rawImports;
|
|
675
|
+
try {
|
|
676
|
+
rawImports = parseFileImports(filePath, source);
|
|
677
|
+
} catch {
|
|
678
|
+
log2(`Parse error on update: ${filePath}`);
|
|
679
|
+
graph.forward.set(filePath, []);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const fromDir = path3.dirname(filePath);
|
|
683
|
+
const newEdges = [];
|
|
684
|
+
for (const raw of rawImports) {
|
|
685
|
+
const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot3);
|
|
686
|
+
if (target) {
|
|
687
|
+
newEdges.push({
|
|
688
|
+
target,
|
|
689
|
+
specifiers: raw.names,
|
|
690
|
+
isTypeOnly: raw.isTypeOnly,
|
|
691
|
+
isDynamic: raw.isDynamic
|
|
692
|
+
});
|
|
792
693
|
}
|
|
793
|
-
} catch (err) {
|
|
794
|
-
fail(
|
|
795
|
-
`oxc-resolver failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
796
|
-
`Reinstall: \`cd ${toolRelPath} && rm -rf node_modules && npm install\``
|
|
797
|
-
);
|
|
798
694
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
"tsserver did not respond",
|
|
807
|
-
"Verify `typescript` is installed and tsconfig.json is valid"
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
} catch (err) {
|
|
811
|
-
fail(
|
|
812
|
-
`tsserver failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
813
|
-
"Verify `typescript` is installed and tsconfig.json is valid"
|
|
814
|
-
);
|
|
695
|
+
graph.forward.set(filePath, newEdges);
|
|
696
|
+
graph.files.add(filePath);
|
|
697
|
+
for (const edge of newEdges) {
|
|
698
|
+
let revEdges = graph.reverse.get(edge.target);
|
|
699
|
+
if (!revEdges) {
|
|
700
|
+
revEdges = [];
|
|
701
|
+
graph.reverse.set(edge.target, revEdges);
|
|
815
702
|
}
|
|
816
|
-
|
|
817
|
-
|
|
703
|
+
revEdges.push({
|
|
704
|
+
target: filePath,
|
|
705
|
+
specifiers: edge.specifiers,
|
|
706
|
+
isTypeOnly: edge.isTypeOnly,
|
|
707
|
+
isDynamic: edge.isDynamic
|
|
708
|
+
});
|
|
818
709
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
710
|
+
}
|
|
711
|
+
function removeFile(graph, filePath) {
|
|
712
|
+
const edges = graph.forward.get(filePath) ?? [];
|
|
713
|
+
for (const edge of edges) {
|
|
714
|
+
const revEdges2 = graph.reverse.get(edge.target);
|
|
715
|
+
if (revEdges2) {
|
|
716
|
+
const idx = revEdges2.findIndex((r) => r.target === filePath);
|
|
717
|
+
if (idx !== -1) revEdges2.splice(idx, 1);
|
|
718
|
+
if (revEdges2.length === 0) graph.reverse.delete(edge.target);
|
|
825
719
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
if (graph.files.size > 0 && edgeCount > 0) {
|
|
834
|
-
pass(`Module graph: ${graph.files.size} files, ${edgeCount} edges [${elapsed}ms]`);
|
|
835
|
-
} else if (graph.files.size > 0) {
|
|
836
|
-
warn(
|
|
837
|
-
`Module graph: ${graph.files.size} files but 0 edges`,
|
|
838
|
-
"Files found but no internal imports resolved. Check tsconfig references."
|
|
839
|
-
);
|
|
840
|
-
} else {
|
|
841
|
-
fail(
|
|
842
|
-
"Module graph: 0 files discovered",
|
|
843
|
-
"Check tsconfig.json includes source files and project root is correct"
|
|
844
|
-
);
|
|
720
|
+
}
|
|
721
|
+
const revEdges = graph.reverse.get(filePath) ?? [];
|
|
722
|
+
for (const revEdge of revEdges) {
|
|
723
|
+
const fwdEdges = graph.forward.get(revEdge.target);
|
|
724
|
+
if (fwdEdges) {
|
|
725
|
+
const idx = fwdEdges.findIndex((e) => e.target === filePath);
|
|
726
|
+
if (idx !== -1) fwdEdges.splice(idx, 1);
|
|
845
727
|
}
|
|
846
|
-
} catch (err) {
|
|
847
|
-
fail(
|
|
848
|
-
`Module graph build failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
849
|
-
"Check that oxc-parser and oxc-resolver are installed correctly"
|
|
850
|
-
);
|
|
851
728
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
729
|
+
graph.forward.delete(filePath);
|
|
730
|
+
graph.reverse.delete(filePath);
|
|
731
|
+
graph.files.delete(filePath);
|
|
732
|
+
}
|
|
733
|
+
function startWatcher(projectRoot3, graph, resolver, hooks, excludedPaths2 = []) {
|
|
734
|
+
try {
|
|
735
|
+
const normalizedExclusions = normalizeExcludedPaths(projectRoot3, excludedPaths2);
|
|
736
|
+
const watcher = fs2.watch(
|
|
737
|
+
projectRoot3,
|
|
738
|
+
{ recursive: true },
|
|
739
|
+
(_eventType, filename) => {
|
|
740
|
+
if (!filename) return;
|
|
741
|
+
const ext = path3.extname(filename);
|
|
742
|
+
if (!TS_EXTENSIONS.has(ext)) return;
|
|
743
|
+
const parts = filename.split(path3.sep);
|
|
744
|
+
if (parts.some((p2) => SKIP_DIRS.has(p2))) return;
|
|
745
|
+
if (SKIP_FILES.has(path3.basename(filename))) return;
|
|
746
|
+
if (filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts"))
|
|
747
|
+
return;
|
|
748
|
+
const absPath2 = path3.resolve(projectRoot3, filename);
|
|
749
|
+
if (isExcluded(absPath2, normalizedExclusions)) return;
|
|
750
|
+
if (fs2.existsSync(absPath2)) {
|
|
751
|
+
updateFile(graph, absPath2, resolver, projectRoot3);
|
|
752
|
+
void hooks?.onFileUpdated?.(absPath2);
|
|
862
753
|
} else {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
`Add to ${config.propertyName} in ${config.fileName}:
|
|
866
|
-
"${parentDir}/**",`
|
|
867
|
-
);
|
|
754
|
+
removeFile(graph, absPath2);
|
|
755
|
+
void hooks?.onFileDeleted?.(absPath2);
|
|
868
756
|
}
|
|
869
757
|
}
|
|
870
|
-
} else {
|
|
871
|
-
skip("Lint config check (no ESLint or Oxlint config found)");
|
|
872
|
-
}
|
|
873
|
-
} else {
|
|
874
|
-
skip("Lint config check (typegraph-mcp is external to project)");
|
|
875
|
-
}
|
|
876
|
-
const gitignorePath = path3.resolve(projectRoot3, ".gitignore");
|
|
877
|
-
if (fs2.existsSync(gitignorePath)) {
|
|
878
|
-
const gitignoreContent = fs2.readFileSync(gitignorePath, "utf-8");
|
|
879
|
-
const lines = gitignoreContent.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
880
|
-
const ignoresClaude = lines.some(
|
|
881
|
-
(l) => l === ".claude/" || l === ".claude" || l === "/.claude"
|
|
882
|
-
);
|
|
883
|
-
const parentDir = toolIsEmbedded ? path3.basename(path3.dirname(toolDir)) : null;
|
|
884
|
-
const ignoresParent = parentDir && lines.some((l) => l === `${parentDir}/` || l === parentDir || l === `/${parentDir}`);
|
|
885
|
-
if (!ignoresParent && !ignoresClaude) {
|
|
886
|
-
pass(".gitignore does not exclude .claude/" + (parentDir ? ` or ${parentDir}/` : ""));
|
|
887
|
-
} else {
|
|
888
|
-
const excluded = [];
|
|
889
|
-
if (ignoresParent) excluded.push(`${parentDir}/`);
|
|
890
|
-
if (ignoresClaude) excluded.push(".claude/");
|
|
891
|
-
warn(
|
|
892
|
-
`.gitignore excludes ${excluded.join(" and ")}`,
|
|
893
|
-
"Remove these entries so MCP config and tool source are tracked in git"
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
} else {
|
|
897
|
-
skip(".gitignore check (no .gitignore)");
|
|
898
|
-
}
|
|
899
|
-
console.log("");
|
|
900
|
-
const total = passed + failed;
|
|
901
|
-
if (failed === 0) {
|
|
902
|
-
console.log(
|
|
903
|
-
`${passed}/${total} checks passed` + (warned > 0 ? ` (${warned} warning${warned > 1 ? "s" : ""})` : "") + " -- typegraph-mcp is ready"
|
|
904
|
-
);
|
|
905
|
-
} else {
|
|
906
|
-
console.log(
|
|
907
|
-
`${passed}/${total} checks passed, ${failed} failed` + (warned > 0 ? `, ${warned} warning${warned > 1 ? "s" : ""}` : "") + " -- fix issues above"
|
|
908
758
|
);
|
|
759
|
+
process.on("SIGINT", () => watcher.close());
|
|
760
|
+
process.on("SIGTERM", () => watcher.close());
|
|
761
|
+
log2("File watcher started");
|
|
762
|
+
} catch (err) {
|
|
763
|
+
log2("Failed to start file watcher:", err);
|
|
909
764
|
}
|
|
910
|
-
console.log("");
|
|
911
|
-
return { passed, failed, warned };
|
|
912
765
|
}
|
|
913
|
-
var
|
|
914
|
-
var
|
|
915
|
-
"
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
"
|
|
920
|
-
"
|
|
921
|
-
"
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
".
|
|
925
|
-
"
|
|
926
|
-
"
|
|
927
|
-
"
|
|
928
|
-
"
|
|
929
|
-
];
|
|
766
|
+
var log2, TS_EXTENSIONS, SKIP_DIRS, SKIP_FILES, SOURCE_EXTS;
|
|
767
|
+
var init_module_graph = __esm({
|
|
768
|
+
"module-graph.ts"() {
|
|
769
|
+
log2 = (...args2) => console.error("[typegraph/graph]", ...args2);
|
|
770
|
+
TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
771
|
+
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
772
|
+
"node_modules",
|
|
773
|
+
"dist",
|
|
774
|
+
"build",
|
|
775
|
+
"out",
|
|
776
|
+
".wrangler",
|
|
777
|
+
".mf",
|
|
778
|
+
".git",
|
|
779
|
+
".next",
|
|
780
|
+
".turbo",
|
|
781
|
+
"coverage"
|
|
782
|
+
]);
|
|
783
|
+
SKIP_FILES = /* @__PURE__ */ new Set(["routeTree.gen.ts"]);
|
|
784
|
+
SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts"];
|
|
930
785
|
}
|
|
931
786
|
});
|
|
932
787
|
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
788
|
+
// check.ts
|
|
789
|
+
var check_exports = {};
|
|
790
|
+
__export(check_exports, {
|
|
791
|
+
main: () => main
|
|
792
|
+
});
|
|
936
793
|
import * as fs3 from "fs";
|
|
794
|
+
import * as path4 from "path";
|
|
937
795
|
import { createRequire as createRequire2 } from "module";
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
"
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
this.projectRoot = projectRoot3;
|
|
946
|
-
this.tsconfigPath = tsconfigPath3;
|
|
947
|
-
}
|
|
948
|
-
child = null;
|
|
949
|
-
seq = 0;
|
|
950
|
-
pending = /* @__PURE__ */ new Map();
|
|
951
|
-
openFiles = /* @__PURE__ */ new Set();
|
|
952
|
-
buffer = Buffer.alloc(0);
|
|
953
|
-
ready = false;
|
|
954
|
-
shuttingDown = false;
|
|
955
|
-
restartCount = 0;
|
|
956
|
-
maxRestarts = 3;
|
|
957
|
-
projectsDirty = false;
|
|
958
|
-
// ─── Path Resolution ────────────────────────────────────────────────────
|
|
959
|
-
resolvePath(file) {
|
|
960
|
-
return path4.isAbsolute(file) ? file : path4.resolve(this.projectRoot, file);
|
|
961
|
-
}
|
|
962
|
-
relativePath(file) {
|
|
963
|
-
return path4.relative(this.projectRoot, file);
|
|
964
|
-
}
|
|
965
|
-
/** Read a line from a file (1-based line number). Returns trimmed content. */
|
|
966
|
-
readLine(file, line) {
|
|
967
|
-
try {
|
|
968
|
-
const absPath2 = this.resolvePath(file);
|
|
969
|
-
const content = fs3.readFileSync(absPath2, "utf-8");
|
|
970
|
-
const lines = content.split("\n");
|
|
971
|
-
return lines[line - 1]?.trim() ?? "";
|
|
972
|
-
} catch {
|
|
973
|
-
return "";
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
977
|
-
async start() {
|
|
978
|
-
if (this.child) return;
|
|
979
|
-
const require2 = createRequire2(path4.resolve(this.projectRoot, "package.json"));
|
|
980
|
-
const tsserverPath = require2.resolve("typescript/lib/tsserver.js");
|
|
981
|
-
log2(`Spawning tsserver: ${tsserverPath}`);
|
|
982
|
-
log2(`Project root: ${this.projectRoot}`);
|
|
983
|
-
log2(`tsconfig: ${this.tsconfigPath}`);
|
|
984
|
-
this.child = spawn2("node", [tsserverPath, "--disableAutomaticTypingAcquisition"], {
|
|
985
|
-
cwd: this.projectRoot,
|
|
986
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
987
|
-
env: { ...process.env, TSS_LOG: void 0 }
|
|
988
|
-
});
|
|
989
|
-
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
990
|
-
this.child.stderr.on("data", (chunk) => {
|
|
991
|
-
const text = chunk.toString().trim();
|
|
992
|
-
if (text) log2(`[stderr] ${text}`);
|
|
993
|
-
});
|
|
994
|
-
this.child.on("close", (code) => {
|
|
995
|
-
log2(`tsserver exited with code ${code}`);
|
|
996
|
-
this.child = null;
|
|
997
|
-
this.rejectAllPending(new Error(`tsserver exited with code ${code}`));
|
|
998
|
-
this.tryRestart();
|
|
999
|
-
});
|
|
1000
|
-
this.child.on("error", (err) => {
|
|
1001
|
-
log2(`tsserver error: ${err.message}`);
|
|
1002
|
-
this.rejectAllPending(err);
|
|
1003
|
-
});
|
|
1004
|
-
await this.sendRequest("configure", {
|
|
1005
|
-
preferences: {
|
|
1006
|
-
disableSuggestions: true
|
|
1007
|
-
}
|
|
1008
|
-
});
|
|
1009
|
-
const warmStart = performance.now();
|
|
1010
|
-
const tsconfigAbs = this.resolvePath(this.tsconfigPath);
|
|
1011
|
-
if (fs3.existsSync(tsconfigAbs)) {
|
|
1012
|
-
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
1013
|
-
options: { allowJs: true, checkJs: false }
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
this.ready = true;
|
|
1017
|
-
log2(`Ready [${(performance.now() - warmStart).toFixed(0)}ms configure]`);
|
|
1018
|
-
}
|
|
1019
|
-
shutdown() {
|
|
1020
|
-
this.shuttingDown = true;
|
|
1021
|
-
if (this.child) {
|
|
1022
|
-
this.child.kill("SIGTERM");
|
|
1023
|
-
this.child = null;
|
|
1024
|
-
}
|
|
1025
|
-
this.rejectAllPending(new Error("Client shutdown"));
|
|
796
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
797
|
+
function findFirstTsFile(dir, excludedPaths2 = []) {
|
|
798
|
+
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".wrangler", "coverage"]);
|
|
799
|
+
try {
|
|
800
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
801
|
+
if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
802
|
+
return path4.join(dir, entry.name);
|
|
1026
803
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
const filesToReopen = [...this.openFiles];
|
|
1037
|
-
this.openFiles.clear();
|
|
1038
|
-
this.start().then(async () => {
|
|
1039
|
-
for (const file of filesToReopen) {
|
|
1040
|
-
await this.ensureOpen(file).catch(() => {
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
804
|
+
}
|
|
805
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
806
|
+
if (entry.isDirectory() && !skipDirs.has(entry.name) && !entry.name.startsWith(".")) {
|
|
807
|
+
const childDir = path4.join(dir, entry.name);
|
|
808
|
+
if (excludedPaths2.some(
|
|
809
|
+
(excludedPath) => childDir === excludedPath || childDir.startsWith(excludedPath + path4.sep)
|
|
810
|
+
)) continue;
|
|
811
|
+
const found = findFirstTsFile(childDir, excludedPaths2);
|
|
812
|
+
if (found) return found;
|
|
1044
813
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
814
|
+
}
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
function testTsserver(projectRoot3, resolution) {
|
|
820
|
+
return new Promise((resolve10) => {
|
|
821
|
+
const child = spawn2("node", [resolution.path, "--disableAutomaticTypingAcquisition"], {
|
|
822
|
+
cwd: projectRoot3,
|
|
823
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
824
|
+
});
|
|
825
|
+
const timeout = setTimeout(() => {
|
|
826
|
+
child.kill();
|
|
827
|
+
resolve10(false);
|
|
828
|
+
}, 1e4);
|
|
829
|
+
let buffer = "";
|
|
830
|
+
child.stdout.on("data", (chunk) => {
|
|
831
|
+
buffer += chunk.toString();
|
|
832
|
+
if (buffer.includes('"success":true')) {
|
|
833
|
+
clearTimeout(timeout);
|
|
834
|
+
child.kill();
|
|
835
|
+
resolve10(true);
|
|
1051
836
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
837
|
+
});
|
|
838
|
+
child.on("error", () => {
|
|
839
|
+
clearTimeout(timeout);
|
|
840
|
+
resolve10(false);
|
|
841
|
+
});
|
|
842
|
+
child.on("exit", () => {
|
|
843
|
+
clearTimeout(timeout);
|
|
844
|
+
});
|
|
845
|
+
const request = JSON.stringify({
|
|
846
|
+
seq: 1,
|
|
847
|
+
type: "request",
|
|
848
|
+
command: "configure",
|
|
849
|
+
arguments: {
|
|
850
|
+
preferences: { disableSuggestions: true }
|
|
1056
851
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
852
|
+
});
|
|
853
|
+
child.stdin.write(request + "\n");
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function readProjectCodexConfig(projectRoot3) {
|
|
857
|
+
const configPath = path4.resolve(projectRoot3, ".codex/config.toml");
|
|
858
|
+
if (!fs3.existsSync(configPath)) return null;
|
|
859
|
+
return fs3.readFileSync(configPath, "utf-8");
|
|
860
|
+
}
|
|
861
|
+
function hasCodexTypegraphRegistration(content) {
|
|
862
|
+
return /\[mcp_servers\.typegraph\]/.test(content);
|
|
863
|
+
}
|
|
864
|
+
function hasCodexTsxLauncher(content) {
|
|
865
|
+
return /command\s*=\s*"[^"]*tsx(?:\.cmd)?"/.test(content) || /args\s*=\s*\[[\s\S]*"tsx"/.test(content);
|
|
866
|
+
}
|
|
867
|
+
function hasCompleteCodexTypegraphRegistration(content) {
|
|
868
|
+
return hasCodexTypegraphRegistration(content) && /command\s*=\s*"[^"]+"/.test(content) && /args\s*=\s*\[[\s\S]*\]/.test(content) && hasCodexTsxLauncher(content) && /TYPEGRAPH_PROJECT_ROOT\s*=/.test(content) && /TYPEGRAPH_TSCONFIG\s*=/.test(content);
|
|
869
|
+
}
|
|
870
|
+
function hasTrustedCodexProject(projectRoot3) {
|
|
871
|
+
const home = process.env.HOME;
|
|
872
|
+
if (!home) return null;
|
|
873
|
+
const globalConfigPath = path4.join(home, ".codex/config.toml");
|
|
874
|
+
if (!fs3.existsSync(globalConfigPath)) return null;
|
|
875
|
+
const lines = fs3.readFileSync(globalConfigPath, "utf-8").split(/\r?\n/);
|
|
876
|
+
let currentProject = null;
|
|
877
|
+
let currentTrusted = false;
|
|
878
|
+
const matchesTrustedProject = () => currentProject !== null && currentTrusted && (projectRoot3 === currentProject || projectRoot3.startsWith(currentProject + path4.sep));
|
|
879
|
+
for (const line of lines) {
|
|
880
|
+
const sectionMatch = line.match(/^\[projects\."([^"]+)"\]\s*$/);
|
|
881
|
+
if (sectionMatch) {
|
|
882
|
+
if (matchesTrustedProject()) return true;
|
|
883
|
+
currentProject = path4.resolve(sectionMatch[1]);
|
|
884
|
+
currentTrusted = false;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (line.startsWith("[")) {
|
|
888
|
+
if (matchesTrustedProject()) return true;
|
|
889
|
+
currentProject = null;
|
|
890
|
+
currentTrusted = false;
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (currentProject && /\btrust_level\s*=\s*"trusted"/.test(line)) {
|
|
894
|
+
currentTrusted = true;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return matchesTrustedProject();
|
|
898
|
+
}
|
|
899
|
+
function hasCompleteJsonTypegraphRegistration(config, serverName, projectRoot3) {
|
|
900
|
+
if (typeof config !== "object" || config === null) return false;
|
|
901
|
+
const servers = config["mcpServers"];
|
|
902
|
+
if (typeof servers !== "object" || servers === null) return false;
|
|
903
|
+
const entry = servers[serverName];
|
|
904
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
905
|
+
const record = entry;
|
|
906
|
+
const command2 = record["command"];
|
|
907
|
+
const args2 = record["args"];
|
|
908
|
+
const env = record["env"];
|
|
909
|
+
const serializedArgs = JSON.stringify(args2 ?? []);
|
|
910
|
+
return typeof command2 === "string" && command2.length > 0 && Array.isArray(args2) && serializedArgs.includes("server.ts") && serializedArgs.includes("tsx") && typeof env === "object" && env !== null && env["TYPEGRAPH_PROJECT_ROOT"] === projectRoot3 && typeof env["TYPEGRAPH_TSCONFIG"] === "string";
|
|
911
|
+
}
|
|
912
|
+
function readJsonConfig(configPath) {
|
|
913
|
+
if (!fs3.existsSync(configPath)) return null;
|
|
914
|
+
try {
|
|
915
|
+
return JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
916
|
+
} catch {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function getAntigravityMcpConfigPaths() {
|
|
921
|
+
const home = process.env.HOME || "";
|
|
922
|
+
return [
|
|
923
|
+
path4.join(home, ".gemini/antigravity/mcp_config.json"),
|
|
924
|
+
path4.join(home, ".gemini/antigravity-cli/plugins/typegraph-mcp/mcp_config.json")
|
|
925
|
+
];
|
|
926
|
+
}
|
|
927
|
+
function findAntigravityRegistration(projectRoot3) {
|
|
928
|
+
const home = process.env.HOME || "";
|
|
929
|
+
for (const configPath of getAntigravityMcpConfigPaths()) {
|
|
930
|
+
const config = readJsonConfig(configPath);
|
|
931
|
+
if (hasCompleteJsonTypegraphRegistration(config, "typegraph-mcp", projectRoot3)) {
|
|
932
|
+
return configPath.replace(home, "~");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
function findLintConfigs(projectRoot3) {
|
|
938
|
+
const configs = [];
|
|
939
|
+
for (const fileName of ESLINT_CONFIG_NAMES) {
|
|
940
|
+
const fullPath = path4.resolve(projectRoot3, fileName);
|
|
941
|
+
if (fs3.existsSync(fullPath)) {
|
|
942
|
+
configs.push({ tool: "ESLint", fileName, fullPath, propertyName: "ignores" });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const fileName of OXLINT_CONFIG_NAMES) {
|
|
946
|
+
const fullPath = path4.resolve(projectRoot3, fileName);
|
|
947
|
+
if (fs3.existsSync(fullPath)) {
|
|
948
|
+
configs.push({ tool: "Oxlint", fileName, fullPath, propertyName: "ignorePatterns" });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
for (const fileName of BIOME_CONFIG_NAMES) {
|
|
952
|
+
const fullPath = path4.resolve(projectRoot3, fileName);
|
|
953
|
+
if (fs3.existsSync(fullPath)) {
|
|
954
|
+
configs.push({ tool: "Biome", fileName, fullPath, propertyName: "files.includes" });
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return configs;
|
|
959
|
+
}
|
|
960
|
+
async function main(configOverride) {
|
|
961
|
+
const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir: toolDir2, toolIsEmbedded: toolIsEmbedded2, toolRelPath } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
962
|
+
const excludedPaths2 = toolIsEmbedded2 ? [toolDir2] : [];
|
|
963
|
+
let passed = 0;
|
|
964
|
+
let failed = 0;
|
|
965
|
+
let warned = 0;
|
|
966
|
+
function pass(msg) {
|
|
967
|
+
console.log(` \u2713 ${msg}`);
|
|
968
|
+
passed++;
|
|
969
|
+
}
|
|
970
|
+
function fail(msg, fix) {
|
|
971
|
+
console.log(` \u2717 ${msg}`);
|
|
972
|
+
console.log(` Fix: ${fix}`);
|
|
973
|
+
failed++;
|
|
974
|
+
}
|
|
975
|
+
function warn(msg, note) {
|
|
976
|
+
console.log(` ! ${msg}`);
|
|
977
|
+
console.log(` ${note}`);
|
|
978
|
+
warned++;
|
|
979
|
+
}
|
|
980
|
+
function skip(msg) {
|
|
981
|
+
console.log(` - ${msg} (skipped)`);
|
|
982
|
+
}
|
|
983
|
+
console.log("");
|
|
984
|
+
console.log("typegraph-mcp Health Check");
|
|
985
|
+
console.log("=======================");
|
|
986
|
+
console.log(`Project root: ${projectRoot3}`);
|
|
987
|
+
console.log("");
|
|
988
|
+
const nodeVersion = process.version;
|
|
989
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
990
|
+
if (nodeMajor >= 22) {
|
|
991
|
+
pass(`Node.js ${nodeVersion} (>= 22 required)`);
|
|
992
|
+
} else {
|
|
993
|
+
fail(`Node.js ${nodeVersion} is too old`, "Upgrade Node.js to >= 22");
|
|
994
|
+
}
|
|
995
|
+
const tsxInRoot = fs3.existsSync(path4.join(projectRoot3, "node_modules/.bin/tsx"));
|
|
996
|
+
const tsxInTool = fs3.existsSync(path4.join(toolDir2, "node_modules/.bin/tsx"));
|
|
997
|
+
if (tsxInRoot || tsxInTool) {
|
|
998
|
+
pass(`tsx available (in ${tsxInRoot ? "project" : "tool"} node_modules)`);
|
|
999
|
+
} else {
|
|
1000
|
+
pass("tsx available (via npx/global)");
|
|
1001
|
+
}
|
|
1002
|
+
let tsResolution = null;
|
|
1003
|
+
try {
|
|
1004
|
+
tsResolution = resolveTsServer(projectRoot3);
|
|
1005
|
+
if (tsResolution.source === "project") {
|
|
1006
|
+
pass(`TypeScript found (v${tsResolution.version})`);
|
|
1007
|
+
} else if (tsResolution.projectVersion) {
|
|
1008
|
+
pass(
|
|
1009
|
+
`Compatible tsserver found (TypeScript v${tsResolution.version} tool runtime; project uses v${tsResolution.projectVersion})`
|
|
1010
|
+
);
|
|
1011
|
+
} else {
|
|
1012
|
+
pass(`TypeScript found in typegraph-mcp (v${tsResolution.version})`);
|
|
1013
|
+
}
|
|
1014
|
+
} catch {
|
|
1015
|
+
fail(
|
|
1016
|
+
"Compatible tsserver runtime not found",
|
|
1017
|
+
`Run \`cd ${toolRelPath} && npm install --include=optional\``
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
const tsconfigAbs = path4.resolve(projectRoot3, tsconfigPath3);
|
|
1021
|
+
if (fs3.existsSync(tsconfigAbs)) {
|
|
1022
|
+
pass(`tsconfig.json exists at ${tsconfigPath3}`);
|
|
1023
|
+
} else {
|
|
1024
|
+
pass("No tsconfig.json; semantic tools will use an inferred TypeScript project");
|
|
1025
|
+
}
|
|
1026
|
+
const pluginMcpPath = path4.join(toolDir2, ".mcp.json");
|
|
1027
|
+
const hasPluginMcp = fs3.existsSync(pluginMcpPath) && fs3.existsSync(path4.join(toolDir2, ".claude-plugin/plugin.json"));
|
|
1028
|
+
const projectCodexConfig = readProjectCodexConfig(projectRoot3);
|
|
1029
|
+
const hasProjectCodexRegistration = projectCodexConfig !== null && hasCompleteCodexTypegraphRegistration(projectCodexConfig);
|
|
1030
|
+
const codexGet = spawnSync("codex", ["mcp", "get", "typegraph"], {
|
|
1031
|
+
stdio: "pipe",
|
|
1032
|
+
encoding: "utf-8"
|
|
1033
|
+
});
|
|
1034
|
+
const hasGlobalCodexRegistration = codexGet.status === 0;
|
|
1035
|
+
const antigravityRegistrationPath = findAntigravityRegistration(projectRoot3);
|
|
1036
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
1037
|
+
pass("MCP registered via plugin (CLAUDE_PLUGIN_ROOT set)");
|
|
1038
|
+
} else if (hasPluginMcp) {
|
|
1039
|
+
pass("MCP registered via plugin (.mcp.json + .claude-plugin/ present)");
|
|
1040
|
+
} else if (projectCodexConfig !== null) {
|
|
1041
|
+
const codexConfigPath = path4.resolve(projectRoot3, ".codex/config.toml");
|
|
1042
|
+
const hasSection = hasCodexTypegraphRegistration(projectCodexConfig);
|
|
1043
|
+
const hasCommand = /command\s*=\s*"[^"]+"/.test(projectCodexConfig);
|
|
1044
|
+
const hasArgs = /args\s*=\s*\[[\s\S]*\]/.test(projectCodexConfig);
|
|
1045
|
+
const hasTsxLauncher = hasCodexTsxLauncher(projectCodexConfig);
|
|
1046
|
+
const hasEnvRoot = /TYPEGRAPH_PROJECT_ROOT\s*=/.test(projectCodexConfig);
|
|
1047
|
+
const hasEnvTsconfig = /TYPEGRAPH_TSCONFIG\s*=/.test(projectCodexConfig);
|
|
1048
|
+
if (hasProjectCodexRegistration) {
|
|
1049
|
+
pass("MCP registered in project .codex/config.toml");
|
|
1050
|
+
const trusted = hasTrustedCodexProject(projectRoot3);
|
|
1051
|
+
if (trusted === false) {
|
|
1052
|
+
warn(
|
|
1053
|
+
"Project Codex config may be ignored",
|
|
1054
|
+
'Add the project (or a parent directory) to ~/.codex/config.toml with trust_level = "trusted"'
|
|
1055
|
+
);
|
|
1081
1056
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1057
|
+
} else {
|
|
1058
|
+
const issues = [];
|
|
1059
|
+
if (!hasSection) issues.push("[mcp_servers.typegraph] section is missing");
|
|
1060
|
+
if (!hasCommand) issues.push("command is missing");
|
|
1061
|
+
if (!hasArgs) issues.push("args are missing");
|
|
1062
|
+
if (!hasTsxLauncher) issues.push("command should point to tsx or args should include 'tsx'");
|
|
1063
|
+
if (!hasEnvRoot) issues.push("TYPEGRAPH_PROJECT_ROOT is missing");
|
|
1064
|
+
if (!hasEnvTsconfig) issues.push("TYPEGRAPH_TSCONFIG is missing");
|
|
1065
|
+
fail(
|
|
1066
|
+
`Project .codex/config.toml registration incomplete: ${issues.join(", ")}`,
|
|
1067
|
+
`Update ${codexConfigPath} with a complete [mcp_servers.typegraph] entry`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
} else if (hasGlobalCodexRegistration) {
|
|
1071
|
+
pass("MCP registered in global Codex CLI config");
|
|
1072
|
+
} else if (antigravityRegistrationPath !== null) {
|
|
1073
|
+
pass(`MCP registered in Antigravity config (${antigravityRegistrationPath})`);
|
|
1074
|
+
} else {
|
|
1075
|
+
const codexConfigPath = path4.resolve(projectRoot3, ".codex/config.toml");
|
|
1076
|
+
const mcpJsonPath = path4.resolve(projectRoot3, ".claude/mcp.json");
|
|
1077
|
+
if (fs3.existsSync(mcpJsonPath)) {
|
|
1078
|
+
try {
|
|
1079
|
+
const mcpJson = JSON.parse(fs3.readFileSync(mcpJsonPath, "utf-8"));
|
|
1080
|
+
const tsNav = mcpJson?.mcpServers?.["typegraph"];
|
|
1081
|
+
if (tsNav) {
|
|
1082
|
+
const hasCommand = tsNav.command === "npx";
|
|
1083
|
+
const hasArgs = Array.isArray(tsNav.args) && tsNav.args.includes("tsx");
|
|
1084
|
+
const hasEnv = tsNav.env?.["TYPEGRAPH_PROJECT_ROOT"] && tsNav.env?.["TYPEGRAPH_TSCONFIG"];
|
|
1085
|
+
if (hasCommand && hasArgs && hasEnv) {
|
|
1086
|
+
pass("MCP registered in .claude/mcp.json");
|
|
1087
|
+
} else {
|
|
1088
|
+
const issues = [];
|
|
1089
|
+
if (!hasCommand) issues.push("command should be 'npx'");
|
|
1090
|
+
if (!hasArgs) issues.push("args should include 'tsx'");
|
|
1091
|
+
if (!hasEnv) issues.push("env should set TYPEGRAPH_PROJECT_ROOT and TYPEGRAPH_TSCONFIG");
|
|
1092
|
+
fail(
|
|
1093
|
+
`MCP registration incomplete: ${issues.join(", ")}`,
|
|
1094
|
+
"See README for correct .claude/mcp.json format"
|
|
1095
|
+
);
|
|
1095
1096
|
}
|
|
1097
|
+
} else {
|
|
1098
|
+
const serverPath = toolIsEmbedded2 ? `./${toolRelPath}/server.ts` : path4.resolve(toolDir2, "server.ts");
|
|
1099
|
+
fail(
|
|
1100
|
+
"MCP entry 'typegraph' not found in .claude/mcp.json",
|
|
1101
|
+
`Add to .claude/mcp.json:
|
|
1102
|
+
{
|
|
1103
|
+
"mcpServers": {
|
|
1104
|
+
"typegraph": {
|
|
1105
|
+
"command": "npx",
|
|
1106
|
+
"args": ["tsx", "${serverPath}"],
|
|
1107
|
+
"env": { "TYPEGRAPH_PROJECT_ROOT": ".", "TYPEGRAPH_TSCONFIG": "./tsconfig.json" }
|
|
1096
1108
|
}
|
|
1097
1109
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
type: "request",
|
|
1107
|
-
command: command2,
|
|
1108
|
-
arguments: args2
|
|
1109
|
-
};
|
|
1110
|
-
return new Promise((resolve9, reject) => {
|
|
1111
|
-
const timer = setTimeout(() => {
|
|
1112
|
-
this.pending.delete(seq);
|
|
1113
|
-
reject(new Error(`tsserver ${command2} timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
1114
|
-
}, REQUEST_TIMEOUT_MS);
|
|
1115
|
-
this.pending.set(seq, { resolve: resolve9, reject, timer, command: command2 });
|
|
1116
|
-
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
// Fire-and-forget — for commands like `open` that may not send a response
|
|
1120
|
-
sendNotification(command2, args2) {
|
|
1121
|
-
if (!this.child?.stdin?.writable) return;
|
|
1122
|
-
const seq = ++this.seq;
|
|
1123
|
-
const request = { seq, type: "request", command: command2, arguments: args2 };
|
|
1124
|
-
this.child.stdin.write(JSON.stringify(request) + "\n");
|
|
1125
|
-
}
|
|
1126
|
-
// ─── File Management ───────────────────────────────────────────────────
|
|
1127
|
-
async ensureOpen(file) {
|
|
1128
|
-
const absPath2 = this.resolvePath(file);
|
|
1129
|
-
if (this.openFiles.has(absPath2)) return;
|
|
1130
|
-
this.openFiles.add(absPath2);
|
|
1131
|
-
this.sendNotification("open", { file: absPath2 });
|
|
1132
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Mark tsserver's view of the disk as stale (a file changed that is NOT open
|
|
1136
|
-
* in tsserver). tsserver's own disk watching silently decays in long-lived
|
|
1137
|
-
* instances at monorepo scale, leaving closed-file contents frozen at project
|
|
1138
|
-
* load time and new files unassigned (they land in single-file inferred
|
|
1139
|
-
* projects, producing definition-only reference results). The next query
|
|
1140
|
-
* forces a `reloadProjects`, which re-globs configs and re-reads closed files
|
|
1141
|
-
* from disk. Open files are unaffected by design — their content is
|
|
1142
|
-
* protocol-owned and refreshed via reloadOpenFile().
|
|
1143
|
-
*/
|
|
1144
|
-
markProjectsDirty() {
|
|
1145
|
-
this.projectsDirty = true;
|
|
1146
|
-
}
|
|
1147
|
-
refreshIfDirty() {
|
|
1148
|
-
if (!this.projectsDirty) return;
|
|
1149
|
-
this.projectsDirty = false;
|
|
1150
|
-
this.sendNotification("reloadProjects");
|
|
1151
|
-
}
|
|
1152
|
-
async reloadOpenFile(file) {
|
|
1153
|
-
const absPath2 = this.resolvePath(file);
|
|
1154
|
-
if (!this.openFiles.has(absPath2)) return false;
|
|
1155
|
-
await this.sendRequest("reload", {
|
|
1156
|
-
file: absPath2,
|
|
1157
|
-
tmpfile: absPath2
|
|
1158
|
-
});
|
|
1159
|
-
return true;
|
|
1160
|
-
}
|
|
1161
|
-
closeFile(file) {
|
|
1162
|
-
const absPath2 = this.resolvePath(file);
|
|
1163
|
-
if (!this.openFiles.delete(absPath2)) return false;
|
|
1164
|
-
this.sendNotification("close", { file: absPath2 });
|
|
1165
|
-
return true;
|
|
1166
|
-
}
|
|
1167
|
-
// ─── Public API ────────────────────────────────────────────────────────
|
|
1168
|
-
async definition(file, line, offset) {
|
|
1169
|
-
this.refreshIfDirty();
|
|
1170
|
-
const absPath2 = this.resolvePath(file);
|
|
1171
|
-
await this.ensureOpen(absPath2);
|
|
1172
|
-
const body = await this.sendRequest("definition", {
|
|
1173
|
-
file: absPath2,
|
|
1174
|
-
line,
|
|
1175
|
-
offset
|
|
1176
|
-
});
|
|
1177
|
-
if (!body || !Array.isArray(body)) return [];
|
|
1178
|
-
return body.map((d) => ({
|
|
1179
|
-
...d,
|
|
1180
|
-
file: this.relativePath(d.file)
|
|
1181
|
-
}));
|
|
1182
|
-
}
|
|
1183
|
-
async references(file, line, offset) {
|
|
1184
|
-
this.refreshIfDirty();
|
|
1185
|
-
const absPath2 = this.resolvePath(file);
|
|
1186
|
-
await this.ensureOpen(absPath2);
|
|
1187
|
-
const body = await this.sendRequest("references", {
|
|
1188
|
-
file: absPath2,
|
|
1189
|
-
line,
|
|
1190
|
-
offset
|
|
1191
|
-
});
|
|
1192
|
-
if (!body?.refs) return [];
|
|
1193
|
-
return body.refs.map((r) => ({
|
|
1194
|
-
...r,
|
|
1195
|
-
file: this.relativePath(r.file)
|
|
1196
|
-
}));
|
|
1197
|
-
}
|
|
1198
|
-
async quickinfo(file, line, offset) {
|
|
1199
|
-
this.refreshIfDirty();
|
|
1200
|
-
const absPath2 = this.resolvePath(file);
|
|
1201
|
-
await this.ensureOpen(absPath2);
|
|
1202
|
-
try {
|
|
1203
|
-
const body = await this.sendRequest("quickinfo", {
|
|
1204
|
-
file: absPath2,
|
|
1205
|
-
line,
|
|
1206
|
-
offset
|
|
1207
|
-
});
|
|
1208
|
-
return body ?? null;
|
|
1209
|
-
} catch {
|
|
1210
|
-
return null;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
async navto(searchValue, maxResults = 10, file) {
|
|
1214
|
-
this.refreshIfDirty();
|
|
1215
|
-
if (file) await this.ensureOpen(file);
|
|
1216
|
-
const args2 = {
|
|
1217
|
-
searchValue,
|
|
1218
|
-
maxResultCount: maxResults
|
|
1219
|
-
};
|
|
1220
|
-
if (file) args2["file"] = this.resolvePath(file);
|
|
1221
|
-
const body = await this.sendRequest("navto", args2);
|
|
1222
|
-
if (!body || !Array.isArray(body)) return [];
|
|
1223
|
-
return body.map((item) => ({
|
|
1224
|
-
...item,
|
|
1225
|
-
file: this.relativePath(item.file)
|
|
1226
|
-
}));
|
|
1110
|
+
}`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
fail(
|
|
1115
|
+
"Failed to parse .claude/mcp.json",
|
|
1116
|
+
`Check JSON syntax: ${err instanceof Error ? err.message : String(err)}`
|
|
1117
|
+
);
|
|
1227
1118
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1119
|
+
} else {
|
|
1120
|
+
fail(
|
|
1121
|
+
"No MCP registration found",
|
|
1122
|
+
`Create ${codexConfigPath} with [mcp_servers.typegraph] or create .claude/mcp.json with typegraph server registration`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const toolNodeModules = path4.join(toolDir2, "node_modules");
|
|
1127
|
+
if (fs3.existsSync(toolNodeModules)) {
|
|
1128
|
+
const requiredPkgs = [
|
|
1129
|
+
"@modelcontextprotocol/sdk",
|
|
1130
|
+
"oxc-parser",
|
|
1131
|
+
"oxc-resolver",
|
|
1132
|
+
"typescript",
|
|
1133
|
+
"zod"
|
|
1134
|
+
];
|
|
1135
|
+
const missing = requiredPkgs.filter(
|
|
1136
|
+
(pkg) => !fs3.existsSync(path4.join(toolNodeModules, ...pkg.split("/")))
|
|
1137
|
+
);
|
|
1138
|
+
if (missing.length === 0) {
|
|
1139
|
+
pass(`Dependencies installed (${requiredPkgs.length} packages)`);
|
|
1140
|
+
} else {
|
|
1141
|
+
fail(`Missing packages: ${missing.join(", ")}`, `Run \`cd ${toolRelPath} && npm install\``);
|
|
1142
|
+
}
|
|
1143
|
+
} else {
|
|
1144
|
+
fail("typegraph-mcp dependencies not installed", `Run \`cd ${toolRelPath} && npm install\``);
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
const oxcParserReq = createRequire2(path4.join(toolDir2, "package.json"));
|
|
1148
|
+
const { parseSync: parseSync3 } = await import(oxcParserReq.resolve("oxc-parser"));
|
|
1149
|
+
const result = parseSync3("test.ts", 'import { x } from "./y";');
|
|
1150
|
+
if (result.module?.staticImports?.length === 1) {
|
|
1151
|
+
pass("oxc-parser working");
|
|
1152
|
+
} else {
|
|
1153
|
+
fail(
|
|
1154
|
+
"oxc-parser parseSync returned unexpected result",
|
|
1155
|
+
`Reinstall: \`cd ${toolRelPath} && rm -rf node_modules && npm install\``
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
fail(
|
|
1160
|
+
`oxc-parser failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1161
|
+
`Reinstall: \`cd ${toolRelPath} && rm -rf node_modules && npm install\``
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
try {
|
|
1165
|
+
const oxcResolverReq = createRequire2(path4.join(toolDir2, "package.json"));
|
|
1166
|
+
const { ResolverFactory: ResolverFactory2 } = await import(oxcResolverReq.resolve("oxc-resolver"));
|
|
1167
|
+
const resolver = new ResolverFactory2({
|
|
1168
|
+
...fs3.existsSync(tsconfigAbs) ? { tsconfig: { configFile: tsconfigAbs, references: "auto" } } : {},
|
|
1169
|
+
extensions: [".ts", ".tsx", ".js"],
|
|
1170
|
+
extensionAlias: { ".js": [".ts", ".tsx", ".js"] }
|
|
1171
|
+
});
|
|
1172
|
+
let resolveOk = false;
|
|
1173
|
+
const testFile = findFirstTsFile(projectRoot3, excludedPaths2);
|
|
1174
|
+
if (testFile) {
|
|
1175
|
+
const dir = path4.dirname(testFile);
|
|
1176
|
+
const base = "./" + path4.basename(testFile);
|
|
1177
|
+
const result = resolver.sync(dir, base);
|
|
1178
|
+
resolveOk = !!result.path;
|
|
1179
|
+
}
|
|
1180
|
+
if (resolveOk) {
|
|
1181
|
+
pass("oxc-resolver working");
|
|
1182
|
+
} else {
|
|
1183
|
+
warn(
|
|
1184
|
+
"oxc-resolver loaded but couldn't resolve a test import",
|
|
1185
|
+
"Check tsconfig.json is valid and has correct `references`"
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
fail(
|
|
1190
|
+
`oxc-resolver failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1191
|
+
`Reinstall: \`cd ${toolRelPath} && rm -rf node_modules && npm install\``
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (tsResolution) {
|
|
1195
|
+
try {
|
|
1196
|
+
const ok = await testTsserver(projectRoot3, tsResolution);
|
|
1197
|
+
if (ok) {
|
|
1198
|
+
pass("tsserver responds to configure");
|
|
1199
|
+
} else {
|
|
1200
|
+
fail(
|
|
1201
|
+
"tsserver did not respond",
|
|
1202
|
+
"Verify `typescript` is installed and tsconfig.json is valid"
|
|
1203
|
+
);
|
|
1236
1204
|
}
|
|
1237
|
-
}
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
fail(
|
|
1207
|
+
`tsserver failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
1208
|
+
"Verify `typescript` is installed and tsconfig.json is valid"
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
skip("tsserver test (TypeScript not found)");
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
let buildGraph2;
|
|
1216
|
+
try {
|
|
1217
|
+
({ buildGraph: buildGraph2 } = await import(path4.resolve(toolDir2, "module-graph.js")));
|
|
1218
|
+
} catch {
|
|
1219
|
+
({ buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_module_graph(), module_graph_exports)));
|
|
1220
|
+
}
|
|
1221
|
+
const start2 = performance.now();
|
|
1222
|
+
const { graph } = await buildGraph2(
|
|
1223
|
+
projectRoot3,
|
|
1224
|
+
tsconfigPath3,
|
|
1225
|
+
excludedPaths2
|
|
1226
|
+
);
|
|
1227
|
+
const elapsed = (performance.now() - start2).toFixed(0);
|
|
1228
|
+
const edgeCount = [...graph.forward.values()].reduce(
|
|
1229
|
+
(s, e) => s + e.length,
|
|
1230
|
+
0
|
|
1231
|
+
);
|
|
1232
|
+
if (graph.files.size > 0 && edgeCount > 0) {
|
|
1233
|
+
pass(`Module graph: ${graph.files.size} files, ${edgeCount} edges [${elapsed}ms]`);
|
|
1234
|
+
} else if (graph.files.size > 0) {
|
|
1235
|
+
warn(
|
|
1236
|
+
`Module graph: ${graph.files.size} files but 0 edges`,
|
|
1237
|
+
"Files found but no internal imports resolved. Check tsconfig references."
|
|
1238
|
+
);
|
|
1239
|
+
} else {
|
|
1240
|
+
fail(
|
|
1241
|
+
"Module graph: 0 files discovered",
|
|
1242
|
+
"Check tsconfig.json includes source files and project root is correct"
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
fail(
|
|
1247
|
+
`Module graph build failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1248
|
+
"Check that oxc-parser and oxc-resolver are installed correctly"
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
if (toolIsEmbedded2) {
|
|
1252
|
+
const lintConfigs = findLintConfigs(projectRoot3);
|
|
1253
|
+
if (lintConfigs.length > 0) {
|
|
1254
|
+
const parentDir = path4.basename(path4.dirname(toolDir2));
|
|
1255
|
+
const parentIgnorePattern = new RegExp(`["']${parentDir}\\/\\*\\*["']`);
|
|
1256
|
+
for (const config of lintConfigs) {
|
|
1257
|
+
const content = fs3.readFileSync(config.fullPath, "utf-8");
|
|
1258
|
+
const hasParentIgnore = config.tool === "Biome" ? biomeScopeExcludes(content, parentDir) : parentIgnorePattern.test(content);
|
|
1259
|
+
if (hasParentIgnore) {
|
|
1260
|
+
pass(`${config.tool} ignores ${parentDir}/ (${config.fileName})`);
|
|
1261
|
+
} else {
|
|
1262
|
+
const expectedPattern = config.tool === "Biome" ? `!!${parentDir}` : `${parentDir}/**`;
|
|
1263
|
+
fail(
|
|
1264
|
+
`${config.tool} missing ignore: "${expectedPattern}" (${config.fileName})`,
|
|
1265
|
+
`Add to ${config.propertyName} in ${config.fileName}:
|
|
1266
|
+
"${expectedPattern}",`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
} else {
|
|
1271
|
+
skip("Lint config check (no ESLint, Oxlint, or Biome config found)");
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
skip("Lint config check (typegraph-mcp is external to project)");
|
|
1275
|
+
}
|
|
1276
|
+
const gitignorePath = path4.resolve(projectRoot3, ".gitignore");
|
|
1277
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
1278
|
+
const gitignoreContent = fs3.readFileSync(gitignorePath, "utf-8");
|
|
1279
|
+
const lines = gitignoreContent.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
1280
|
+
const ignoresClaude = lines.some(
|
|
1281
|
+
(l) => l === ".claude/" || l === ".claude" || l === "/.claude"
|
|
1282
|
+
);
|
|
1283
|
+
const parentDir = toolIsEmbedded2 ? path4.basename(path4.dirname(toolDir2)) : null;
|
|
1284
|
+
const ignoresParent = parentDir && lines.some((l) => l === `${parentDir}/` || l === parentDir || l === `/${parentDir}`);
|
|
1285
|
+
if (!ignoresParent && !ignoresClaude) {
|
|
1286
|
+
pass(".gitignore does not exclude .claude/" + (parentDir ? ` or ${parentDir}/` : ""));
|
|
1287
|
+
} else {
|
|
1288
|
+
const excluded = [];
|
|
1289
|
+
if (ignoresParent) excluded.push(`${parentDir}/`);
|
|
1290
|
+
if (ignoresClaude) excluded.push(".claude/");
|
|
1291
|
+
warn(
|
|
1292
|
+
`.gitignore excludes ${excluded.join(" and ")}`,
|
|
1293
|
+
"Remove these entries so MCP config and tool source are tracked in git"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
} else {
|
|
1297
|
+
skip(".gitignore check (no .gitignore)");
|
|
1298
|
+
}
|
|
1299
|
+
console.log("");
|
|
1300
|
+
const total = passed + failed;
|
|
1301
|
+
if (failed === 0) {
|
|
1302
|
+
console.log(
|
|
1303
|
+
`${passed}/${total} checks passed` + (warned > 0 ? ` (${warned} warning${warned > 1 ? "s" : ""})` : "") + " -- typegraph-mcp is ready"
|
|
1304
|
+
);
|
|
1305
|
+
} else {
|
|
1306
|
+
console.log(
|
|
1307
|
+
`${passed}/${total} checks passed, ${failed} failed` + (warned > 0 ? `, ${warned} warning${warned > 1 ? "s" : ""}` : "") + " -- fix issues above"
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
console.log("");
|
|
1311
|
+
return { passed, failed, warned };
|
|
1312
|
+
}
|
|
1313
|
+
var ESLINT_CONFIG_NAMES, OXLINT_CONFIG_NAMES;
|
|
1314
|
+
var init_check = __esm({
|
|
1315
|
+
"check.ts"() {
|
|
1316
|
+
init_config();
|
|
1317
|
+
init_tsserver_client();
|
|
1318
|
+
init_biome_config();
|
|
1319
|
+
ESLINT_CONFIG_NAMES = [
|
|
1320
|
+
"eslint.config.mjs",
|
|
1321
|
+
"eslint.config.js",
|
|
1322
|
+
"eslint.config.ts",
|
|
1323
|
+
"eslint.config.cjs"
|
|
1324
|
+
];
|
|
1325
|
+
OXLINT_CONFIG_NAMES = [
|
|
1326
|
+
".oxlintrc.json",
|
|
1327
|
+
"oxlint.config.ts",
|
|
1328
|
+
"oxlint.config.js",
|
|
1329
|
+
"oxlint.config.mjs",
|
|
1330
|
+
"oxlint.config.cjs"
|
|
1331
|
+
];
|
|
1238
1332
|
}
|
|
1239
1333
|
});
|
|
1240
1334
|
|
|
@@ -1518,7 +1612,8 @@ var init_graph_queries = __esm({
|
|
|
1518
1612
|
// smoke-test.ts
|
|
1519
1613
|
var smoke_test_exports = {};
|
|
1520
1614
|
__export(smoke_test_exports, {
|
|
1521
|
-
main: () => main2
|
|
1615
|
+
main: () => main2,
|
|
1616
|
+
selectQuickInfoSymbol: () => selectQuickInfoSymbol
|
|
1522
1617
|
});
|
|
1523
1618
|
import * as fs5 from "fs";
|
|
1524
1619
|
import * as path6 from "path";
|
|
@@ -1535,8 +1630,50 @@ function findInNavBar(items, predicate) {
|
|
|
1535
1630
|
}
|
|
1536
1631
|
return null;
|
|
1537
1632
|
}
|
|
1538
|
-
function
|
|
1633
|
+
function symbolPositions(symbol, sourceLines) {
|
|
1634
|
+
const span = symbol.spans[0];
|
|
1635
|
+
const positions = [];
|
|
1636
|
+
const lineText = sourceLines[span.start.line - 1];
|
|
1637
|
+
if (lineText) {
|
|
1638
|
+
const nameIndex = lineText.indexOf(symbol.text, Math.max(0, span.start.offset - 1));
|
|
1639
|
+
if (nameIndex >= 0) {
|
|
1640
|
+
positions.push({ line: span.start.line, offset: nameIndex + 1 });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (!positions.some((position) => position.offset === span.start.offset)) {
|
|
1644
|
+
positions.push(span.start);
|
|
1645
|
+
}
|
|
1646
|
+
return positions;
|
|
1647
|
+
}
|
|
1648
|
+
async function probeQuickInfo(client2, file, symbol, sourceLines) {
|
|
1649
|
+
for (const position of symbolPositions(symbol, sourceLines)) {
|
|
1650
|
+
const info = await client2.quickinfo(file, position.line, position.offset);
|
|
1651
|
+
if (info) return { info, position };
|
|
1652
|
+
}
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
async function selectQuickInfoSymbol(client2, file, symbols, sourceLines) {
|
|
1656
|
+
const kindPriority = /* @__PURE__ */ new Map([
|
|
1657
|
+
["const", 0],
|
|
1658
|
+
["let", 1],
|
|
1659
|
+
["var", 2],
|
|
1660
|
+
["class", 3],
|
|
1661
|
+
["enum", 4],
|
|
1662
|
+
["function", 5],
|
|
1663
|
+
["method", 6]
|
|
1664
|
+
]);
|
|
1665
|
+
const concreteSymbols = symbols.map((symbol, index) => ({ symbol, index })).filter(({ symbol }) => kindPriority.has(symbol.kind)).sort(
|
|
1666
|
+
(a, b) => kindPriority.get(a.symbol.kind) - kindPriority.get(b.symbol.kind) || a.index - b.index
|
|
1667
|
+
).map(({ symbol }) => symbol);
|
|
1668
|
+
for (const symbol of concreteSymbols) {
|
|
1669
|
+
const probe = await probeQuickInfo(client2, file, symbol, sourceLines);
|
|
1670
|
+
if (probe) return { symbol, ...probe };
|
|
1671
|
+
}
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
function findTestFile(rootDir, excludedPaths2 = []) {
|
|
1539
1675
|
const candidates = [];
|
|
1676
|
+
const normalizedExclusions = excludedPaths2.map((excludedPath) => path6.resolve(excludedPath));
|
|
1540
1677
|
function walk(dir, depth) {
|
|
1541
1678
|
if (depth > 5 || candidates.length >= 30) return;
|
|
1542
1679
|
let entries;
|
|
@@ -1548,7 +1685,11 @@ function findTestFile(rootDir) {
|
|
|
1548
1685
|
for (const entry of entries) {
|
|
1549
1686
|
if (entry.isDirectory()) {
|
|
1550
1687
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1551
|
-
|
|
1688
|
+
const childDir = path6.join(dir, entry.name);
|
|
1689
|
+
if (normalizedExclusions.some(
|
|
1690
|
+
(excludedPath) => childDir === excludedPath || childDir.startsWith(excludedPath + path6.sep)
|
|
1691
|
+
)) continue;
|
|
1692
|
+
walk(childDir, depth + 1);
|
|
1552
1693
|
} else if (entry.isFile()) {
|
|
1553
1694
|
const name = entry.name;
|
|
1554
1695
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts") || name.endsWith(".spec.ts"))
|
|
@@ -1556,7 +1697,7 @@ function findTestFile(rootDir) {
|
|
|
1556
1697
|
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
1557
1698
|
try {
|
|
1558
1699
|
const stat = fs5.statSync(path6.join(dir, name));
|
|
1559
|
-
if (stat.size >
|
|
1700
|
+
if (stat.size > 0 && stat.size < 5e4) {
|
|
1560
1701
|
candidates.push({ file: path6.join(dir, name), size: stat.size });
|
|
1561
1702
|
}
|
|
1562
1703
|
} catch {
|
|
@@ -1580,7 +1721,8 @@ function findImporter(graph, file) {
|
|
|
1580
1721
|
return (preferred ?? revEdges[0]).target;
|
|
1581
1722
|
}
|
|
1582
1723
|
async function main2(configOverride) {
|
|
1583
|
-
const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3 } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
1724
|
+
const { projectRoot: projectRoot3, tsconfigPath: tsconfigPath3, toolDir: toolDir2, toolIsEmbedded: toolIsEmbedded2 } = configOverride ?? resolveConfig(import.meta.dirname);
|
|
1725
|
+
const excludedPaths2 = toolIsEmbedded2 ? [toolDir2] : [];
|
|
1584
1726
|
let passed = 0;
|
|
1585
1727
|
let failed = 0;
|
|
1586
1728
|
let skipped = 0;
|
|
@@ -1603,7 +1745,7 @@ async function main2(configOverride) {
|
|
|
1603
1745
|
console.log("=====================");
|
|
1604
1746
|
console.log(`Project root: ${projectRoot3}`);
|
|
1605
1747
|
console.log("");
|
|
1606
|
-
const testFile = findTestFile(projectRoot3);
|
|
1748
|
+
const testFile = findTestFile(projectRoot3, excludedPaths2);
|
|
1607
1749
|
if (!testFile) {
|
|
1608
1750
|
console.log(" No suitable .ts file found in project. Cannot run smoke tests.");
|
|
1609
1751
|
return { passed, failed: failed + 1, skipped };
|
|
@@ -1616,7 +1758,7 @@ async function main2(configOverride) {
|
|
|
1616
1758
|
let t0;
|
|
1617
1759
|
t0 = performance.now();
|
|
1618
1760
|
try {
|
|
1619
|
-
const result = await buildGraph(projectRoot3, tsconfigPath3);
|
|
1761
|
+
const result = await buildGraph(projectRoot3, tsconfigPath3, excludedPaths2);
|
|
1620
1762
|
graph = result.graph;
|
|
1621
1763
|
const ms = performance.now() - t0;
|
|
1622
1764
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
@@ -1733,8 +1875,17 @@ async function main2(configOverride) {
|
|
|
1733
1875
|
} else {
|
|
1734
1876
|
fail("navbar", `No symbols found in ${testFileRel}`, navbarMs);
|
|
1735
1877
|
}
|
|
1736
|
-
const
|
|
1737
|
-
const
|
|
1878
|
+
const source = fs5.readFileSync(testFile, "utf-8");
|
|
1879
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1880
|
+
const quickInfoSelection = await selectQuickInfoSymbol(
|
|
1881
|
+
client2,
|
|
1882
|
+
testFileRel,
|
|
1883
|
+
allSymbols,
|
|
1884
|
+
sourceLines
|
|
1885
|
+
);
|
|
1886
|
+
const sym = quickInfoSelection?.symbol ?? allSymbols[0];
|
|
1887
|
+
const selectedQuickInfo = quickInfoSelection?.info ?? null;
|
|
1888
|
+
const selectedPosition = quickInfoSelection?.position ?? sym?.spans[0]?.start;
|
|
1738
1889
|
if (!sym) {
|
|
1739
1890
|
const toolNames = [
|
|
1740
1891
|
"find_symbol",
|
|
@@ -1749,6 +1900,7 @@ async function main2(configOverride) {
|
|
|
1749
1900
|
for (const name of toolNames) skip(name, "No symbol discovered");
|
|
1750
1901
|
} else {
|
|
1751
1902
|
const span = sym.spans[0];
|
|
1903
|
+
const point = selectedPosition ?? span.start;
|
|
1752
1904
|
t0 = performance.now();
|
|
1753
1905
|
const found = findInNavBar(bar, (item) => item.text === sym.text && item.kind === sym.kind);
|
|
1754
1906
|
if (found && found.spans.length > 0) {
|
|
@@ -1761,7 +1913,7 @@ async function main2(configOverride) {
|
|
|
1761
1913
|
fail("find_symbol", `Could not re-find ${sym.text}`, performance.now() - t0);
|
|
1762
1914
|
}
|
|
1763
1915
|
t0 = performance.now();
|
|
1764
|
-
const defs = await client2.definition(testFileRel,
|
|
1916
|
+
const defs = await client2.definition(testFileRel, point.line, point.offset);
|
|
1765
1917
|
if (defs.length > 0) {
|
|
1766
1918
|
const def = defs[0];
|
|
1767
1919
|
pass("definition", `${sym.text} -> ${def.file}:${def.start.line}`, performance.now() - t0);
|
|
@@ -1769,7 +1921,7 @@ async function main2(configOverride) {
|
|
|
1769
1921
|
pass("definition", `${sym.text} is its own definition`, performance.now() - t0);
|
|
1770
1922
|
}
|
|
1771
1923
|
t0 = performance.now();
|
|
1772
|
-
const refs = await client2.references(testFileRel,
|
|
1924
|
+
const refs = await client2.references(testFileRel, point.line, point.offset);
|
|
1773
1925
|
const refFiles = new Set(refs.map((r) => r.file));
|
|
1774
1926
|
pass(
|
|
1775
1927
|
"references",
|
|
@@ -1777,7 +1929,10 @@ async function main2(configOverride) {
|
|
|
1777
1929
|
performance.now() - t0
|
|
1778
1930
|
);
|
|
1779
1931
|
t0 = performance.now();
|
|
1780
|
-
let info =
|
|
1932
|
+
let info = selectedQuickInfo;
|
|
1933
|
+
if (!info) {
|
|
1934
|
+
info = await client2.quickinfo(testFileRel, point.line, point.offset);
|
|
1935
|
+
}
|
|
1781
1936
|
if (!info && defs.length > 0) {
|
|
1782
1937
|
const def = defs[0];
|
|
1783
1938
|
info = await client2.quickinfo(testFileRel, def.start.line, def.start.offset);
|
|
@@ -1786,7 +1941,7 @@ async function main2(configOverride) {
|
|
|
1786
1941
|
const typeStr = info.displayString.length > 80 ? info.displayString.slice(0, 80) + "..." : info.displayString;
|
|
1787
1942
|
pass("type_info", typeStr, performance.now() - t0);
|
|
1788
1943
|
} else {
|
|
1789
|
-
|
|
1944
|
+
skip("type_info", `No discovered symbol returned type info`);
|
|
1790
1945
|
}
|
|
1791
1946
|
t0 = performance.now();
|
|
1792
1947
|
const navItems = await client2.navto(sym.text, 5);
|
|
@@ -1818,7 +1973,6 @@ async function main2(configOverride) {
|
|
|
1818
1973
|
const exportSymbols = topItems.filter((item) => symbolKinds.has(item.kind));
|
|
1819
1974
|
pass("module_exports", `${exportSymbols.length} top-level symbol(s)`, performance.now() - t0);
|
|
1820
1975
|
t0 = performance.now();
|
|
1821
|
-
const source = fs5.readFileSync(testFile, "utf-8");
|
|
1822
1976
|
const importMatch = source.match(/^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/m);
|
|
1823
1977
|
if (importMatch) {
|
|
1824
1978
|
const firstName = importMatch[1].split(",")[0].replace(/^type\s+/, "").trim();
|
|
@@ -2355,14 +2509,16 @@ async function benchmarkAccuracy(client2, graph) {
|
|
|
2355
2509
|
return scenarios;
|
|
2356
2510
|
}
|
|
2357
2511
|
async function main3(config) {
|
|
2358
|
-
|
|
2512
|
+
const resolvedConfig = config ?? resolveConfig(import.meta.dirname);
|
|
2513
|
+
({ projectRoot, tsconfigPath } = resolvedConfig);
|
|
2514
|
+
const excludedPaths2 = resolvedConfig.toolIsEmbedded && resolvedConfig.toolDir ? [resolvedConfig.toolDir] : [];
|
|
2359
2515
|
console.log("");
|
|
2360
2516
|
console.log("typegraph-mcp Benchmark");
|
|
2361
2517
|
console.log("=======================");
|
|
2362
2518
|
console.log(`Project: ${projectRoot}`);
|
|
2363
2519
|
console.log("");
|
|
2364
2520
|
const graphStart = performance.now();
|
|
2365
|
-
const { graph } = await buildGraph(projectRoot, tsconfigPath);
|
|
2521
|
+
const { graph } = await buildGraph(projectRoot, tsconfigPath, excludedPaths2);
|
|
2366
2522
|
const graphMs = performance.now() - graphStart;
|
|
2367
2523
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
2368
2524
|
console.log(`Module graph: ${graph.files.size} files, ${edgeCount} edges [${graphMs.toFixed(0)}ms]`);
|
|
@@ -2716,37 +2872,44 @@ async function main4() {
|
|
|
2716
2872
|
log3(`tsconfig: ${tsconfigPath2}`);
|
|
2717
2873
|
const [, graphResult] = await Promise.all([
|
|
2718
2874
|
client.start(),
|
|
2719
|
-
buildGraph(projectRoot2, tsconfigPath2)
|
|
2875
|
+
buildGraph(projectRoot2, tsconfigPath2, excludedPaths)
|
|
2720
2876
|
]);
|
|
2721
2877
|
moduleGraph = graphResult.graph;
|
|
2722
2878
|
moduleResolver = graphResult.resolver;
|
|
2723
|
-
startWatcher(
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
(
|
|
2879
|
+
startWatcher(
|
|
2880
|
+
projectRoot2,
|
|
2881
|
+
moduleGraph,
|
|
2882
|
+
graphResult.resolver,
|
|
2883
|
+
{
|
|
2884
|
+
onFileUpdated: (filePath) => client.reloadOpenFile(filePath).then(
|
|
2885
|
+
(wasOpen) => {
|
|
2886
|
+
if (!wasOpen) client.markProjectsDirty();
|
|
2887
|
+
},
|
|
2888
|
+
(err) => {
|
|
2889
|
+
client.markProjectsDirty();
|
|
2890
|
+
log3(`Failed to reload open file ${relPath2(filePath)}:`, err);
|
|
2891
|
+
}
|
|
2892
|
+
),
|
|
2893
|
+
onFileDeleted: (filePath) => {
|
|
2894
|
+
client.closeFile(filePath);
|
|
2729
2895
|
client.markProjectsDirty();
|
|
2730
|
-
log3(`Failed to reload open file ${relPath2(filePath)}:`, err);
|
|
2731
2896
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
client.markProjectsDirty();
|
|
2736
|
-
}
|
|
2737
|
-
});
|
|
2897
|
+
},
|
|
2898
|
+
excludedPaths
|
|
2899
|
+
);
|
|
2738
2900
|
const transport = new StdioServerTransport();
|
|
2739
2901
|
await mcpServer.connect(transport);
|
|
2740
2902
|
log3("MCP server connected and ready");
|
|
2741
2903
|
}
|
|
2742
|
-
var projectRoot2, tsconfigPath2, log3, client, moduleGraph, moduleResolver, mcpServer, locationOrSymbol, exportKinds, normalizedProjectRoot;
|
|
2904
|
+
var projectRoot2, tsconfigPath2, toolDir, toolIsEmbedded, excludedPaths, log3, client, moduleGraph, moduleResolver, mcpServer, locationOrSymbol, exportKinds, normalizedProjectRoot;
|
|
2743
2905
|
var init_server = __esm({
|
|
2744
2906
|
"server.ts"() {
|
|
2745
2907
|
init_tsserver_client();
|
|
2746
2908
|
init_module_graph();
|
|
2747
2909
|
init_graph_queries();
|
|
2748
2910
|
init_config();
|
|
2749
|
-
({ projectRoot: projectRoot2, tsconfigPath: tsconfigPath2 } = resolveConfig(import.meta.dirname));
|
|
2911
|
+
({ projectRoot: projectRoot2, tsconfigPath: tsconfigPath2, toolDir, toolIsEmbedded } = resolveConfig(import.meta.dirname));
|
|
2912
|
+
excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
2750
2913
|
log3 = (...args2) => console.error("[typegraph]", ...args2);
|
|
2751
2914
|
client = new TsServerClient(projectRoot2, tsconfigPath2);
|
|
2752
2915
|
mcpServer = new McpServer({
|
|
@@ -3256,6 +3419,7 @@ var init_server = __esm({
|
|
|
3256
3419
|
});
|
|
3257
3420
|
|
|
3258
3421
|
// cli.ts
|
|
3422
|
+
init_biome_config();
|
|
3259
3423
|
init_config();
|
|
3260
3424
|
import * as fs8 from "fs";
|
|
3261
3425
|
import * as path9 from "path";
|
|
@@ -3344,6 +3508,7 @@ var AGENTS = {
|
|
|
3344
3508
|
};
|
|
3345
3509
|
var CORE_FILES = [
|
|
3346
3510
|
"server.ts",
|
|
3511
|
+
"biome-config.ts",
|
|
3347
3512
|
"module-graph.ts",
|
|
3348
3513
|
"tsserver-client.ts",
|
|
3349
3514
|
"graph-queries.ts",
|
|
@@ -3777,43 +3942,6 @@ function deregisterAntigravityMcp(projectRoot3) {
|
|
|
3777
3942
|
}
|
|
3778
3943
|
}
|
|
3779
3944
|
}
|
|
3780
|
-
function ensureTsconfigExclude(projectRoot3) {
|
|
3781
|
-
const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
|
|
3782
|
-
if (!fs8.existsSync(tsconfigPath3)) return;
|
|
3783
|
-
try {
|
|
3784
|
-
const raw = fs8.readFileSync(tsconfigPath3, "utf-8");
|
|
3785
|
-
const excludeArrayMatch = raw.match(/("exclude"\s*:\s*\[)([\s\S]*?)(\])/);
|
|
3786
|
-
if (excludeArrayMatch && /["']plugins(?:\/\*\*|\/\*|)["']/.test(excludeArrayMatch[2])) {
|
|
3787
|
-
return;
|
|
3788
|
-
}
|
|
3789
|
-
if (excludeArrayMatch) {
|
|
3790
|
-
const updated = raw.replace(
|
|
3791
|
-
/("exclude"\s*:\s*\[)([\s\S]*?)(\])/,
|
|
3792
|
-
(_match, open, items, close) => {
|
|
3793
|
-
const trimmed = items.trimEnd();
|
|
3794
|
-
const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
|
|
3795
|
-
return `${open}${items.trimEnd()}${needsComma ? "," : ""}
|
|
3796
|
-
"plugins/**"${close}`;
|
|
3797
|
-
}
|
|
3798
|
-
);
|
|
3799
|
-
fs8.writeFileSync(tsconfigPath3, updated);
|
|
3800
|
-
} else {
|
|
3801
|
-
const lastBrace = raw.lastIndexOf("}");
|
|
3802
|
-
if (lastBrace !== -1) {
|
|
3803
|
-
const before = raw.slice(0, lastBrace).trimEnd();
|
|
3804
|
-
const needsComma = !before.endsWith(",") && !before.endsWith("{");
|
|
3805
|
-
const patched = `${before}${needsComma ? "," : ""}
|
|
3806
|
-
"exclude": ["plugins/**"]
|
|
3807
|
-
}
|
|
3808
|
-
`;
|
|
3809
|
-
fs8.writeFileSync(tsconfigPath3, patched);
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
p.log.success('Added "plugins/**" to tsconfig.json exclude (prevents build errors)');
|
|
3813
|
-
} catch {
|
|
3814
|
-
p.log.warn('Could not update tsconfig.json \u2014 manually add "plugins/**" to the exclude array to prevent build errors');
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
3945
|
var ESLINT_CONFIG_NAMES2 = [
|
|
3818
3946
|
"eslint.config.mjs",
|
|
3819
3947
|
"eslint.config.js",
|
|
@@ -3846,6 +3974,13 @@ function findLintConfigs2(projectRoot3) {
|
|
|
3846
3974
|
});
|
|
3847
3975
|
}
|
|
3848
3976
|
}
|
|
3977
|
+
for (const fileName of BIOME_CONFIG_NAMES) {
|
|
3978
|
+
const fullPath = path9.resolve(projectRoot3, fileName);
|
|
3979
|
+
if (fs8.existsSync(fullPath)) {
|
|
3980
|
+
configs.push({ tool: "Biome", fileName, fullPath, format: "json" });
|
|
3981
|
+
break;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3849
3984
|
return configs;
|
|
3850
3985
|
}
|
|
3851
3986
|
function appendToArrayLiteral(raw, propertyPattern, valueLiteral) {
|
|
@@ -3912,27 +4047,37 @@ function patchOxlintModuleConfig(raw) {
|
|
|
3912
4047
|
}
|
|
3913
4048
|
return null;
|
|
3914
4049
|
}
|
|
4050
|
+
function lintPropertyName(config) {
|
|
4051
|
+
if (config.tool === "ESLint") return "ignores";
|
|
4052
|
+
if (config.tool === "Oxlint") return "ignorePatterns";
|
|
4053
|
+
return "files.includes";
|
|
4054
|
+
}
|
|
3915
4055
|
function ensureLintIgnores(projectRoot3) {
|
|
3916
4056
|
const configs = findLintConfigs2(projectRoot3);
|
|
3917
4057
|
for (const config of configs) {
|
|
3918
4058
|
try {
|
|
3919
4059
|
const raw = fs8.readFileSync(config.fullPath, "utf-8");
|
|
3920
|
-
if (/["']plugins\/\*\*["']/.test(raw))
|
|
3921
|
-
|
|
4060
|
+
if (config.tool === "Biome" ? biomeScopeExcludes(raw, "plugins") : /["']plugins\/\*\*["']/.test(raw)) {
|
|
4061
|
+
continue;
|
|
4062
|
+
}
|
|
4063
|
+
const updated = config.tool === "ESLint" ? patchEslintConfig(raw) : config.tool === "Biome" ? patchBiomeConfig(raw, "plugins") : config.format === "json" ? patchOxlintJsonConfig(raw) : patchOxlintModuleConfig(raw);
|
|
3922
4064
|
if (updated) {
|
|
3923
4065
|
fs8.writeFileSync(config.fullPath, updated);
|
|
3924
|
-
const propertyName = config
|
|
3925
|
-
|
|
4066
|
+
const propertyName = lintPropertyName(config);
|
|
4067
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
4068
|
+
p.log.success(`Added "${ignorePattern}" to ${config.fileName} ${propertyName}`);
|
|
3926
4069
|
} else {
|
|
3927
|
-
const propertyName = config
|
|
4070
|
+
const propertyName = lintPropertyName(config);
|
|
4071
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
3928
4072
|
p.log.warn(
|
|
3929
|
-
`Could not patch ${config.fileName} \u2014 manually add "
|
|
4073
|
+
`Could not patch ${config.fileName} \u2014 manually add "${ignorePattern}" to ${propertyName}`
|
|
3930
4074
|
);
|
|
3931
4075
|
}
|
|
3932
4076
|
} catch {
|
|
3933
|
-
const propertyName = config
|
|
4077
|
+
const propertyName = lintPropertyName(config);
|
|
4078
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
3934
4079
|
p.log.warn(
|
|
3935
|
-
`Could not update ${config.fileName} \u2014 manually add "
|
|
4080
|
+
`Could not update ${config.fileName} \u2014 manually add "${ignorePattern}" to ${propertyName}`
|
|
3936
4081
|
);
|
|
3937
4082
|
}
|
|
3938
4083
|
}
|
|
@@ -3978,11 +4123,12 @@ async function setup(yes2) {
|
|
|
3978
4123
|
p.cancel("No package.json found. Run this from the root of your TypeScript project.");
|
|
3979
4124
|
process.exit(1);
|
|
3980
4125
|
}
|
|
3981
|
-
if (
|
|
3982
|
-
p.
|
|
3983
|
-
|
|
4126
|
+
if (fs8.existsSync(tsconfigPath3)) {
|
|
4127
|
+
p.log.success("Found package.json and tsconfig.json");
|
|
4128
|
+
} else {
|
|
4129
|
+
p.log.success("Found package.json");
|
|
4130
|
+
p.log.info("No tsconfig.json found; semantic tools will use an inferred TypeScript project");
|
|
3984
4131
|
}
|
|
3985
|
-
p.log.success("Found package.json and tsconfig.json");
|
|
3986
4132
|
const targetDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
|
|
3987
4133
|
const isUpdate = fs8.existsSync(targetDir);
|
|
3988
4134
|
if (isUpdate && !yes2) {
|
|
@@ -4108,7 +4254,6 @@ async function setup(yes2) {
|
|
|
4108
4254
|
}
|
|
4109
4255
|
await setupAgentInstructions(projectRoot3, selectedAgents);
|
|
4110
4256
|
registerMcpServers(projectRoot3, selectedAgents);
|
|
4111
|
-
ensureTsconfigExclude(projectRoot3);
|
|
4112
4257
|
ensureLintIgnores(projectRoot3);
|
|
4113
4258
|
await runVerification(targetDir, selectedAgents);
|
|
4114
4259
|
}
|