@launchsecure/launch-kit 0.0.9 → 0.0.11
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/dist/chart-client/assets/index-DIPKWwEJ.js +404 -0
- package/dist/chart-client/assets/index-DjnSR-Hf.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BIpeUMYR.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +595 -324
- package/dist/server/cli.js +701 -326
- package/dist/server/graph/queries/db-calls.scm +8 -0
- package/dist/server/graph/queries/deep/conditions.scm +10 -0
- package/dist/server/graph/queries/deep/jsx-semantic.scm +21 -0
- package/dist/server/graph/queries/deep/request-params.scm +24 -0
- package/dist/server/graph/queries/deep/responses.scm +26 -0
- package/dist/server/graph/queries/deep/state-hooks.scm +23 -0
- package/dist/server/graph/queries/deep/variables.scm +17 -0
- package/dist/server/graph/queries/exports.scm +66 -0
- package/dist/server/graph/queries/fetch-calls.scm +57 -0
- package/dist/server/graph/queries/imports.scm +14 -0
- package/dist/server/graph/queries/jsx-elements.scm +14 -0
- package/dist/server/graph/queries/navigations.scm +85 -0
- package/dist/server/graph/queries/wrappers.scm +8 -0
- package/dist/server/graph-mcp-entry.js +710 -331
- package/package.json +5 -2
- package/dist/chart-client/assets/index-8p8Otm3A.js +0 -379
- package/dist/chart-client/assets/index-CuRWRjsg.css +0 -1
- package/dist/client/assets/index-DlwXprgf.css +0 -32
- /package/dist/client/assets/{index-B4wZOxci.js → index-qzK1AD2G.js} +0 -0
|
@@ -155,37 +155,134 @@ var init_config = __esm({
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
// src/server/graph/core/
|
|
159
|
-
function
|
|
160
|
-
if (
|
|
161
|
-
|
|
158
|
+
// src/server/graph/core/ts-extractor.ts
|
|
159
|
+
async function initTreeSitter() {
|
|
160
|
+
if (initialized) return;
|
|
161
|
+
if (initPromise) return initPromise;
|
|
162
|
+
initPromise = (async () => {
|
|
163
|
+
const TreeSitter = require("web-tree-sitter");
|
|
164
|
+
await TreeSitter.init();
|
|
165
|
+
const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
|
|
166
|
+
tsxLanguage = await TreeSitter.Language.load(wasmPath);
|
|
167
|
+
parserInstance = new TreeSitter();
|
|
168
|
+
parserInstance.setLanguage(tsxLanguage);
|
|
169
|
+
initialized = true;
|
|
170
|
+
})();
|
|
171
|
+
return initPromise;
|
|
172
|
+
}
|
|
173
|
+
function ensureInit() {
|
|
174
|
+
if (!initialized || !tsxLanguage || !parserInstance) {
|
|
175
|
+
throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
|
|
162
176
|
}
|
|
163
|
-
return tsModule;
|
|
164
177
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
178
|
+
function getQuery(name) {
|
|
179
|
+
ensureInit();
|
|
180
|
+
const cached = queryCache.get(name);
|
|
181
|
+
if (cached) return cached;
|
|
182
|
+
const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
|
|
183
|
+
const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
|
|
184
|
+
const query = tsxLanguage.query(scm);
|
|
185
|
+
queryCache.set(name, query);
|
|
186
|
+
return query;
|
|
187
|
+
}
|
|
188
|
+
function parseSource(absPath) {
|
|
189
|
+
ensureInit();
|
|
167
190
|
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
return parserInstance.parse(content);
|
|
192
|
+
}
|
|
193
|
+
function setExtractorConfig(config) {
|
|
194
|
+
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
195
|
+
extraMutationMethods = config.mutationMethods ?? [];
|
|
196
|
+
}
|
|
197
|
+
function getMutationMethods() {
|
|
198
|
+
return /* @__PURE__ */ new Set([...PRISMA_MUTATION_METHODS_BUILTIN, ...extraMutationMethods]);
|
|
199
|
+
}
|
|
200
|
+
function getFallbackDbIdentifiers() {
|
|
201
|
+
return /* @__PURE__ */ new Set([...DB_IDENTIFIERS_FALLBACK, ...extraDbIdentifiers]);
|
|
202
|
+
}
|
|
203
|
+
function looksLikeUrl(s) {
|
|
204
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
205
|
+
}
|
|
206
|
+
function templateStartsWithSlash(text) {
|
|
207
|
+
const content = text.slice(1);
|
|
208
|
+
return content.startsWith("/");
|
|
209
|
+
}
|
|
210
|
+
function captureMap(match) {
|
|
211
|
+
const map = {};
|
|
212
|
+
for (const c of match.captures) {
|
|
213
|
+
map[c.name] = c.node.text;
|
|
214
|
+
}
|
|
215
|
+
return map;
|
|
216
|
+
}
|
|
217
|
+
function childrenOfType(node, type) {
|
|
218
|
+
return node.children.filter((n) => n.type === type);
|
|
219
|
+
}
|
|
220
|
+
function childOfType(node, type) {
|
|
221
|
+
return node.children.find((n) => n.type === type);
|
|
222
|
+
}
|
|
223
|
+
function parseFileTS(absPath) {
|
|
224
|
+
const tree = parseSource(absPath);
|
|
225
|
+
const root = tree.rootNode;
|
|
226
|
+
const imports = [];
|
|
227
|
+
const importStatements = childrenOfType(root, "import_statement");
|
|
228
|
+
for (const stmt of importStatements) {
|
|
229
|
+
const sourceNode = childOfType(stmt, "string");
|
|
230
|
+
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
231
|
+
if (!frag) continue;
|
|
232
|
+
const specifier = frag.text;
|
|
233
|
+
const hasTypeKeyword = stmt.children.some(
|
|
234
|
+
(n) => n.type === "type" && n.text === "type"
|
|
235
|
+
);
|
|
236
|
+
const clause = childOfType(stmt, "import_clause");
|
|
237
|
+
if (!clause) {
|
|
238
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const names = [];
|
|
242
|
+
const typeNames = /* @__PURE__ */ new Set();
|
|
243
|
+
const defaultId = childOfType(clause, "identifier");
|
|
244
|
+
if (defaultId) names.push(defaultId.text);
|
|
245
|
+
const nsImport = childOfType(clause, "namespace_import");
|
|
246
|
+
if (nsImport) {
|
|
247
|
+
const nsId = childOfType(nsImport, "identifier");
|
|
248
|
+
if (nsId) names.push(nsId.text);
|
|
249
|
+
}
|
|
250
|
+
const namedImports = childOfType(clause, "named_imports");
|
|
251
|
+
if (namedImports) {
|
|
252
|
+
for (const spec of childrenOfType(namedImports, "import_specifier")) {
|
|
253
|
+
const identifiers = childrenOfType(spec, "identifier");
|
|
254
|
+
const importedName = identifiers.length > 1 ? identifiers[identifiers.length - 1].text : identifiers[0]?.text;
|
|
255
|
+
if (importedName) {
|
|
256
|
+
names.push(importedName);
|
|
257
|
+
const specIsType = spec.children.some(
|
|
258
|
+
(n) => n.type === "type" && n.text === "type"
|
|
259
|
+
);
|
|
260
|
+
if (specIsType) typeNames.add(importedName);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (names.length > 0 || hasTypeKeyword) {
|
|
265
|
+
imports.push({ names, specifier, isTypeOnly: hasTypeKeyword, typeNames });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const importQuery = getQuery("imports");
|
|
269
|
+
const importMatches = importQuery.matches(root);
|
|
270
|
+
for (const m of importMatches) {
|
|
271
|
+
const caps = captureMap(m);
|
|
272
|
+
if (caps["import.dynamic"]) {
|
|
273
|
+
imports.push({
|
|
274
|
+
names: [],
|
|
275
|
+
specifier: caps["import.dynamic"],
|
|
276
|
+
isTypeOnly: false,
|
|
277
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
178
281
|
const exportsSet = /* @__PURE__ */ new Set();
|
|
179
282
|
const exportsOrdered = [];
|
|
180
283
|
let defaultName = null;
|
|
181
284
|
let firstValueExport = null;
|
|
182
285
|
let firstTypeExport = null;
|
|
183
|
-
const imports = [];
|
|
184
|
-
const reExports = [];
|
|
185
|
-
const jsxElements = /* @__PURE__ */ new Set();
|
|
186
|
-
const navigations = [];
|
|
187
|
-
const fetchCalls = [];
|
|
188
|
-
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
189
286
|
function addExport(name2, kind) {
|
|
190
287
|
if (!exportsSet.has(name2)) {
|
|
191
288
|
exportsSet.add(name2);
|
|
@@ -195,298 +292,354 @@ function parseFile(absPath) {
|
|
|
195
292
|
else if (kind === "value" && !firstValueExport) firstValueExport = name2;
|
|
196
293
|
else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
|
|
197
294
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
295
|
+
const exportQuery = getQuery("exports");
|
|
296
|
+
const exportMatches = exportQuery.matches(root);
|
|
297
|
+
for (const m of exportMatches) {
|
|
298
|
+
const caps = captureMap(m);
|
|
299
|
+
if (caps["export.default.func"]) addExport(caps["export.default.func"], "default");
|
|
300
|
+
else if (caps["export.default.class"]) addExport(caps["export.default.class"], "default");
|
|
301
|
+
else if (caps["export.default.value"]) addExport(caps["export.default.value"], "default");
|
|
302
|
+
else if (caps["export.named.func"]) addExport(caps["export.named.func"], "value");
|
|
303
|
+
else if (caps["export.named.class"]) addExport(caps["export.named.class"], "value");
|
|
304
|
+
else if (caps["export.named.const"]) addExport(caps["export.named.const"], "value");
|
|
305
|
+
else if (caps["export.named.enum"]) addExport(caps["export.named.enum"], "value");
|
|
306
|
+
else if (caps["export.named.type"]) addExport(caps["export.named.type"], "type");
|
|
307
|
+
else if (caps["export.named.interface"]) addExport(caps["export.named.interface"], "type");
|
|
308
|
+
else if (caps["export.bare.name"]) addExport(caps["export.bare.name"], "value");
|
|
309
|
+
if (caps["reexport.name"]) addExport(caps["reexport.name"], "value");
|
|
310
|
+
}
|
|
311
|
+
for (const stmt of childrenOfType(root, "export_statement")) {
|
|
312
|
+
const hasDefault = stmt.children.some((n) => n.text === "default");
|
|
313
|
+
if (hasDefault && !defaultName) {
|
|
314
|
+
defaultName = "default";
|
|
315
|
+
}
|
|
201
316
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
317
|
+
const reExports = [];
|
|
318
|
+
for (const m of exportMatches) {
|
|
319
|
+
const caps = captureMap(m);
|
|
320
|
+
if (caps["reexport.name"] && caps["reexport.source"]) {
|
|
321
|
+
reExports.push({ name: caps["reexport.name"], from: caps["reexport.source"] });
|
|
205
322
|
}
|
|
206
|
-
if (
|
|
207
|
-
|
|
323
|
+
if (caps["reexport.wildcard.source"]) {
|
|
324
|
+
reExports.push({ name: "*", from: caps["reexport.wildcard.source"], isWildcard: true });
|
|
208
325
|
}
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
function looksLikeUrl(s) {
|
|
212
|
-
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
213
326
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
327
|
+
const jsxElements = /* @__PURE__ */ new Set();
|
|
328
|
+
const jsxQuery = getQuery("jsx-elements");
|
|
329
|
+
const jsxCaptures = jsxQuery.captures(root);
|
|
330
|
+
for (const c of jsxCaptures) {
|
|
331
|
+
if (c.name === "jsx.tag") {
|
|
332
|
+
if (/^[A-Z]/.test(c.node.text)) {
|
|
333
|
+
jsxElements.add(c.node.text);
|
|
334
|
+
}
|
|
335
|
+
} else if (c.name === "jsx.member_tag") {
|
|
336
|
+
const rootName = c.node.text.split(".")[0];
|
|
337
|
+
if (rootName && /^[A-Z]/.test(rootName)) {
|
|
338
|
+
jsxElements.add(rootName);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
217
341
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
342
|
+
const navigations = [];
|
|
343
|
+
const navQuery = getQuery("navigations");
|
|
344
|
+
const navMatches = navQuery.matches(root);
|
|
345
|
+
for (const m of navMatches) {
|
|
346
|
+
const caps = captureMap(m);
|
|
347
|
+
if (caps["nav.target.literal"] && caps["nav.method"]) {
|
|
348
|
+
navigations.push({
|
|
349
|
+
kind: caps["nav.method"] === "push" ? "router-push" : "router-replace",
|
|
350
|
+
target: caps["nav.target.literal"],
|
|
351
|
+
isTemplate: false
|
|
352
|
+
});
|
|
222
353
|
}
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
354
|
+
if (caps["nav.target.template"] && caps["nav.method.template"]) {
|
|
355
|
+
navigations.push({
|
|
356
|
+
kind: caps["nav.method.template"] === "push" ? "router-push" : "router-replace",
|
|
357
|
+
target: caps["nav.target.template"],
|
|
358
|
+
isTemplate: true
|
|
359
|
+
});
|
|
226
360
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
361
|
+
const linkLiteral = caps["nav.link.literal"] || caps["nav.link.literal.self"];
|
|
362
|
+
if (linkLiteral) {
|
|
363
|
+
navigations.push({ kind: "link-href", target: linkLiteral, isTemplate: false });
|
|
364
|
+
}
|
|
365
|
+
const linkTemplate = caps["nav.link.template"] || caps["nav.link.template.self"];
|
|
366
|
+
if (linkTemplate) {
|
|
367
|
+
navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
|
|
368
|
+
}
|
|
369
|
+
if (caps["nav.window.literal"]) {
|
|
370
|
+
navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
|
|
371
|
+
}
|
|
372
|
+
if (caps["nav.window.assign.target"]) {
|
|
373
|
+
navigations.push({ kind: "window-location", target: caps["nav.window.assign.target"], isTemplate: false });
|
|
235
374
|
}
|
|
236
|
-
return null;
|
|
237
375
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
376
|
+
const fetchCalls = [];
|
|
377
|
+
const fetchQuery = getQuery("fetch-calls");
|
|
378
|
+
const fetchMatches = fetchQuery.matches(root);
|
|
379
|
+
for (const m of fetchMatches) {
|
|
380
|
+
const caps = captureMap(m);
|
|
381
|
+
if (caps["fetch.url.literal"] && looksLikeUrl(caps["fetch.url.literal"])) {
|
|
382
|
+
fetchCalls.push({ url: caps["fetch.url.literal"], isTemplate: false, kind: "fetch" });
|
|
383
|
+
}
|
|
384
|
+
if (caps["fetch.url.template"] && templateStartsWithSlash(caps["fetch.url.template"])) {
|
|
385
|
+
fetchCalls.push({ url: caps["fetch.url.template"], isTemplate: true, kind: "fetch" });
|
|
386
|
+
}
|
|
387
|
+
const clientUrl = caps["fetch.client.url.literal"] || caps["fetch.await.url.literal"];
|
|
388
|
+
const clientMethod = caps["fetch.method"] || caps["fetch.await.method"];
|
|
389
|
+
if (clientUrl && clientMethod && looksLikeUrl(clientUrl)) {
|
|
390
|
+
fetchCalls.push({
|
|
391
|
+
method: clientMethod.toUpperCase(),
|
|
392
|
+
url: clientUrl,
|
|
393
|
+
isTemplate: false,
|
|
394
|
+
kind: "client-method"
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const clientUrlTpl = caps["fetch.client.url.template"] || caps["fetch.await.url.template"];
|
|
398
|
+
const clientMethodTpl = caps["fetch.method.template"] || caps["fetch.await.method.template"];
|
|
399
|
+
if (clientUrlTpl && clientMethodTpl && templateStartsWithSlash(clientUrlTpl)) {
|
|
400
|
+
fetchCalls.push({
|
|
401
|
+
method: clientMethodTpl.toUpperCase(),
|
|
402
|
+
url: clientUrlTpl,
|
|
403
|
+
isTemplate: true,
|
|
404
|
+
kind: "client-method"
|
|
405
|
+
});
|
|
265
406
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
407
|
+
}
|
|
408
|
+
const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
|
|
409
|
+
return { name, exports: exportsOrdered, imports, reExports, jsxElements, navigations, fetchCalls };
|
|
410
|
+
}
|
|
411
|
+
function extractDbCallsTS(absPath) {
|
|
412
|
+
const tree = parseSource(absPath);
|
|
413
|
+
const root = tree.rootNode;
|
|
414
|
+
const dbQuery = getQuery("db-calls");
|
|
415
|
+
const matches = dbQuery.matches(root);
|
|
416
|
+
const dbIdentifiers = /* @__PURE__ */ new Set();
|
|
417
|
+
for (const stmt of childrenOfType(root, "import_statement")) {
|
|
418
|
+
const sourceNode = childOfType(stmt, "string");
|
|
419
|
+
const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
|
|
420
|
+
if (!frag) continue;
|
|
421
|
+
const spec = frag.text;
|
|
422
|
+
if (spec.includes("prisma") || spec.includes("/db") || spec === "@prisma/client") {
|
|
423
|
+
const clause = childOfType(stmt, "import_clause");
|
|
424
|
+
if (clause) {
|
|
425
|
+
const defaultId = childOfType(clause, "identifier");
|
|
426
|
+
if (defaultId) dbIdentifiers.add(defaultId.text);
|
|
427
|
+
const named = childOfType(clause, "named_imports");
|
|
428
|
+
if (named) {
|
|
429
|
+
for (const specNode of childrenOfType(named, "import_specifier")) {
|
|
430
|
+
const ids = childrenOfType(specNode, "identifier");
|
|
431
|
+
const importedName = ids[ids.length - 1];
|
|
432
|
+
if (importedName) dbIdentifiers.add(importedName.text);
|
|
274
433
|
}
|
|
275
434
|
}
|
|
276
|
-
} else if (!node.exportClause && fromSpec) {
|
|
277
|
-
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
278
435
|
}
|
|
279
436
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
437
|
+
}
|
|
438
|
+
if (dbIdentifiers.size === 0) {
|
|
439
|
+
for (const id of getFallbackDbIdentifiers()) dbIdentifiers.add(id);
|
|
440
|
+
} else {
|
|
441
|
+
for (const id of extraDbIdentifiers) dbIdentifiers.add(id);
|
|
442
|
+
}
|
|
443
|
+
const calls = [];
|
|
444
|
+
const seen = /* @__PURE__ */ new Set();
|
|
445
|
+
for (const m of matches) {
|
|
446
|
+
const caps = captureMap(m);
|
|
447
|
+
const identifier = caps["db.identifier"];
|
|
448
|
+
const model = caps["db.model"];
|
|
449
|
+
const method = caps["db.method"];
|
|
450
|
+
if (!identifier || !model || !method) continue;
|
|
451
|
+
if (!dbIdentifiers.has(identifier)) continue;
|
|
452
|
+
const key = `${model}.${method}`;
|
|
453
|
+
if (seen.has(key)) continue;
|
|
454
|
+
seen.add(key);
|
|
455
|
+
calls.push({ model, method, isMutation: getMutationMethods().has(method) });
|
|
456
|
+
}
|
|
457
|
+
return calls;
|
|
458
|
+
}
|
|
459
|
+
function extractAuthWrappersTS(absPath) {
|
|
460
|
+
const tree = parseSource(absPath);
|
|
461
|
+
const root = tree.rootNode;
|
|
462
|
+
const wrapperQuery = getQuery("wrappers");
|
|
463
|
+
const matches = wrapperQuery.matches(root);
|
|
464
|
+
const wrappers = /* @__PURE__ */ new Set();
|
|
465
|
+
for (const m of matches) {
|
|
466
|
+
const caps = captureMap(m);
|
|
467
|
+
if (caps["wrapper.fn_name"]) {
|
|
468
|
+
wrappers.add(caps["wrapper.fn_name"]);
|
|
290
469
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
470
|
+
}
|
|
471
|
+
return wrappers;
|
|
472
|
+
}
|
|
473
|
+
function trunc(s, max = 120) {
|
|
474
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
475
|
+
}
|
|
476
|
+
function extractDeep(absPath) {
|
|
477
|
+
const tree = parseSource(absPath);
|
|
478
|
+
const root = tree.rootNode;
|
|
479
|
+
const elements = [];
|
|
480
|
+
const elQuery = getQuery("deep/jsx-semantic");
|
|
481
|
+
const elMatches = elQuery.matches(root);
|
|
482
|
+
const elementMap = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const m of elMatches) {
|
|
484
|
+
const tag = m.captures.find((c) => c.name === "el.tag")?.node;
|
|
485
|
+
if (!tag || !/^[A-Z]/.test(tag.text)) continue;
|
|
486
|
+
const elNode = m.captures.find((c) => c.name === "el.self" || c.name === "el.open")?.node;
|
|
487
|
+
const key = elNode ? `${elNode.startPosition.row}:${elNode.startPosition.column}` : `${tag.startPosition.row}:${tag.startPosition.column}`;
|
|
488
|
+
if (!elementMap.has(key)) {
|
|
489
|
+
elementMap.set(key, { tag: tag.text, props: {}, nodeKey: key });
|
|
297
490
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
491
|
+
const entry = elementMap.get(key);
|
|
492
|
+
const propName = m.captures.find((c) => c.name === "el.prop.name")?.node.text;
|
|
493
|
+
const propVal = m.captures.find((c) => c.name === "el.prop.value.str")?.node.text;
|
|
494
|
+
const propExpr = m.captures.find((c) => c.name === "el.prop.value.expr")?.node.text;
|
|
495
|
+
if (propName) {
|
|
496
|
+
entry.props[propName] = propVal ?? (propExpr ? trunc(propExpr, 60) : "true");
|
|
301
497
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
498
|
+
}
|
|
499
|
+
for (const m of elMatches) {
|
|
500
|
+
const textTag = m.captures.find((c) => c.name === "el.text.tag")?.node;
|
|
501
|
+
const textContent = m.captures.find((c) => c.name === "el.text.content")?.node;
|
|
502
|
+
if (textTag && textContent && /^[A-Z]/.test(textTag.text)) {
|
|
503
|
+
const key = `${textTag.startPosition.row}:${textTag.startPosition.column}`;
|
|
504
|
+
if (!elementMap.has(key)) {
|
|
505
|
+
elementMap.set(key, { tag: textTag.text, props: {}, nodeKey: key });
|
|
307
506
|
}
|
|
507
|
+
const entry = elementMap.get(key);
|
|
508
|
+
const text = textContent.text.trim();
|
|
509
|
+
if (text) entry.props["_text"] = text;
|
|
308
510
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
511
|
+
}
|
|
512
|
+
for (const entry of elementMap.values()) {
|
|
513
|
+
const el = { tag: entry.tag, props: entry.props };
|
|
514
|
+
const hasExpr = Object.values(entry.props).some((v) => v.includes("{") || v.includes("("));
|
|
515
|
+
if (hasExpr) el.dynamic = true;
|
|
516
|
+
if (entry.props["_text"]) {
|
|
517
|
+
el.text = entry.props["_text"];
|
|
518
|
+
delete el.props["_text"];
|
|
315
519
|
}
|
|
316
|
-
|
|
317
|
-
|
|
520
|
+
elements.push(el);
|
|
521
|
+
}
|
|
522
|
+
const stateVars = [];
|
|
523
|
+
const hookQuery = getQuery("deep/state-hooks");
|
|
524
|
+
const hookMatches = hookQuery.matches(root);
|
|
525
|
+
for (const m of hookMatches) {
|
|
526
|
+
const caps = captureMap(m);
|
|
527
|
+
if (caps["hook.var"] && caps["hook.setter"] && caps["hook.fn"]) {
|
|
528
|
+
stateVars.push({
|
|
529
|
+
name: caps["hook.var"],
|
|
530
|
+
setter: caps["hook.setter"],
|
|
531
|
+
hook: caps["hook.fn"],
|
|
532
|
+
init: trunc(caps["hook.init"] ?? "", 80)
|
|
533
|
+
});
|
|
318
534
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
535
|
+
if (caps["reducer.var"] && caps["reducer.dispatch"]) {
|
|
536
|
+
stateVars.push({
|
|
537
|
+
name: caps["reducer.var"],
|
|
538
|
+
setter: caps["reducer.dispatch"],
|
|
539
|
+
hook: "useReducer",
|
|
540
|
+
init: trunc(caps["reducer.init"] ?? "", 80)
|
|
541
|
+
});
|
|
321
542
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
543
|
+
}
|
|
544
|
+
const conditions = [];
|
|
545
|
+
const condQuery = getQuery("deep/conditions");
|
|
546
|
+
const condMatches = condQuery.matches(root);
|
|
547
|
+
for (const m of condMatches) {
|
|
548
|
+
const caps = captureMap(m);
|
|
549
|
+
if (caps["cond.test"]) {
|
|
550
|
+
conditions.push({
|
|
551
|
+
kind: "if",
|
|
552
|
+
test: trunc(caps["cond.test"], 100),
|
|
553
|
+
consequence: caps["cond.consequence"] ? trunc(caps["cond.consequence"], 80) : void 0
|
|
554
|
+
});
|
|
333
555
|
}
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const parsed = extractTargetFromExpression(arg);
|
|
340
|
-
if (parsed) {
|
|
341
|
-
navigations.push({
|
|
342
|
-
kind: expr.name.text === "push" ? "router-push" : "router-replace",
|
|
343
|
-
target: parsed.target,
|
|
344
|
-
isTemplate: parsed.isTemplate
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
556
|
+
if (caps["ternary.test"]) {
|
|
557
|
+
conditions.push({
|
|
558
|
+
kind: "ternary",
|
|
559
|
+
test: trunc(caps["ternary.test"], 100)
|
|
560
|
+
});
|
|
349
561
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
365
|
-
const methodName = expr.name.text;
|
|
366
|
-
if (HTTP_METHODS.has(methodName)) {
|
|
367
|
-
const extracted = extractUrlFromFetchArg(firstArg);
|
|
368
|
-
if (extracted) {
|
|
369
|
-
fetchCalls.push({
|
|
370
|
-
method: methodName.toUpperCase(),
|
|
371
|
-
url: extracted.url,
|
|
372
|
-
isTemplate: extracted.isTemplate,
|
|
373
|
-
...extracted.isConcat ? { isConcat: true } : {},
|
|
374
|
-
kind: "client-method"
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
562
|
+
}
|
|
563
|
+
const variables = [];
|
|
564
|
+
const varQuery = getQuery("deep/variables");
|
|
565
|
+
const varMatches = varQuery.matches(root);
|
|
566
|
+
for (const m of varMatches) {
|
|
567
|
+
const caps = captureMap(m);
|
|
568
|
+
const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
|
|
569
|
+
const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
|
|
570
|
+
if (caps["var.name"] && caps["var.init"]) {
|
|
571
|
+
variables.push({
|
|
572
|
+
name: caps["var.name"],
|
|
573
|
+
kind,
|
|
574
|
+
init: trunc(caps["var.init"], 100)
|
|
575
|
+
});
|
|
379
576
|
}
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (ts.isStringLiteral(init)) {
|
|
387
|
-
navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
|
|
388
|
-
} else if (ts.isJsxExpression(init) && init.expression) {
|
|
389
|
-
const parsed = extractTargetFromExpression(init.expression);
|
|
390
|
-
if (parsed) {
|
|
391
|
-
navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
577
|
+
if (caps["var.destructured.obj"]) {
|
|
578
|
+
variables.push({
|
|
579
|
+
name: trunc(caps["var.destructured.obj"], 60),
|
|
580
|
+
kind,
|
|
581
|
+
init: trunc(caps["var.destructured.init"], 100)
|
|
582
|
+
});
|
|
397
583
|
}
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
}
|
|
584
|
+
if (caps["var.array.pattern"]) {
|
|
585
|
+
variables.push({
|
|
586
|
+
name: trunc(caps["var.array.pattern"], 60),
|
|
587
|
+
kind,
|
|
588
|
+
init: trunc(caps["var.array.init"], 100)
|
|
589
|
+
});
|
|
406
590
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
591
|
+
}
|
|
592
|
+
const responses = [];
|
|
593
|
+
const respQuery = getQuery("deep/responses");
|
|
594
|
+
const respMatches = respQuery.matches(root);
|
|
595
|
+
const explicitBodies = /* @__PURE__ */ new Set();
|
|
596
|
+
for (const m of respMatches) {
|
|
597
|
+
const caps = captureMap(m);
|
|
598
|
+
if (caps["resp.status"] && caps["resp.body"]) {
|
|
599
|
+
explicitBodies.add(trunc(caps["resp.body"], 80));
|
|
600
|
+
responses.push({
|
|
601
|
+
status: caps["resp.status"],
|
|
602
|
+
body: trunc(caps["resp.body"], 80)
|
|
603
|
+
});
|
|
418
604
|
}
|
|
419
|
-
ts.forEachChild(node, visit);
|
|
420
605
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
reExports,
|
|
428
|
-
jsxElements,
|
|
429
|
-
navigations,
|
|
430
|
-
fetchCalls
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
function extractDbCalls(absPath) {
|
|
434
|
-
const ts = getTs();
|
|
435
|
-
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
436
|
-
const ext = (0, import_node_path3.extname)(absPath);
|
|
437
|
-
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
438
|
-
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
439
|
-
const calls = [];
|
|
440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
441
|
-
function visit(node) {
|
|
442
|
-
if (ts.isCallExpression(node)) {
|
|
443
|
-
const expr = node.expression;
|
|
444
|
-
if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
|
|
445
|
-
const model = expr.expression.name.text;
|
|
446
|
-
const method = expr.name.text;
|
|
447
|
-
const key = `${model}.${method}`;
|
|
448
|
-
if (!seen.has(key)) {
|
|
449
|
-
seen.add(key);
|
|
450
|
-
calls.push({
|
|
451
|
-
model,
|
|
452
|
-
method,
|
|
453
|
-
isMutation: MUTATION_METHODS.has(method)
|
|
454
|
-
});
|
|
455
|
-
}
|
|
606
|
+
for (const m of respMatches) {
|
|
607
|
+
const caps = captureMap(m);
|
|
608
|
+
if (caps["resp.body.default"] && !caps["resp.status"]) {
|
|
609
|
+
const bodyText = trunc(caps["resp.body.default"], 80);
|
|
610
|
+
if (!explicitBodies.has(bodyText)) {
|
|
611
|
+
responses.push({ status: "200", body: bodyText });
|
|
456
612
|
}
|
|
457
613
|
}
|
|
458
|
-
ts.forEachChild(node, visit);
|
|
459
614
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
|
|
471
|
-
function visit(node) {
|
|
472
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
473
|
-
if (AUTH_WRAPPERS.has(node.expression.text)) {
|
|
474
|
-
wrappers.add(node.expression.text);
|
|
475
|
-
}
|
|
615
|
+
const params = [];
|
|
616
|
+
const paramQuery = getQuery("deep/request-params");
|
|
617
|
+
const paramMatches = paramQuery.matches(root);
|
|
618
|
+
for (const m of paramMatches) {
|
|
619
|
+
const caps = captureMap(m);
|
|
620
|
+
if (caps["param.name"]) {
|
|
621
|
+
params.push({ name: caps["param.name"], source: "body-field" });
|
|
622
|
+
}
|
|
623
|
+
if (caps["param.body"]) {
|
|
624
|
+
params.push({ name: caps["param.body"], source: "body" });
|
|
476
625
|
}
|
|
477
|
-
ts.forEachChild(node, visit);
|
|
478
626
|
}
|
|
479
|
-
|
|
480
|
-
return wrappers;
|
|
627
|
+
return { elements, stateVars, conditions, variables, responses, params };
|
|
481
628
|
}
|
|
482
|
-
var import_node_fs3, import_node_path3,
|
|
483
|
-
var
|
|
484
|
-
"src/server/graph/core/
|
|
629
|
+
var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
630
|
+
var init_ts_extractor = __esm({
|
|
631
|
+
"src/server/graph/core/ts-extractor.ts"() {
|
|
485
632
|
"use strict";
|
|
486
633
|
import_node_fs3 = require("node:fs");
|
|
487
634
|
import_node_path3 = require("node:path");
|
|
488
|
-
|
|
489
|
-
|
|
635
|
+
initialized = false;
|
|
636
|
+
queriesDir = (() => {
|
|
637
|
+
const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
|
|
638
|
+
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
639
|
+
return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
|
|
640
|
+
})();
|
|
641
|
+
queryCache = /* @__PURE__ */ new Map();
|
|
642
|
+
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
490
643
|
"create",
|
|
491
644
|
"createMany",
|
|
492
645
|
"createManyAndReturn",
|
|
@@ -496,7 +649,10 @@ var init_ast_helpers = __esm({
|
|
|
496
649
|
"upsert",
|
|
497
650
|
"delete",
|
|
498
651
|
"deleteMany"
|
|
499
|
-
]
|
|
652
|
+
];
|
|
653
|
+
DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
|
|
654
|
+
extraDbIdentifiers = [];
|
|
655
|
+
extraMutationMethods = [];
|
|
500
656
|
}
|
|
501
657
|
});
|
|
502
658
|
|
|
@@ -822,7 +978,7 @@ function generate(rootDir) {
|
|
|
822
978
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
823
979
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
824
980
|
for (const absPath of allDiscovered) {
|
|
825
|
-
parsedByPath.set(absPath,
|
|
981
|
+
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
826
982
|
}
|
|
827
983
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
828
984
|
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
@@ -836,7 +992,18 @@ function generate(rootDir) {
|
|
|
836
992
|
const parsed = parsedByPath.get(absPath);
|
|
837
993
|
const name = parsed.name || nameFromFilename(absPath);
|
|
838
994
|
const route = extractRoute(id);
|
|
839
|
-
|
|
995
|
+
const deep = extractDeep(absPath);
|
|
996
|
+
nodes.push({
|
|
997
|
+
id,
|
|
998
|
+
type,
|
|
999
|
+
name,
|
|
1000
|
+
route,
|
|
1001
|
+
exports: parsed.exports,
|
|
1002
|
+
elements: deep.elements,
|
|
1003
|
+
stateVars: deep.stateVars,
|
|
1004
|
+
conditions: deep.conditions,
|
|
1005
|
+
variables: deep.variables
|
|
1006
|
+
});
|
|
840
1007
|
nodeIdSet.add(id);
|
|
841
1008
|
nodeTypeMap.set(id, type);
|
|
842
1009
|
if (route) routeToNodeId.set(route, id);
|
|
@@ -895,7 +1062,7 @@ function generate(rootDir) {
|
|
|
895
1062
|
if (externalScanned.has(normalized)) continue;
|
|
896
1063
|
let parsed;
|
|
897
1064
|
try {
|
|
898
|
-
parsed =
|
|
1065
|
+
parsed = parseFileTS(absPath);
|
|
899
1066
|
} catch {
|
|
900
1067
|
continue;
|
|
901
1068
|
}
|
|
@@ -1015,7 +1182,7 @@ var init_react_nextjs = __esm({
|
|
|
1015
1182
|
"use strict";
|
|
1016
1183
|
import_node_fs4 = require("node:fs");
|
|
1017
1184
|
import_node_path4 = require("node:path");
|
|
1018
|
-
|
|
1185
|
+
init_ts_extractor();
|
|
1019
1186
|
RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
1020
1187
|
reactNextjsParser = {
|
|
1021
1188
|
id: "react-nextjs",
|
|
@@ -1065,12 +1232,12 @@ function generate2(rootDir) {
|
|
|
1065
1232
|
let endpointsWithAuth = 0;
|
|
1066
1233
|
let endpointsWithDbAccess = 0;
|
|
1067
1234
|
for (const absPath of routeFiles) {
|
|
1068
|
-
const parsed =
|
|
1069
|
-
const dbCalls =
|
|
1070
|
-
const authWrappers =
|
|
1235
|
+
const parsed = parseFileTS(absPath);
|
|
1236
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1237
|
+
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1071
1238
|
const methods = [];
|
|
1072
1239
|
for (const exp of parsed.exports) {
|
|
1073
|
-
if (
|
|
1240
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1074
1241
|
}
|
|
1075
1242
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
1076
1243
|
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
@@ -1089,6 +1256,7 @@ function generate2(rootDir) {
|
|
|
1089
1256
|
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1090
1257
|
}
|
|
1091
1258
|
if (authStrategy.length > 0) endpointsWithAuth++;
|
|
1259
|
+
const deep = extractDeep(absPath);
|
|
1092
1260
|
nodes.push({
|
|
1093
1261
|
id: relPath,
|
|
1094
1262
|
type: "endpoint",
|
|
@@ -1100,7 +1268,12 @@ function generate2(rootDir) {
|
|
|
1100
1268
|
mutates,
|
|
1101
1269
|
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1102
1270
|
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1103
|
-
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
|
|
1271
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1272
|
+
// Deep extraction fields
|
|
1273
|
+
conditions: deep.conditions,
|
|
1274
|
+
variables: deep.variables,
|
|
1275
|
+
responses: deep.responses,
|
|
1276
|
+
params: deep.params
|
|
1104
1277
|
});
|
|
1105
1278
|
const seenModels = /* @__PURE__ */ new Set();
|
|
1106
1279
|
for (const call of dbCalls) {
|
|
@@ -1140,7 +1313,7 @@ function generate2(rootDir) {
|
|
|
1140
1313
|
flagged_edges: [],
|
|
1141
1314
|
patterns: {
|
|
1142
1315
|
total_endpoints: nodes.length,
|
|
1143
|
-
methods_breakdown: [...
|
|
1316
|
+
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1144
1317
|
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1145
1318
|
return acc;
|
|
1146
1319
|
}, {}),
|
|
@@ -1150,14 +1323,14 @@ function generate2(rootDir) {
|
|
|
1150
1323
|
}
|
|
1151
1324
|
};
|
|
1152
1325
|
}
|
|
1153
|
-
var import_node_fs5, import_node_path5,
|
|
1326
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS, nextjsRoutesParser;
|
|
1154
1327
|
var init_nextjs_routes = __esm({
|
|
1155
1328
|
"src/server/graph/parsers/api/nextjs-routes.ts"() {
|
|
1156
1329
|
"use strict";
|
|
1157
1330
|
import_node_fs5 = require("node:fs");
|
|
1158
1331
|
import_node_path5 = require("node:path");
|
|
1159
|
-
|
|
1160
|
-
|
|
1332
|
+
init_ts_extractor();
|
|
1333
|
+
HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1161
1334
|
nextjsRoutesParser = {
|
|
1162
1335
|
id: "nextjs-routes",
|
|
1163
1336
|
layer: "api",
|
|
@@ -2058,8 +2231,9 @@ function matchParts(pat, pi, id, ii) {
|
|
|
2058
2231
|
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
2059
2232
|
return pi === pat.length && ii === id.length;
|
|
2060
2233
|
}
|
|
2061
|
-
function detectConventionDirs(rootDir) {
|
|
2234
|
+
function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
2062
2235
|
const result = /* @__PURE__ */ new Map();
|
|
2236
|
+
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
2063
2237
|
const searchDirs = [
|
|
2064
2238
|
rootDir,
|
|
2065
2239
|
(0, import_node_path11.join)(rootDir, "src"),
|
|
@@ -2067,7 +2241,7 @@ function detectConventionDirs(rootDir) {
|
|
|
2067
2241
|
(0, import_node_path11.join)(rootDir, "lib")
|
|
2068
2242
|
];
|
|
2069
2243
|
for (const base of searchDirs) {
|
|
2070
|
-
for (const convention of
|
|
2244
|
+
for (const convention of conventionDirs) {
|
|
2071
2245
|
const dir = (0, import_node_path11.join)(base, convention);
|
|
2072
2246
|
if (!(0, import_node_fs10.existsSync)(dir)) continue;
|
|
2073
2247
|
try {
|
|
@@ -2120,9 +2294,13 @@ function isTrivialGroup(name, extraTrivial) {
|
|
|
2120
2294
|
function normalizeGroupName(name) {
|
|
2121
2295
|
return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
2122
2296
|
}
|
|
2123
|
-
function extractModuleFromPath(id, extraTrivial) {
|
|
2297
|
+
function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
2124
2298
|
const segments = id.split("/");
|
|
2125
2299
|
const routeGroups = extractRouteGroups(id);
|
|
2300
|
+
const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
|
|
2301
|
+
if (extraSkipSegments) {
|
|
2302
|
+
for (const s of extraSkipSegments) skipSegments.add(s);
|
|
2303
|
+
}
|
|
2126
2304
|
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
|
|
2127
2305
|
if (moduleGroups.length > 0) {
|
|
2128
2306
|
return moduleGroups[moduleGroups.length - 1];
|
|
@@ -2133,7 +2311,7 @@ function extractModuleFromPath(id, extraTrivial) {
|
|
|
2133
2311
|
if (isRouteGroup(seg)) continue;
|
|
2134
2312
|
if (isDynamicSegment(seg)) continue;
|
|
2135
2313
|
if (isDomainDir(seg)) continue;
|
|
2136
|
-
if (
|
|
2314
|
+
if (skipSegments.has(seg)) continue;
|
|
2137
2315
|
meaningful.push(seg);
|
|
2138
2316
|
}
|
|
2139
2317
|
if (meaningful.length > 0) {
|
|
@@ -2141,14 +2319,83 @@ function extractModuleFromPath(id, extraTrivial) {
|
|
|
2141
2319
|
}
|
|
2142
2320
|
return "root";
|
|
2143
2321
|
}
|
|
2144
|
-
var import_node_fs10, import_node_path11,
|
|
2322
|
+
var import_node_fs10, import_node_path11, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2145
2323
|
var init_module_tagger = __esm({
|
|
2146
2324
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2147
2325
|
"use strict";
|
|
2148
2326
|
import_node_fs10 = require("node:fs");
|
|
2149
2327
|
import_node_path11 = require("node:path");
|
|
2150
|
-
|
|
2151
|
-
|
|
2328
|
+
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
2329
|
+
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
2330
|
+
// JS/TS
|
|
2331
|
+
"components",
|
|
2332
|
+
"hooks",
|
|
2333
|
+
"pages",
|
|
2334
|
+
"views",
|
|
2335
|
+
"screens",
|
|
2336
|
+
"layouts",
|
|
2337
|
+
"utils",
|
|
2338
|
+
"helpers",
|
|
2339
|
+
"lib",
|
|
2340
|
+
"libs",
|
|
2341
|
+
"services",
|
|
2342
|
+
"api",
|
|
2343
|
+
"apis",
|
|
2344
|
+
"stores",
|
|
2345
|
+
"state",
|
|
2346
|
+
"store",
|
|
2347
|
+
"context",
|
|
2348
|
+
"contexts",
|
|
2349
|
+
"providers",
|
|
2350
|
+
"types",
|
|
2351
|
+
"interfaces",
|
|
2352
|
+
"models",
|
|
2353
|
+
"schemas",
|
|
2354
|
+
"constants",
|
|
2355
|
+
"config",
|
|
2356
|
+
"configs",
|
|
2357
|
+
"assets",
|
|
2358
|
+
"styles",
|
|
2359
|
+
"public",
|
|
2360
|
+
"middleware",
|
|
2361
|
+
"middlewares",
|
|
2362
|
+
"routes",
|
|
2363
|
+
"router",
|
|
2364
|
+
"tests",
|
|
2365
|
+
"test",
|
|
2366
|
+
"__tests__",
|
|
2367
|
+
"spec",
|
|
2368
|
+
"specs",
|
|
2369
|
+
// Go
|
|
2370
|
+
"cmd",
|
|
2371
|
+
"pkg",
|
|
2372
|
+
"internal",
|
|
2373
|
+
// Python
|
|
2374
|
+
"management",
|
|
2375
|
+
"migrations",
|
|
2376
|
+
"templatetags",
|
|
2377
|
+
"templates",
|
|
2378
|
+
// Java
|
|
2379
|
+
"controller",
|
|
2380
|
+
"controllers",
|
|
2381
|
+
"service",
|
|
2382
|
+
"repository",
|
|
2383
|
+
"repositories",
|
|
2384
|
+
"entity",
|
|
2385
|
+
"entities",
|
|
2386
|
+
"dto",
|
|
2387
|
+
"dtos",
|
|
2388
|
+
// General
|
|
2389
|
+
"shared",
|
|
2390
|
+
"common",
|
|
2391
|
+
"core",
|
|
2392
|
+
"base",
|
|
2393
|
+
"app",
|
|
2394
|
+
// Next.js specific
|
|
2395
|
+
"client",
|
|
2396
|
+
"server"
|
|
2397
|
+
]);
|
|
2398
|
+
SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
|
|
2152
2399
|
"src",
|
|
2153
2400
|
"app",
|
|
2154
2401
|
"client",
|
|
@@ -2188,12 +2435,10 @@ var init_module_tagger = __esm({
|
|
|
2188
2435
|
layers: null,
|
|
2189
2436
|
// applies to all layers
|
|
2190
2437
|
tag(nodes, layer, rootDir) {
|
|
2191
|
-
if (cachedRootDir !== rootDir) {
|
|
2192
|
-
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
2193
|
-
cachedRootDir = rootDir;
|
|
2194
|
-
}
|
|
2195
2438
|
let configRules = [];
|
|
2196
2439
|
let extraTrivial;
|
|
2440
|
+
let extraSkipSegments;
|
|
2441
|
+
let extraConventionDirs = [];
|
|
2197
2442
|
try {
|
|
2198
2443
|
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
2199
2444
|
const config = loadConfig2(rootDir);
|
|
@@ -2202,8 +2447,21 @@ var init_module_tagger = __esm({
|
|
|
2202
2447
|
if (trivialFromConfig?.length) {
|
|
2203
2448
|
extraTrivial = new Set(trivialFromConfig);
|
|
2204
2449
|
}
|
|
2450
|
+
const skipFromConfig = config.taggers?.module?.skipSegments;
|
|
2451
|
+
if (skipFromConfig?.length) {
|
|
2452
|
+
extraSkipSegments = new Set(skipFromConfig);
|
|
2453
|
+
}
|
|
2454
|
+
extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
|
|
2455
|
+
const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
|
|
2456
|
+
if (roleNamesFromConfig?.length) {
|
|
2457
|
+
for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
|
|
2458
|
+
}
|
|
2205
2459
|
} catch {
|
|
2206
2460
|
}
|
|
2461
|
+
if (cachedRootDir !== rootDir) {
|
|
2462
|
+
cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
|
|
2463
|
+
cachedRootDir = rootDir;
|
|
2464
|
+
}
|
|
2207
2465
|
const result = /* @__PURE__ */ new Map();
|
|
2208
2466
|
for (const node of nodes) {
|
|
2209
2467
|
const id = node.id;
|
|
@@ -2229,7 +2487,7 @@ var init_module_tagger = __esm({
|
|
|
2229
2487
|
}
|
|
2230
2488
|
}
|
|
2231
2489
|
if (matched) continue;
|
|
2232
|
-
const module2 = extractModuleFromPath(id, extraTrivial);
|
|
2490
|
+
const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
|
|
2233
2491
|
result.set(id, module2);
|
|
2234
2492
|
}
|
|
2235
2493
|
return result;
|
|
@@ -2476,7 +2734,13 @@ function readAllGraphs(rootDir) {
|
|
|
2476
2734
|
}
|
|
2477
2735
|
return result;
|
|
2478
2736
|
}
|
|
2479
|
-
function generateGraph(rootDir, layer) {
|
|
2737
|
+
async function generateGraph(rootDir, layer) {
|
|
2738
|
+
await initTreeSitter();
|
|
2739
|
+
const config = loadConfig(rootDir);
|
|
2740
|
+
setExtractorConfig({
|
|
2741
|
+
dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
|
|
2742
|
+
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
2743
|
+
});
|
|
2480
2744
|
const dir = graphsDir(rootDir);
|
|
2481
2745
|
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2482
2746
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
@@ -2498,6 +2762,7 @@ var init_graph = __esm({
|
|
|
2498
2762
|
init_config();
|
|
2499
2763
|
init_tagger_registry();
|
|
2500
2764
|
init_tag_store();
|
|
2765
|
+
init_ts_extractor();
|
|
2501
2766
|
init_tag_store();
|
|
2502
2767
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2503
2768
|
LAYERS = ["ui", "api", "db"];
|
|
@@ -2533,10 +2798,10 @@ function findProjectRoot(startDir) {
|
|
|
2533
2798
|
}
|
|
2534
2799
|
return startDir;
|
|
2535
2800
|
}
|
|
2536
|
-
function buildMergedGraph(projectRoot) {
|
|
2801
|
+
async function buildMergedGraph(projectRoot) {
|
|
2537
2802
|
let graphs = readAllGraphs(projectRoot);
|
|
2538
2803
|
if (!graphs.ui && !graphs.api && !graphs.db) {
|
|
2539
|
-
generateGraph(projectRoot);
|
|
2804
|
+
await generateGraph(projectRoot);
|
|
2540
2805
|
graphs = readAllGraphs(projectRoot);
|
|
2541
2806
|
}
|
|
2542
2807
|
const nodes = [];
|
|
@@ -2646,13 +2911,18 @@ async function startChartServer(opts = {}) {
|
|
|
2646
2911
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
2647
2912
|
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2648
2913
|
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2914
|
+
(async () => {
|
|
2915
|
+
if (regenerate) await generateGraph(projectRoot);
|
|
2916
|
+
const merged = await buildMergedGraph(projectRoot);
|
|
2917
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2918
|
+
res.end(JSON.stringify({
|
|
2919
|
+
...merged,
|
|
2920
|
+
debug: { cwd, projectRoot }
|
|
2921
|
+
}));
|
|
2922
|
+
})().catch((e) => {
|
|
2923
|
+
res.writeHead(500);
|
|
2924
|
+
res.end(String(e));
|
|
2925
|
+
});
|
|
2656
2926
|
return;
|
|
2657
2927
|
}
|
|
2658
2928
|
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
@@ -2662,8 +2932,8 @@ async function startChartServer(opts = {}) {
|
|
|
2662
2932
|
return;
|
|
2663
2933
|
}
|
|
2664
2934
|
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2665
|
-
|
|
2666
|
-
generateGraph(projectRoot);
|
|
2935
|
+
(async () => {
|
|
2936
|
+
await generateGraph(projectRoot);
|
|
2667
2937
|
const graphs = readAllGraphs(projectRoot);
|
|
2668
2938
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2669
2939
|
res.end(JSON.stringify({
|
|
@@ -2672,10 +2942,10 @@ async function startChartServer(opts = {}) {
|
|
|
2672
2942
|
api: graphs.api ?? null,
|
|
2673
2943
|
db: graphs.db ?? null
|
|
2674
2944
|
}));
|
|
2675
|
-
}
|
|
2945
|
+
})().catch((err2) => {
|
|
2676
2946
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2677
2947
|
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2678
|
-
}
|
|
2948
|
+
});
|
|
2679
2949
|
return;
|
|
2680
2950
|
}
|
|
2681
2951
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
@@ -2941,7 +3211,7 @@ function toCompactNode(n) {
|
|
|
2941
3211
|
if (n.columns != null) out.c = n.columns;
|
|
2942
3212
|
if (tags != null) out.tg = tags;
|
|
2943
3213
|
for (const k of Object.keys(n)) {
|
|
2944
|
-
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
3214
|
+
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && !DEEP_FIELDS.has(k) && n[k] != null) out[k] = n[k];
|
|
2945
3215
|
}
|
|
2946
3216
|
return out;
|
|
2947
3217
|
}
|
|
@@ -3041,13 +3311,13 @@ function okJson(data) {
|
|
|
3041
3311
|
function err(text) {
|
|
3042
3312
|
return { content: [{ type: "text", text }], isError: true };
|
|
3043
3313
|
}
|
|
3044
|
-
function handleGenerateGraph(args) {
|
|
3314
|
+
async function handleGenerateGraph(args) {
|
|
3045
3315
|
const rootDir = process.cwd();
|
|
3046
3316
|
const layer = args.layer;
|
|
3047
3317
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3048
3318
|
return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
|
|
3049
3319
|
}
|
|
3050
|
-
const results = generateGraph(rootDir, layer);
|
|
3320
|
+
const results = await generateGraph(rootDir, layer);
|
|
3051
3321
|
if (results.length === 0) {
|
|
3052
3322
|
return err(
|
|
3053
3323
|
layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
|
|
@@ -3080,6 +3350,8 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3080
3350
|
const layerIsDb = args.layer === "db";
|
|
3081
3351
|
const minimal = args.minimal ?? layerIsDb;
|
|
3082
3352
|
const includeEdges = args.include_edges;
|
|
3353
|
+
const offset = args.offset ?? 0;
|
|
3354
|
+
const limit = args.limit;
|
|
3083
3355
|
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
3084
3356
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3085
3357
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
@@ -3151,18 +3423,29 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3151
3423
|
hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
|
|
3152
3424
|
};
|
|
3153
3425
|
}
|
|
3426
|
+
const totalMatched = matched.length;
|
|
3427
|
+
const paginatedNodes = limit != null ? matched.slice(offset, offset + limit) : matched.slice(offset);
|
|
3428
|
+
const hasMore = offset + paginatedNodes.length < totalMatched;
|
|
3154
3429
|
const wantEdges = includeEdges ?? false;
|
|
3430
|
+
const returnedIds = new Set(paginatedNodes.map((n) => n.id));
|
|
3431
|
+
const returnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
|
|
3155
3432
|
const result = {
|
|
3156
3433
|
layer,
|
|
3157
3434
|
filter: { search, type, module: module_ },
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3435
|
+
total: totalMatched,
|
|
3436
|
+
returned: paginatedNodes.length,
|
|
3437
|
+
offset,
|
|
3438
|
+
has_more: hasMore,
|
|
3439
|
+
edge_count: returnedEdges.length,
|
|
3440
|
+
nodes: minimal ? toMinimal(paginatedNodes) : paginatedNodes
|
|
3161
3441
|
};
|
|
3442
|
+
if (hasMore) {
|
|
3443
|
+
result.next_offset = offset + paginatedNodes.length;
|
|
3444
|
+
}
|
|
3162
3445
|
if (wantEdges) {
|
|
3163
|
-
result.edges =
|
|
3164
|
-
} else {
|
|
3165
|
-
result.edges_hint = `${
|
|
3446
|
+
result.edges = returnedEdges;
|
|
3447
|
+
} else if (returnedEdges.length > 0) {
|
|
3448
|
+
result.edges_hint = `${returnedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
|
|
3166
3449
|
}
|
|
3167
3450
|
return result;
|
|
3168
3451
|
}
|
|
@@ -3230,6 +3513,48 @@ function nodeToFilePath(rootDir, layer, nodeId) {
|
|
|
3230
3513
|
if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
|
|
3231
3514
|
return null;
|
|
3232
3515
|
}
|
|
3516
|
+
function handleInspectNode(args) {
|
|
3517
|
+
const rootDir = process.cwd();
|
|
3518
|
+
const layer = args.layer;
|
|
3519
|
+
const nodeId = args.node_id;
|
|
3520
|
+
const search = args.search;
|
|
3521
|
+
const fields = args.fields;
|
|
3522
|
+
if (!layer) return err("layer is required.");
|
|
3523
|
+
if (!nodeId && !search) return err("Either node_id or search is required.");
|
|
3524
|
+
const graph = readGraph(rootDir, layer);
|
|
3525
|
+
if (!graph) return err(`No graph found for layer "${layer}". Run generate_graph first.`);
|
|
3526
|
+
let matched;
|
|
3527
|
+
if (nodeId) {
|
|
3528
|
+
const node = graph.nodes.find((n) => n.id === nodeId);
|
|
3529
|
+
if (!node) return err(`Node "${nodeId}" not found in ${layer} layer.`);
|
|
3530
|
+
matched = [node];
|
|
3531
|
+
} else {
|
|
3532
|
+
const searchLower = search.toLowerCase();
|
|
3533
|
+
matched = graph.nodes.filter(
|
|
3534
|
+
(n) => n.id.toLowerCase().includes(searchLower) || n.name.toLowerCase().includes(searchLower) || n.route?.toLowerCase().includes(searchLower)
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
if (matched.length === 0) return err(`No nodes matching "${search}" in ${layer} layer.`);
|
|
3538
|
+
if (matched.length > 5) {
|
|
3539
|
+
return err(`${matched.length} nodes match "${search}". Narrow your search (max 5 for inspect_node).`);
|
|
3540
|
+
}
|
|
3541
|
+
const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
|
|
3542
|
+
const requestedFields = fields ?? allDeepFields;
|
|
3543
|
+
const results = matched.map((node) => {
|
|
3544
|
+
const deep = { id: node.id, name: node.name, type: node.type };
|
|
3545
|
+
for (const field of requestedFields) {
|
|
3546
|
+
if (allDeepFields.includes(field) && node[field] != null) {
|
|
3547
|
+
deep[field] = node[field];
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
return deep;
|
|
3551
|
+
});
|
|
3552
|
+
return okJson({
|
|
3553
|
+
layer,
|
|
3554
|
+
matched: results.length,
|
|
3555
|
+
nodes: results
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3233
3558
|
function handleGrepNodes(args) {
|
|
3234
3559
|
const rootDir = process.cwd();
|
|
3235
3560
|
const pattern = args.pattern;
|
|
@@ -3492,7 +3817,7 @@ function respond(id, result) {
|
|
|
3492
3817
|
function respondError(id, code, message) {
|
|
3493
3818
|
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
3494
3819
|
}
|
|
3495
|
-
function handleMessage(msg) {
|
|
3820
|
+
async function handleMessage(msg) {
|
|
3496
3821
|
const method = msg.method;
|
|
3497
3822
|
const id = msg.id;
|
|
3498
3823
|
if (method === "initialize") {
|
|
@@ -3515,7 +3840,7 @@ function handleMessage(msg) {
|
|
|
3515
3840
|
const toolName = params.name;
|
|
3516
3841
|
const args = params.arguments ?? {};
|
|
3517
3842
|
if (toolName === "generate_graph") {
|
|
3518
|
-
respond(id ?? null, handleGenerateGraph(args));
|
|
3843
|
+
respond(id ?? null, await handleGenerateGraph(args));
|
|
3519
3844
|
return;
|
|
3520
3845
|
}
|
|
3521
3846
|
if (toolName === "read_graph") {
|
|
@@ -3526,6 +3851,10 @@ function handleMessage(msg) {
|
|
|
3526
3851
|
respond(id ?? null, handleGrepNodes(args));
|
|
3527
3852
|
return;
|
|
3528
3853
|
}
|
|
3854
|
+
if (toolName === "inspect_node") {
|
|
3855
|
+
respond(id ?? null, handleInspectNode(args));
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3529
3858
|
if (toolName === "chart_server_status") {
|
|
3530
3859
|
respond(id ?? null, handleChartServerStatus());
|
|
3531
3860
|
return;
|
|
@@ -3585,7 +3914,7 @@ function startGraphMcpServer() {
|
|
|
3585
3914
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
3586
3915
|
`);
|
|
3587
3916
|
}
|
|
3588
|
-
var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3917
|
+
var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3589
3918
|
var init_graph_mcp = __esm({
|
|
3590
3919
|
"src/server/graph-mcp.ts"() {
|
|
3591
3920
|
"use strict";
|
|
@@ -3620,7 +3949,7 @@ var init_graph_mcp = __esm({
|
|
|
3620
3949
|
},
|
|
3621
3950
|
{
|
|
3622
3951
|
name: "read_graph",
|
|
3623
|
-
description: 'Query the structural project graph \u2014
|
|
3952
|
+
description: 'Query the structural project graph \u2014 use INSTEAD of Glob and Grep for locating files, understanding structure, and navigating the codebase. Faster and more accurate than file-system search because it returns typed nodes with metadata and relationships. \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module", "which endpoints touch the User table", "what auth strategy does this endpoint use". \n\nDO NOT USE FOR: understanding what\'s INSIDE a component (use inspect_node for elements, conditions, state, variables, responses), reading actual source code (use Read). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
3624
3953
|
inputSchema: {
|
|
3625
3954
|
type: "object",
|
|
3626
3955
|
properties: {
|
|
@@ -3665,6 +3994,14 @@ var init_graph_mcp = __esm({
|
|
|
3665
3994
|
type: "boolean",
|
|
3666
3995
|
description: "Include the edge list in the response. Default TRUE for neighborhood queries (node_id), FALSE for filter queries. Filter responses always include edge_count. Only set true on filter queries when you actually need edge data."
|
|
3667
3996
|
},
|
|
3997
|
+
offset: {
|
|
3998
|
+
type: "number",
|
|
3999
|
+
description: "Skip first N matched nodes (pagination). Default 0. Use next_offset from a previous response to get the next page."
|
|
4000
|
+
},
|
|
4001
|
+
limit: {
|
|
4002
|
+
type: "number",
|
|
4003
|
+
description: "Max nodes to return. Default: all matched nodes. Use with offset for pagination."
|
|
4004
|
+
},
|
|
3668
4005
|
queries: {
|
|
3669
4006
|
type: "array",
|
|
3670
4007
|
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
|
|
@@ -3687,7 +4024,7 @@ var init_graph_mcp = __esm({
|
|
|
3687
4024
|
},
|
|
3688
4025
|
{
|
|
3689
4026
|
name: "grep_nodes",
|
|
3690
|
-
description: `Search for text patterns WITHIN files selected by the project graph.
|
|
4027
|
+
description: `Search for text patterns WITHIN files selected by the project graph. Use INSTEAD of Grep when searching within code \u2014 combines structural filtering (type/module/neighborhood) with regex content search. Narrower than plain Grep because it only scans files matching the graph filter, reducing noise from tests, docs, generated code, unrelated modules.
|
|
3691
4028
|
|
|
3692
4029
|
USE THIS FOR: "which auth hooks use JWT decoding", "find TODO comments in pages only", "which deployment writers call Sentry", "what validation schemas exist in form components". It's grep scoped to a structurally-selected file set.
|
|
3693
4030
|
|
|
@@ -3733,6 +4070,40 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
3733
4070
|
required: ["layer", "pattern"]
|
|
3734
4071
|
}
|
|
3735
4072
|
},
|
|
4073
|
+
{
|
|
4074
|
+
name: "inspect_node",
|
|
4075
|
+
description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params. Use INSTEAD of Grep/Read when you need to understand component internals without reading source.
|
|
4076
|
+
|
|
4077
|
+
USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?"
|
|
4078
|
+
|
|
4079
|
+
DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
|
|
4080
|
+
|
|
4081
|
+
Returns deep fields only \u2014 not structural metadata (use read_graph for that).`,
|
|
4082
|
+
inputSchema: {
|
|
4083
|
+
type: "object",
|
|
4084
|
+
properties: {
|
|
4085
|
+
layer: {
|
|
4086
|
+
type: "string",
|
|
4087
|
+
enum: ["ui", "api", "db"],
|
|
4088
|
+
description: "Graph layer (required)."
|
|
4089
|
+
},
|
|
4090
|
+
node_id: {
|
|
4091
|
+
type: "string",
|
|
4092
|
+
description: "Node ID to inspect. Use read_graph to find node IDs first."
|
|
4093
|
+
},
|
|
4094
|
+
search: {
|
|
4095
|
+
type: "string",
|
|
4096
|
+
description: "Substring match on node id/name/route. Returns deep data for all matching nodes (max 5)."
|
|
4097
|
+
},
|
|
4098
|
+
fields: {
|
|
4099
|
+
type: "array",
|
|
4100
|
+
items: { type: "string" },
|
|
4101
|
+
description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
|
|
4102
|
+
}
|
|
4103
|
+
},
|
|
4104
|
+
required: ["layer"]
|
|
4105
|
+
}
|
|
4106
|
+
},
|
|
3736
4107
|
{
|
|
3737
4108
|
name: "chart_server_status",
|
|
3738
4109
|
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
@@ -3844,6 +4215,14 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
3844
4215
|
"columns",
|
|
3845
4216
|
"tags"
|
|
3846
4217
|
]);
|
|
4218
|
+
DEEP_FIELDS = /* @__PURE__ */ new Set([
|
|
4219
|
+
"elements",
|
|
4220
|
+
"stateVars",
|
|
4221
|
+
"conditions",
|
|
4222
|
+
"variables",
|
|
4223
|
+
"responses",
|
|
4224
|
+
"params"
|
|
4225
|
+
]);
|
|
3847
4226
|
EST_CHARS_PER_NODE_FULL = {
|
|
3848
4227
|
ui: 300,
|
|
3849
4228
|
api: 300,
|