typegraph-mcp 0.9.47 → 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 +1310 -1219
- package/dist/module-graph.js +22 -9
- package/dist/server.js +82 -38
- package/dist/smoke-test.js +66 -25
- 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 +15 -6
- 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/smoke-test-selection-test.ts +0 -67
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
|
+
}
|
|
198
199
|
});
|
|
200
|
+
const warmStart = performance.now();
|
|
201
|
+
await this.sendRequest("compilerOptionsForInferredProjects", {
|
|
202
|
+
options: { allowJs: true, checkJs: false }
|
|
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
|
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
}));
|
|
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" }
|
|
1108
|
+
}
|
|
1197
1109
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
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;
|
|
1110
|
+
}`
|
|
1111
|
+
);
|
|
1211
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
|
+
);
|
|
1212
1118
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
+
);
|
|
1227
1204
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
+
}
|
|
1236
1269
|
}
|
|
1237
|
-
}
|
|
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
|
|
|
@@ -1577,8 +1671,9 @@ async function selectQuickInfoSymbol(client2, file, symbols, sourceLines) {
|
|
|
1577
1671
|
}
|
|
1578
1672
|
return null;
|
|
1579
1673
|
}
|
|
1580
|
-
function findTestFile(rootDir) {
|
|
1674
|
+
function findTestFile(rootDir, excludedPaths2 = []) {
|
|
1581
1675
|
const candidates = [];
|
|
1676
|
+
const normalizedExclusions = excludedPaths2.map((excludedPath) => path6.resolve(excludedPath));
|
|
1582
1677
|
function walk(dir, depth) {
|
|
1583
1678
|
if (depth > 5 || candidates.length >= 30) return;
|
|
1584
1679
|
let entries;
|
|
@@ -1590,7 +1685,11 @@ function findTestFile(rootDir) {
|
|
|
1590
1685
|
for (const entry of entries) {
|
|
1591
1686
|
if (entry.isDirectory()) {
|
|
1592
1687
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1593
|
-
|
|
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);
|
|
1594
1693
|
} else if (entry.isFile()) {
|
|
1595
1694
|
const name = entry.name;
|
|
1596
1695
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts") || name.endsWith(".spec.ts"))
|
|
@@ -1598,7 +1697,7 @@ function findTestFile(rootDir) {
|
|
|
1598
1697
|
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
1599
1698
|
try {
|
|
1600
1699
|
const stat = fs5.statSync(path6.join(dir, name));
|
|
1601
|
-
if (stat.size >
|
|
1700
|
+
if (stat.size > 0 && stat.size < 5e4) {
|
|
1602
1701
|
candidates.push({ file: path6.join(dir, name), size: stat.size });
|
|
1603
1702
|
}
|
|
1604
1703
|
} catch {
|
|
@@ -1622,7 +1721,8 @@ function findImporter(graph, file) {
|
|
|
1622
1721
|
return (preferred ?? revEdges[0]).target;
|
|
1623
1722
|
}
|
|
1624
1723
|
async function main2(configOverride) {
|
|
1625
|
-
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] : [];
|
|
1626
1726
|
let passed = 0;
|
|
1627
1727
|
let failed = 0;
|
|
1628
1728
|
let skipped = 0;
|
|
@@ -1645,7 +1745,7 @@ async function main2(configOverride) {
|
|
|
1645
1745
|
console.log("=====================");
|
|
1646
1746
|
console.log(`Project root: ${projectRoot3}`);
|
|
1647
1747
|
console.log("");
|
|
1648
|
-
const testFile = findTestFile(projectRoot3);
|
|
1748
|
+
const testFile = findTestFile(projectRoot3, excludedPaths2);
|
|
1649
1749
|
if (!testFile) {
|
|
1650
1750
|
console.log(" No suitable .ts file found in project. Cannot run smoke tests.");
|
|
1651
1751
|
return { passed, failed: failed + 1, skipped };
|
|
@@ -1658,7 +1758,7 @@ async function main2(configOverride) {
|
|
|
1658
1758
|
let t0;
|
|
1659
1759
|
t0 = performance.now();
|
|
1660
1760
|
try {
|
|
1661
|
-
const result = await buildGraph(projectRoot3, tsconfigPath3);
|
|
1761
|
+
const result = await buildGraph(projectRoot3, tsconfigPath3, excludedPaths2);
|
|
1662
1762
|
graph = result.graph;
|
|
1663
1763
|
const ms = performance.now() - t0;
|
|
1664
1764
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
@@ -2409,14 +2509,16 @@ async function benchmarkAccuracy(client2, graph) {
|
|
|
2409
2509
|
return scenarios;
|
|
2410
2510
|
}
|
|
2411
2511
|
async function main3(config) {
|
|
2412
|
-
|
|
2512
|
+
const resolvedConfig = config ?? resolveConfig(import.meta.dirname);
|
|
2513
|
+
({ projectRoot, tsconfigPath } = resolvedConfig);
|
|
2514
|
+
const excludedPaths2 = resolvedConfig.toolIsEmbedded && resolvedConfig.toolDir ? [resolvedConfig.toolDir] : [];
|
|
2413
2515
|
console.log("");
|
|
2414
2516
|
console.log("typegraph-mcp Benchmark");
|
|
2415
2517
|
console.log("=======================");
|
|
2416
2518
|
console.log(`Project: ${projectRoot}`);
|
|
2417
2519
|
console.log("");
|
|
2418
2520
|
const graphStart = performance.now();
|
|
2419
|
-
const { graph } = await buildGraph(projectRoot, tsconfigPath);
|
|
2521
|
+
const { graph } = await buildGraph(projectRoot, tsconfigPath, excludedPaths2);
|
|
2420
2522
|
const graphMs = performance.now() - graphStart;
|
|
2421
2523
|
const edgeCount = [...graph.forward.values()].reduce((s, e) => s + e.length, 0);
|
|
2422
2524
|
console.log(`Module graph: ${graph.files.size} files, ${edgeCount} edges [${graphMs.toFixed(0)}ms]`);
|
|
@@ -2770,37 +2872,44 @@ async function main4() {
|
|
|
2770
2872
|
log3(`tsconfig: ${tsconfigPath2}`);
|
|
2771
2873
|
const [, graphResult] = await Promise.all([
|
|
2772
2874
|
client.start(),
|
|
2773
|
-
buildGraph(projectRoot2, tsconfigPath2)
|
|
2875
|
+
buildGraph(projectRoot2, tsconfigPath2, excludedPaths)
|
|
2774
2876
|
]);
|
|
2775
2877
|
moduleGraph = graphResult.graph;
|
|
2776
2878
|
moduleResolver = graphResult.resolver;
|
|
2777
|
-
startWatcher(
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
(
|
|
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);
|
|
2783
2895
|
client.markProjectsDirty();
|
|
2784
|
-
log3(`Failed to reload open file ${relPath2(filePath)}:`, err);
|
|
2785
2896
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
client.markProjectsDirty();
|
|
2790
|
-
}
|
|
2791
|
-
});
|
|
2897
|
+
},
|
|
2898
|
+
excludedPaths
|
|
2899
|
+
);
|
|
2792
2900
|
const transport = new StdioServerTransport();
|
|
2793
2901
|
await mcpServer.connect(transport);
|
|
2794
2902
|
log3("MCP server connected and ready");
|
|
2795
2903
|
}
|
|
2796
|
-
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;
|
|
2797
2905
|
var init_server = __esm({
|
|
2798
2906
|
"server.ts"() {
|
|
2799
2907
|
init_tsserver_client();
|
|
2800
2908
|
init_module_graph();
|
|
2801
2909
|
init_graph_queries();
|
|
2802
2910
|
init_config();
|
|
2803
|
-
({ projectRoot: projectRoot2, tsconfigPath: tsconfigPath2 } = resolveConfig(import.meta.dirname));
|
|
2911
|
+
({ projectRoot: projectRoot2, tsconfigPath: tsconfigPath2, toolDir, toolIsEmbedded } = resolveConfig(import.meta.dirname));
|
|
2912
|
+
excludedPaths = toolIsEmbedded ? [toolDir] : [];
|
|
2804
2913
|
log3 = (...args2) => console.error("[typegraph]", ...args2);
|
|
2805
2914
|
client = new TsServerClient(projectRoot2, tsconfigPath2);
|
|
2806
2915
|
mcpServer = new McpServer({
|
|
@@ -3310,6 +3419,7 @@ var init_server = __esm({
|
|
|
3310
3419
|
});
|
|
3311
3420
|
|
|
3312
3421
|
// cli.ts
|
|
3422
|
+
init_biome_config();
|
|
3313
3423
|
init_config();
|
|
3314
3424
|
import * as fs8 from "fs";
|
|
3315
3425
|
import * as path9 from "path";
|
|
@@ -3398,6 +3508,7 @@ var AGENTS = {
|
|
|
3398
3508
|
};
|
|
3399
3509
|
var CORE_FILES = [
|
|
3400
3510
|
"server.ts",
|
|
3511
|
+
"biome-config.ts",
|
|
3401
3512
|
"module-graph.ts",
|
|
3402
3513
|
"tsserver-client.ts",
|
|
3403
3514
|
"graph-queries.ts",
|
|
@@ -3831,43 +3942,6 @@ function deregisterAntigravityMcp(projectRoot3) {
|
|
|
3831
3942
|
}
|
|
3832
3943
|
}
|
|
3833
3944
|
}
|
|
3834
|
-
function ensureTsconfigExclude(projectRoot3) {
|
|
3835
|
-
const tsconfigPath3 = path9.resolve(projectRoot3, "tsconfig.json");
|
|
3836
|
-
if (!fs8.existsSync(tsconfigPath3)) return;
|
|
3837
|
-
try {
|
|
3838
|
-
const raw = fs8.readFileSync(tsconfigPath3, "utf-8");
|
|
3839
|
-
const excludeArrayMatch = raw.match(/("exclude"\s*:\s*\[)([\s\S]*?)(\])/);
|
|
3840
|
-
if (excludeArrayMatch && /["']plugins(?:\/\*\*|\/\*|)["']/.test(excludeArrayMatch[2])) {
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
|
-
if (excludeArrayMatch) {
|
|
3844
|
-
const updated = raw.replace(
|
|
3845
|
-
/("exclude"\s*:\s*\[)([\s\S]*?)(\])/,
|
|
3846
|
-
(_match, open, items, close) => {
|
|
3847
|
-
const trimmed = items.trimEnd();
|
|
3848
|
-
const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
|
|
3849
|
-
return `${open}${items.trimEnd()}${needsComma ? "," : ""}
|
|
3850
|
-
"plugins/**"${close}`;
|
|
3851
|
-
}
|
|
3852
|
-
);
|
|
3853
|
-
fs8.writeFileSync(tsconfigPath3, updated);
|
|
3854
|
-
} else {
|
|
3855
|
-
const lastBrace = raw.lastIndexOf("}");
|
|
3856
|
-
if (lastBrace !== -1) {
|
|
3857
|
-
const before = raw.slice(0, lastBrace).trimEnd();
|
|
3858
|
-
const needsComma = !before.endsWith(",") && !before.endsWith("{");
|
|
3859
|
-
const patched = `${before}${needsComma ? "," : ""}
|
|
3860
|
-
"exclude": ["plugins/**"]
|
|
3861
|
-
}
|
|
3862
|
-
`;
|
|
3863
|
-
fs8.writeFileSync(tsconfigPath3, patched);
|
|
3864
|
-
}
|
|
3865
|
-
}
|
|
3866
|
-
p.log.success('Added "plugins/**" to tsconfig.json exclude (prevents build errors)');
|
|
3867
|
-
} catch {
|
|
3868
|
-
p.log.warn('Could not update tsconfig.json \u2014 manually add "plugins/**" to the exclude array to prevent build errors');
|
|
3869
|
-
}
|
|
3870
|
-
}
|
|
3871
3945
|
var ESLINT_CONFIG_NAMES2 = [
|
|
3872
3946
|
"eslint.config.mjs",
|
|
3873
3947
|
"eslint.config.js",
|
|
@@ -3900,6 +3974,13 @@ function findLintConfigs2(projectRoot3) {
|
|
|
3900
3974
|
});
|
|
3901
3975
|
}
|
|
3902
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
|
+
}
|
|
3903
3984
|
return configs;
|
|
3904
3985
|
}
|
|
3905
3986
|
function appendToArrayLiteral(raw, propertyPattern, valueLiteral) {
|
|
@@ -3966,27 +4047,37 @@ function patchOxlintModuleConfig(raw) {
|
|
|
3966
4047
|
}
|
|
3967
4048
|
return null;
|
|
3968
4049
|
}
|
|
4050
|
+
function lintPropertyName(config) {
|
|
4051
|
+
if (config.tool === "ESLint") return "ignores";
|
|
4052
|
+
if (config.tool === "Oxlint") return "ignorePatterns";
|
|
4053
|
+
return "files.includes";
|
|
4054
|
+
}
|
|
3969
4055
|
function ensureLintIgnores(projectRoot3) {
|
|
3970
4056
|
const configs = findLintConfigs2(projectRoot3);
|
|
3971
4057
|
for (const config of configs) {
|
|
3972
4058
|
try {
|
|
3973
4059
|
const raw = fs8.readFileSync(config.fullPath, "utf-8");
|
|
3974
|
-
if (/["']plugins\/\*\*["']/.test(raw))
|
|
3975
|
-
|
|
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);
|
|
3976
4064
|
if (updated) {
|
|
3977
4065
|
fs8.writeFileSync(config.fullPath, updated);
|
|
3978
|
-
const propertyName = config
|
|
3979
|
-
|
|
4066
|
+
const propertyName = lintPropertyName(config);
|
|
4067
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
4068
|
+
p.log.success(`Added "${ignorePattern}" to ${config.fileName} ${propertyName}`);
|
|
3980
4069
|
} else {
|
|
3981
|
-
const propertyName = config
|
|
4070
|
+
const propertyName = lintPropertyName(config);
|
|
4071
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
3982
4072
|
p.log.warn(
|
|
3983
|
-
`Could not patch ${config.fileName} \u2014 manually add "
|
|
4073
|
+
`Could not patch ${config.fileName} \u2014 manually add "${ignorePattern}" to ${propertyName}`
|
|
3984
4074
|
);
|
|
3985
4075
|
}
|
|
3986
4076
|
} catch {
|
|
3987
|
-
const propertyName = config
|
|
4077
|
+
const propertyName = lintPropertyName(config);
|
|
4078
|
+
const ignorePattern = config.tool === "Biome" ? "!!plugins" : "plugins/**";
|
|
3988
4079
|
p.log.warn(
|
|
3989
|
-
`Could not update ${config.fileName} \u2014 manually add "
|
|
4080
|
+
`Could not update ${config.fileName} \u2014 manually add "${ignorePattern}" to ${propertyName}`
|
|
3990
4081
|
);
|
|
3991
4082
|
}
|
|
3992
4083
|
}
|
|
@@ -4032,11 +4123,12 @@ async function setup(yes2) {
|
|
|
4032
4123
|
p.cancel("No package.json found. Run this from the root of your TypeScript project.");
|
|
4033
4124
|
process.exit(1);
|
|
4034
4125
|
}
|
|
4035
|
-
if (
|
|
4036
|
-
p.
|
|
4037
|
-
|
|
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");
|
|
4038
4131
|
}
|
|
4039
|
-
p.log.success("Found package.json and tsconfig.json");
|
|
4040
4132
|
const targetDir = path9.resolve(projectRoot3, PLUGIN_DIR_NAME);
|
|
4041
4133
|
const isUpdate = fs8.existsSync(targetDir);
|
|
4042
4134
|
if (isUpdate && !yes2) {
|
|
@@ -4162,7 +4254,6 @@ async function setup(yes2) {
|
|
|
4162
4254
|
}
|
|
4163
4255
|
await setupAgentInstructions(projectRoot3, selectedAgents);
|
|
4164
4256
|
registerMcpServers(projectRoot3, selectedAgents);
|
|
4165
|
-
ensureTsconfigExclude(projectRoot3);
|
|
4166
4257
|
ensureLintIgnores(projectRoot3);
|
|
4167
4258
|
await runVerification(targetDir, selectedAgents);
|
|
4168
4259
|
}
|