@launchsecure/launch-kit 0.0.3 → 0.0.5
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-BUih0oqR.js +358 -0
- package/dist/chart-client/assets/index-DFslt72L.css +1 -0
- package/dist/chart-client/index.html +21 -0
- package/dist/client/assets/index-BCxRNp8I.css +32 -0
- package/dist/client/assets/{index-CcHIoRl6.js → index-DCC--GO-.js} +68 -63
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2285 -0
- package/dist/server/cli.js +1222 -120
- package/dist/server/graph-mcp-entry.js +1879 -300
- package/package.json +7 -3
- package/dist/client/assets/index-C8GAsRGO.css +0 -32
package/dist/server/cli.js
CHANGED
|
@@ -552,7 +552,7 @@ var init_esm_node = __esm({
|
|
|
552
552
|
var require_claude_bridge = __commonJS({
|
|
553
553
|
"../claude-code-web/src/claude-bridge.js"(exports2, module2) {
|
|
554
554
|
"use strict";
|
|
555
|
-
var { spawn:
|
|
555
|
+
var { spawn: spawn3 } = require("node-pty");
|
|
556
556
|
var path9 = require("path");
|
|
557
557
|
var fs9 = require("fs");
|
|
558
558
|
var ClaudeBridge = class {
|
|
@@ -624,7 +624,7 @@ var require_claude_bridge = __commonJS({
|
|
|
624
624
|
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
|
625
625
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
626
626
|
if (initialPrompt) args.push(initialPrompt);
|
|
627
|
-
const claudeProcess =
|
|
627
|
+
const claudeProcess = spawn3(this.claudeCommand, args, {
|
|
628
628
|
cwd: workingDir,
|
|
629
629
|
env: {
|
|
630
630
|
...process.env,
|
|
@@ -771,7 +771,7 @@ var require_claude_bridge = __commonJS({
|
|
|
771
771
|
var require_codex_bridge = __commonJS({
|
|
772
772
|
"../claude-code-web/src/codex-bridge.js"(exports2, module2) {
|
|
773
773
|
"use strict";
|
|
774
|
-
var { spawn:
|
|
774
|
+
var { spawn: spawn3 } = require("node-pty");
|
|
775
775
|
var path9 = require("path");
|
|
776
776
|
var fs9 = require("fs");
|
|
777
777
|
var CodexBridge = class {
|
|
@@ -834,7 +834,7 @@ var require_codex_bridge = __commonJS({
|
|
|
834
834
|
console.log(`\u26A0\uFE0F WARNING: Bypassing approvals and sandbox with --dangerously-bypass-approvals-and-sandbox flag`);
|
|
835
835
|
}
|
|
836
836
|
const args = dangerouslySkipPermissions ? ["--dangerously-bypass-approvals-and-sandbox"] : [];
|
|
837
|
-
const codexProcess =
|
|
837
|
+
const codexProcess = spawn3(this.codexCommand, args, {
|
|
838
838
|
cwd: workingDir,
|
|
839
839
|
env: {
|
|
840
840
|
...process.env,
|
|
@@ -964,7 +964,7 @@ var require_codex_bridge = __commonJS({
|
|
|
964
964
|
var require_agent_bridge = __commonJS({
|
|
965
965
|
"../claude-code-web/src/agent-bridge.js"(exports2, module2) {
|
|
966
966
|
"use strict";
|
|
967
|
-
var { spawn:
|
|
967
|
+
var { spawn: spawn3 } = require("node-pty");
|
|
968
968
|
var path9 = require("path");
|
|
969
969
|
var fs9 = require("fs");
|
|
970
970
|
var AgentBridge = class {
|
|
@@ -1021,7 +1021,7 @@ var require_agent_bridge = __commonJS({
|
|
|
1021
1021
|
console.log(`Command: ${this.agentCommand}`);
|
|
1022
1022
|
console.log(`Working directory: ${workingDir}`);
|
|
1023
1023
|
console.log(`Terminal size: ${cols}x${rows}`);
|
|
1024
|
-
const agentProcess =
|
|
1024
|
+
const agentProcess = spawn3(this.agentCommand, [], {
|
|
1025
1025
|
cwd: workingDir,
|
|
1026
1026
|
env: {
|
|
1027
1027
|
...process.env,
|
|
@@ -1151,7 +1151,7 @@ var require_agent_bridge = __commonJS({
|
|
|
1151
1151
|
var require_script_bridge = __commonJS({
|
|
1152
1152
|
"../claude-code-web/src/script-bridge.js"(exports2, module2) {
|
|
1153
1153
|
"use strict";
|
|
1154
|
-
var { spawn:
|
|
1154
|
+
var { spawn: spawn3 } = require("node-pty");
|
|
1155
1155
|
var ScriptBridge = class {
|
|
1156
1156
|
constructor() {
|
|
1157
1157
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -1180,7 +1180,7 @@ var require_script_bridge = __commonJS({
|
|
|
1180
1180
|
try {
|
|
1181
1181
|
console.log(`Starting script session ${sessionId}: ${command} ${args.join(" ")}`);
|
|
1182
1182
|
console.log(`Working directory: ${workingDir}`);
|
|
1183
|
-
const proc =
|
|
1183
|
+
const proc = spawn3(command, args, {
|
|
1184
1184
|
cwd: workingDir,
|
|
1185
1185
|
env: {
|
|
1186
1186
|
...process.env,
|
|
@@ -1727,7 +1727,7 @@ var require_usage_reader = __commonJS({
|
|
|
1727
1727
|
async readJsonlFile(filePath, cutoffTime) {
|
|
1728
1728
|
const entries = [];
|
|
1729
1729
|
const fileProcessedEntries = /* @__PURE__ */ new Set();
|
|
1730
|
-
return new Promise((
|
|
1730
|
+
return new Promise((resolve2) => {
|
|
1731
1731
|
const rl = readline.createInterface({
|
|
1732
1732
|
input: createReadStream(filePath),
|
|
1733
1733
|
crlfDelay: Infinity
|
|
@@ -1785,11 +1785,11 @@ var require_usage_reader = __commonJS({
|
|
|
1785
1785
|
}
|
|
1786
1786
|
});
|
|
1787
1787
|
rl.on("close", () => {
|
|
1788
|
-
|
|
1788
|
+
resolve2(entries);
|
|
1789
1789
|
});
|
|
1790
1790
|
rl.on("error", (error) => {
|
|
1791
1791
|
console.error("Error reading file:", filePath, error);
|
|
1792
|
-
|
|
1792
|
+
resolve2(entries);
|
|
1793
1793
|
});
|
|
1794
1794
|
});
|
|
1795
1795
|
}
|
|
@@ -3587,7 +3587,7 @@ var require_src = __commonJS({
|
|
|
3587
3587
|
if (session.active) throw new Error(`Agent already running in session ${sessionId}`);
|
|
3588
3588
|
const { command, args = [], env = {} } = options;
|
|
3589
3589
|
if (!command) throw new Error("startScriptInSession requires a command");
|
|
3590
|
-
return new Promise((
|
|
3590
|
+
return new Promise((resolve2, reject) => {
|
|
3591
3591
|
this.scriptBridge.startSession(sessionId, {
|
|
3592
3592
|
command,
|
|
3593
3593
|
args,
|
|
@@ -3609,7 +3609,7 @@ var require_src = __commonJS({
|
|
|
3609
3609
|
session.lastActivity = /* @__PURE__ */ new Date();
|
|
3610
3610
|
this.broadcastToSession(sessionId, { type: "script_stopped", sessionId });
|
|
3611
3611
|
if (exitCode === 0) {
|
|
3612
|
-
|
|
3612
|
+
resolve2({ code: exitCode, signal });
|
|
3613
3613
|
} else {
|
|
3614
3614
|
reject(new Error(`Script exited with code ${exitCode}`));
|
|
3615
3615
|
}
|
|
@@ -5270,7 +5270,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5270
5270
|
return 3001;
|
|
5271
5271
|
}
|
|
5272
5272
|
startDevServer(port, databaseUrl) {
|
|
5273
|
-
return new Promise((
|
|
5273
|
+
return new Promise((resolve2) => {
|
|
5274
5274
|
const env = { ...process.env, PORT: String(port), ...databaseUrl ? { DATABASE_URL: databaseUrl } : {} };
|
|
5275
5275
|
this.devProcess = (0, import_child_process3.spawn)("npm", ["run", "dev"], {
|
|
5276
5276
|
cwd: this.workingDir,
|
|
@@ -5282,7 +5282,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5282
5282
|
const timeout = setTimeout(() => {
|
|
5283
5283
|
if (!resolved) {
|
|
5284
5284
|
resolved = true;
|
|
5285
|
-
this.healthCheck(port).then(
|
|
5285
|
+
this.healthCheck(port).then(resolve2);
|
|
5286
5286
|
}
|
|
5287
5287
|
}, 15e3);
|
|
5288
5288
|
const onData = (data) => {
|
|
@@ -5291,7 +5291,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5291
5291
|
if (!resolved) {
|
|
5292
5292
|
resolved = true;
|
|
5293
5293
|
clearTimeout(timeout);
|
|
5294
|
-
|
|
5294
|
+
resolve2(true);
|
|
5295
5295
|
}
|
|
5296
5296
|
}
|
|
5297
5297
|
};
|
|
@@ -5302,7 +5302,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5302
5302
|
if (!resolved) {
|
|
5303
5303
|
resolved = true;
|
|
5304
5304
|
clearTimeout(timeout);
|
|
5305
|
-
|
|
5305
|
+
resolve2(false);
|
|
5306
5306
|
}
|
|
5307
5307
|
});
|
|
5308
5308
|
this.devProcess.unref();
|
|
@@ -6324,16 +6324,37 @@ ${links}
|
|
|
6324
6324
|
}
|
|
6325
6325
|
|
|
6326
6326
|
// src/server/graph/index.ts
|
|
6327
|
-
var
|
|
6328
|
-
var
|
|
6327
|
+
var import_node_fs9 = require("node:fs");
|
|
6328
|
+
var import_node_path10 = require("node:path");
|
|
6329
6329
|
|
|
6330
|
-
// src/server/graph/
|
|
6331
|
-
var
|
|
6332
|
-
var
|
|
6330
|
+
// src/server/graph/core/graph-builder.ts
|
|
6331
|
+
var import_node_fs8 = require("node:fs");
|
|
6332
|
+
var import_node_path9 = require("node:path");
|
|
6333
6333
|
|
|
6334
|
-
// src/server/graph/core/
|
|
6334
|
+
// src/server/graph/core/config.ts
|
|
6335
6335
|
var import_node_fs = require("node:fs");
|
|
6336
6336
|
var import_node_path = require("node:path");
|
|
6337
|
+
var CONFIG_FILENAME = ".launchchart.json";
|
|
6338
|
+
function loadConfig(rootDir) {
|
|
6339
|
+
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
6340
|
+
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
6341
|
+
try {
|
|
6342
|
+
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
6343
|
+
} catch {
|
|
6344
|
+
return {};
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
|
|
6348
|
+
// src/server/graph/core/parser-registry.ts
|
|
6349
|
+
var import_node_path8 = require("node:path");
|
|
6350
|
+
|
|
6351
|
+
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
6352
|
+
var import_node_fs3 = require("node:fs");
|
|
6353
|
+
var import_node_path3 = require("node:path");
|
|
6354
|
+
|
|
6355
|
+
// src/server/graph/core/ast-helpers.ts
|
|
6356
|
+
var import_node_fs2 = require("node:fs");
|
|
6357
|
+
var import_node_path2 = require("node:path");
|
|
6337
6358
|
var tsModule;
|
|
6338
6359
|
function getTs() {
|
|
6339
6360
|
if (!tsModule) {
|
|
@@ -6341,10 +6362,11 @@ function getTs() {
|
|
|
6341
6362
|
}
|
|
6342
6363
|
return tsModule;
|
|
6343
6364
|
}
|
|
6365
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
6344
6366
|
function parseFile(absPath) {
|
|
6345
6367
|
const ts = getTs();
|
|
6346
|
-
const content = (0,
|
|
6347
|
-
const ext = (0,
|
|
6368
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6369
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6348
6370
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
6349
6371
|
const sourceFile = ts.createSourceFile(
|
|
6350
6372
|
absPath,
|
|
@@ -6363,6 +6385,8 @@ function parseFile(absPath) {
|
|
|
6363
6385
|
const reExports = [];
|
|
6364
6386
|
const jsxElements = /* @__PURE__ */ new Set();
|
|
6365
6387
|
const navigations = [];
|
|
6388
|
+
const fetchCalls = [];
|
|
6389
|
+
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
6366
6390
|
function addExport(name2, kind) {
|
|
6367
6391
|
if (!exportsSet.has(name2)) {
|
|
6368
6392
|
exportsSet.add(name2);
|
|
@@ -6385,6 +6409,33 @@ function parseFile(absPath) {
|
|
|
6385
6409
|
}
|
|
6386
6410
|
return null;
|
|
6387
6411
|
}
|
|
6412
|
+
function looksLikeUrl(s) {
|
|
6413
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
6414
|
+
}
|
|
6415
|
+
function templateStartsWithSlash(expr) {
|
|
6416
|
+
const head = expr.head.text;
|
|
6417
|
+
return head.startsWith("/");
|
|
6418
|
+
}
|
|
6419
|
+
function extractUrlFromFetchArg(arg) {
|
|
6420
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
6421
|
+
if (!looksLikeUrl(arg.text)) return null;
|
|
6422
|
+
return { url: arg.text, isTemplate: false };
|
|
6423
|
+
}
|
|
6424
|
+
if (ts.isTemplateExpression(arg)) {
|
|
6425
|
+
if (!templateStartsWithSlash(arg)) return null;
|
|
6426
|
+
return { url: arg.getText(sourceFile), isTemplate: true };
|
|
6427
|
+
}
|
|
6428
|
+
if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
6429
|
+
let leftmost = arg;
|
|
6430
|
+
while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
6431
|
+
leftmost = leftmost.left;
|
|
6432
|
+
}
|
|
6433
|
+
if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
|
|
6434
|
+
return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
return null;
|
|
6438
|
+
}
|
|
6388
6439
|
function visit(node) {
|
|
6389
6440
|
if (ts.isImportDeclaration(node)) {
|
|
6390
6441
|
const moduleSpec = node.moduleSpecifier;
|
|
@@ -6408,6 +6459,8 @@ function parseFile(absPath) {
|
|
|
6408
6459
|
}
|
|
6409
6460
|
if (names.length > 0 || isTypeOnly) {
|
|
6410
6461
|
imports.push({ names, specifier, isTypeOnly, typeNames });
|
|
6462
|
+
} else if (!clause) {
|
|
6463
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
6411
6464
|
}
|
|
6412
6465
|
}
|
|
6413
6466
|
}
|
|
@@ -6421,6 +6474,19 @@ function parseFile(absPath) {
|
|
|
6421
6474
|
reExports.push({ name: exportedName, from: fromSpec });
|
|
6422
6475
|
}
|
|
6423
6476
|
}
|
|
6477
|
+
} else if (!node.exportClause && fromSpec) {
|
|
6478
|
+
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
6482
|
+
const arg = node.arguments[0];
|
|
6483
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
6484
|
+
imports.push({
|
|
6485
|
+
names: [],
|
|
6486
|
+
specifier: arg.text,
|
|
6487
|
+
isTypeOnly: false,
|
|
6488
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
6489
|
+
});
|
|
6424
6490
|
}
|
|
6425
6491
|
}
|
|
6426
6492
|
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
@@ -6482,6 +6548,36 @@ function parseFile(absPath) {
|
|
|
6482
6548
|
}
|
|
6483
6549
|
}
|
|
6484
6550
|
}
|
|
6551
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
6552
|
+
const expr = node.expression;
|
|
6553
|
+
const firstArg = node.arguments[0];
|
|
6554
|
+
if (ts.isIdentifier(expr) && expr.text === "fetch") {
|
|
6555
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
6556
|
+
if (extracted) {
|
|
6557
|
+
fetchCalls.push({
|
|
6558
|
+
url: extracted.url,
|
|
6559
|
+
isTemplate: extracted.isTemplate,
|
|
6560
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
6561
|
+
kind: "fetch"
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
6566
|
+
const methodName = expr.name.text;
|
|
6567
|
+
if (HTTP_METHODS.has(methodName)) {
|
|
6568
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
6569
|
+
if (extracted) {
|
|
6570
|
+
fetchCalls.push({
|
|
6571
|
+
method: methodName.toUpperCase(),
|
|
6572
|
+
url: extracted.url,
|
|
6573
|
+
isTemplate: extracted.isTemplate,
|
|
6574
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
6575
|
+
kind: "client-method"
|
|
6576
|
+
});
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6485
6581
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
6486
6582
|
const tagName = node.tagName;
|
|
6487
6583
|
if (ts.isIdentifier(tagName) && tagName.text === "Link") {
|
|
@@ -6531,7 +6627,8 @@ function parseFile(absPath) {
|
|
|
6531
6627
|
imports,
|
|
6532
6628
|
reExports,
|
|
6533
6629
|
jsxElements,
|
|
6534
|
-
navigations
|
|
6630
|
+
navigations,
|
|
6631
|
+
fetchCalls
|
|
6535
6632
|
};
|
|
6536
6633
|
}
|
|
6537
6634
|
var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -6547,8 +6644,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
|
6547
6644
|
]);
|
|
6548
6645
|
function extractDbCalls(absPath) {
|
|
6549
6646
|
const ts = getTs();
|
|
6550
|
-
const content = (0,
|
|
6551
|
-
const ext = (0,
|
|
6647
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6648
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6552
6649
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
6553
6650
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
6554
6651
|
const calls = [];
|
|
@@ -6577,8 +6674,8 @@ function extractDbCalls(absPath) {
|
|
|
6577
6674
|
}
|
|
6578
6675
|
function extractAuthWrappers(absPath) {
|
|
6579
6676
|
const ts = getTs();
|
|
6580
|
-
const content = (0,
|
|
6581
|
-
const ext = (0,
|
|
6677
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6678
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6582
6679
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
6583
6680
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
6584
6681
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -6599,50 +6696,101 @@ function extractAuthWrappers(absPath) {
|
|
|
6599
6696
|
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
6600
6697
|
function walk(dir, exts) {
|
|
6601
6698
|
const results = [];
|
|
6602
|
-
if (!(0,
|
|
6603
|
-
for (const entry of (0,
|
|
6604
|
-
const full = (0,
|
|
6699
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
6700
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
6701
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
6605
6702
|
if (entry.isDirectory()) {
|
|
6606
6703
|
results.push(...walk(full, exts));
|
|
6607
|
-
} else if (exts.includes((0,
|
|
6704
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
6608
6705
|
results.push(full);
|
|
6609
6706
|
}
|
|
6610
6707
|
}
|
|
6611
6708
|
return results;
|
|
6612
6709
|
}
|
|
6710
|
+
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
6711
|
+
const results = [];
|
|
6712
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
6713
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
6714
|
+
if (entry.isDirectory()) {
|
|
6715
|
+
if (ignoreDirs.has(entry.name)) continue;
|
|
6716
|
+
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
|
|
6717
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
6718
|
+
results.push((0, import_node_path3.join)(dir, entry.name));
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
return results;
|
|
6722
|
+
}
|
|
6613
6723
|
function toNodeId(srcDir, absPath) {
|
|
6614
|
-
return (0,
|
|
6724
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
6615
6725
|
}
|
|
6616
6726
|
function resolveImport(srcDir, specifier) {
|
|
6617
6727
|
if (!specifier.startsWith("@/")) return null;
|
|
6618
6728
|
const rel = specifier.slice(2);
|
|
6619
|
-
const base = (0,
|
|
6620
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
6621
|
-
if ((0,
|
|
6729
|
+
const base = (0, import_node_path3.join)(srcDir, rel);
|
|
6730
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
6731
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
6622
6732
|
}
|
|
6623
6733
|
return null;
|
|
6624
6734
|
}
|
|
6625
6735
|
function resolveRelativeImport(fromFile, specifier) {
|
|
6626
|
-
const base = (0,
|
|
6627
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
6628
|
-
if ((0,
|
|
6736
|
+
const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
|
|
6737
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
6738
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
6629
6739
|
}
|
|
6630
6740
|
return null;
|
|
6631
6741
|
}
|
|
6742
|
+
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
6743
|
+
const cached = memo.get(barrelAbsPath);
|
|
6744
|
+
if (cached) return cached;
|
|
6745
|
+
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
6746
|
+
visiting.add(barrelAbsPath);
|
|
6747
|
+
const parsed = parsedByPath.get(barrelAbsPath);
|
|
6748
|
+
const map = /* @__PURE__ */ new Map();
|
|
6749
|
+
if (!parsed) {
|
|
6750
|
+
visiting.delete(barrelAbsPath);
|
|
6751
|
+
memo.set(barrelAbsPath, map);
|
|
6752
|
+
return map;
|
|
6753
|
+
}
|
|
6754
|
+
for (const re of parsed.reExports) {
|
|
6755
|
+
if (!re.from.startsWith(".")) continue;
|
|
6756
|
+
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
6757
|
+
if (!resolved) continue;
|
|
6758
|
+
if (re.isWildcard) {
|
|
6759
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
6760
|
+
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
6761
|
+
if (targetIsBarrel) {
|
|
6762
|
+
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
6763
|
+
for (const [name, target] of nested) {
|
|
6764
|
+
if (!map.has(name)) map.set(name, target);
|
|
6765
|
+
}
|
|
6766
|
+
} else {
|
|
6767
|
+
const targetParsed = parsedByPath.get(resolved);
|
|
6768
|
+
if (targetParsed) {
|
|
6769
|
+
for (const exp of targetParsed.exports) {
|
|
6770
|
+
if (!map.has(exp)) map.set(exp, resolved);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
}
|
|
6774
|
+
} else {
|
|
6775
|
+
if (!map.has(re.name)) map.set(re.name, resolved);
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
visiting.delete(barrelAbsPath);
|
|
6779
|
+
memo.set(barrelAbsPath, map);
|
|
6780
|
+
return map;
|
|
6781
|
+
}
|
|
6632
6782
|
function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
6633
6783
|
const barrels = /* @__PURE__ */ new Map();
|
|
6784
|
+
const memo = /* @__PURE__ */ new Map();
|
|
6634
6785
|
for (const [absPath, parsed] of parsedByPath) {
|
|
6635
|
-
const bn = (0,
|
|
6786
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
6636
6787
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
6637
6788
|
if (parsed.reExports.length === 0) continue;
|
|
6638
|
-
const
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
const resolved = resolveRelativeImport(absPath, re.from);
|
|
6643
|
-
if (resolved) map.set(re.name, resolved);
|
|
6789
|
+
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
6790
|
+
if (map.size > 0) {
|
|
6791
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
6792
|
+
barrels.set(barrelId, map);
|
|
6644
6793
|
}
|
|
6645
|
-
if (map.size > 0) barrels.set(barrelId, map);
|
|
6646
6794
|
}
|
|
6647
6795
|
return barrels;
|
|
6648
6796
|
}
|
|
@@ -6699,7 +6847,7 @@ function extractRoute(id) {
|
|
|
6699
6847
|
return route || "/";
|
|
6700
6848
|
}
|
|
6701
6849
|
function nameFromFilename(absPath) {
|
|
6702
|
-
return (0,
|
|
6850
|
+
return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
6703
6851
|
}
|
|
6704
6852
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
6705
6853
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -6879,26 +7027,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
6879
7027
|
return { edges, flagged };
|
|
6880
7028
|
}
|
|
6881
7029
|
function detect(rootDir) {
|
|
6882
|
-
return (0,
|
|
7030
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app")) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.ts")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.js")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.mjs"));
|
|
6883
7031
|
}
|
|
6884
7032
|
function generate(rootDir) {
|
|
6885
|
-
const srcDir = (0,
|
|
6886
|
-
const appFiles = walk((0,
|
|
6887
|
-
(f) =>
|
|
7033
|
+
const srcDir = (0, import_node_path3.join)(rootDir, "src");
|
|
7034
|
+
const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
7035
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
6888
7036
|
);
|
|
6889
|
-
const clientFiles = walk((0,
|
|
6890
|
-
const serverFiles = walk((0,
|
|
6891
|
-
(f) => (0,
|
|
7037
|
+
const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
7038
|
+
const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
7039
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
6892
7040
|
);
|
|
6893
|
-
const libFiles = walk((0,
|
|
6894
|
-
const configFiles = walk((0,
|
|
7041
|
+
const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
7042
|
+
const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
6895
7043
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
6896
7044
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
6897
7045
|
for (const absPath of allDiscovered) {
|
|
6898
7046
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
6899
7047
|
}
|
|
6900
7048
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
6901
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
7049
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
6902
7050
|
const nodes = [];
|
|
6903
7051
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
6904
7052
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -6933,6 +7081,94 @@ function generate(rootDir) {
|
|
|
6933
7081
|
allEdges.push(...edges);
|
|
6934
7082
|
allFlagged.push(...flagged);
|
|
6935
7083
|
}
|
|
7084
|
+
const fetchCallEntries = [];
|
|
7085
|
+
for (const absPath of fileSet) {
|
|
7086
|
+
const sourceId = toNodeId(srcDir, absPath);
|
|
7087
|
+
const parsed = parsedByPath.get(absPath);
|
|
7088
|
+
if (parsed.fetchCalls.length === 0) continue;
|
|
7089
|
+
fetchCallEntries.push({
|
|
7090
|
+
nodeId: sourceId,
|
|
7091
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
7092
|
+
url: c.url,
|
|
7093
|
+
method: c.method,
|
|
7094
|
+
isTemplate: c.isTemplate,
|
|
7095
|
+
isConcat: c.isConcat,
|
|
7096
|
+
kind: c.kind
|
|
7097
|
+
}))
|
|
7098
|
+
});
|
|
7099
|
+
}
|
|
7100
|
+
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
7101
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
7102
|
+
"node_modules",
|
|
7103
|
+
".next",
|
|
7104
|
+
"dist",
|
|
7105
|
+
".launchsecure",
|
|
7106
|
+
".git",
|
|
7107
|
+
"src",
|
|
7108
|
+
"coverage",
|
|
7109
|
+
".turbo",
|
|
7110
|
+
"build",
|
|
7111
|
+
"out",
|
|
7112
|
+
".vercel"
|
|
7113
|
+
]);
|
|
7114
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
|
|
7115
|
+
for (const absPath of externalCandidates) {
|
|
7116
|
+
const normalized = absPath.replace(/\\/g, "/");
|
|
7117
|
+
if (externalScanned.has(normalized)) continue;
|
|
7118
|
+
let parsed;
|
|
7119
|
+
try {
|
|
7120
|
+
parsed = parseFile(absPath);
|
|
7121
|
+
} catch {
|
|
7122
|
+
continue;
|
|
7123
|
+
}
|
|
7124
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7125
|
+
const edgesFromThis = [];
|
|
7126
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7127
|
+
for (const imp of parsed.imports) {
|
|
7128
|
+
const { specifier, isTypeOnly, names } = imp;
|
|
7129
|
+
let resolved = null;
|
|
7130
|
+
if (specifier.startsWith("@/")) {
|
|
7131
|
+
const relToSrc = specifier.slice(2);
|
|
7132
|
+
const barrelMap = barrelMaps.get(relToSrc);
|
|
7133
|
+
if (barrelMap && names.length > 0) {
|
|
7134
|
+
for (const name of names) {
|
|
7135
|
+
const targetAbs = barrelMap.get(name);
|
|
7136
|
+
if (!targetAbs) continue;
|
|
7137
|
+
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
7138
|
+
if (!nodeIdSet.has(targetId2)) continue;
|
|
7139
|
+
const key2 = `${externalId}\u2192${targetId2}`;
|
|
7140
|
+
if (seen.has(key2)) continue;
|
|
7141
|
+
seen.add(key2);
|
|
7142
|
+
edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
|
|
7143
|
+
}
|
|
7144
|
+
continue;
|
|
7145
|
+
}
|
|
7146
|
+
resolved = resolveImport(srcDir, specifier);
|
|
7147
|
+
} else if (specifier.startsWith(".")) {
|
|
7148
|
+
resolved = resolveRelativeImport(absPath, specifier);
|
|
7149
|
+
}
|
|
7150
|
+
if (!resolved) continue;
|
|
7151
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
7152
|
+
if (!nodeIdSet.has(targetId)) continue;
|
|
7153
|
+
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
7154
|
+
const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
|
|
7155
|
+
if (seen.has(key)) continue;
|
|
7156
|
+
seen.add(key);
|
|
7157
|
+
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
7158
|
+
}
|
|
7159
|
+
if (edgesFromThis.length === 0) continue;
|
|
7160
|
+
nodes.push({
|
|
7161
|
+
id: externalId,
|
|
7162
|
+
type: "external",
|
|
7163
|
+
name: parsed.name || nameFromFilename(absPath),
|
|
7164
|
+
route: null,
|
|
7165
|
+
module: "external",
|
|
7166
|
+
exports: parsed.exports
|
|
7167
|
+
});
|
|
7168
|
+
nodeIdSet.add(externalId);
|
|
7169
|
+
nodeTypeMap.set(externalId, "external");
|
|
7170
|
+
allEdges.push(...edgesFromThis);
|
|
7171
|
+
}
|
|
6936
7172
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
6937
7173
|
const dedupedFlagged = allFlagged.filter((f) => {
|
|
6938
7174
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
@@ -6964,6 +7200,7 @@ function generate(rootDir) {
|
|
|
6964
7200
|
total_configs: byType("config"),
|
|
6965
7201
|
total_utils: byType("util"),
|
|
6966
7202
|
total_libs: byType("lib"),
|
|
7203
|
+
total_external: byType("external"),
|
|
6967
7204
|
total_edges: allEdges.length,
|
|
6968
7205
|
total_flagged: dedupedFlagged.length
|
|
6969
7206
|
};
|
|
@@ -6990,7 +7227,8 @@ function generate(rootDir) {
|
|
|
6990
7227
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
6991
7228
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
6992
7229
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
6993
|
-
}
|
|
7230
|
+
},
|
|
7231
|
+
fetch_calls: fetchCallEntries
|
|
6994
7232
|
}
|
|
6995
7233
|
};
|
|
6996
7234
|
}
|
|
@@ -7002,14 +7240,14 @@ var reactNextjsParser = {
|
|
|
7002
7240
|
};
|
|
7003
7241
|
|
|
7004
7242
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
7005
|
-
var
|
|
7006
|
-
var
|
|
7007
|
-
var
|
|
7243
|
+
var import_node_fs4 = require("node:fs");
|
|
7244
|
+
var import_node_path4 = require("node:path");
|
|
7245
|
+
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
7008
7246
|
function walk2(dir) {
|
|
7009
7247
|
const results = [];
|
|
7010
|
-
if (!(0,
|
|
7011
|
-
for (const entry of (0,
|
|
7012
|
-
const full = (0,
|
|
7248
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
7249
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
7250
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
7013
7251
|
if (entry.isDirectory()) {
|
|
7014
7252
|
results.push(...walk2(full));
|
|
7015
7253
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -7019,7 +7257,7 @@ function walk2(dir) {
|
|
|
7019
7257
|
return results;
|
|
7020
7258
|
}
|
|
7021
7259
|
function filePathToRoute(apiDir, absPath) {
|
|
7022
|
-
let route = "/" + (0,
|
|
7260
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
7023
7261
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
7024
7262
|
route = route.replace(/\/+/g, "/");
|
|
7025
7263
|
if (route === "/") return "/api";
|
|
@@ -7030,10 +7268,10 @@ function camelToPascal(s) {
|
|
|
7030
7268
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
7031
7269
|
}
|
|
7032
7270
|
function detect2(rootDir) {
|
|
7033
|
-
return (0,
|
|
7271
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
7034
7272
|
}
|
|
7035
7273
|
function generate2(rootDir) {
|
|
7036
|
-
const apiDir = (0,
|
|
7274
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
7037
7275
|
const routeFiles = walk2(apiDir);
|
|
7038
7276
|
const nodes = [];
|
|
7039
7277
|
const edges = [];
|
|
@@ -7048,10 +7286,10 @@ function generate2(rootDir) {
|
|
|
7048
7286
|
const authWrappers = extractAuthWrappers(absPath);
|
|
7049
7287
|
const methods = [];
|
|
7050
7288
|
for (const exp of parsed.exports) {
|
|
7051
|
-
if (
|
|
7289
|
+
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
7052
7290
|
}
|
|
7053
7291
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
7054
|
-
const relPath = (0,
|
|
7292
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7055
7293
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
7056
7294
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
7057
7295
|
const mutates = mutations.length > 0;
|
|
@@ -7118,7 +7356,7 @@ function generate2(rootDir) {
|
|
|
7118
7356
|
flagged_edges: [],
|
|
7119
7357
|
patterns: {
|
|
7120
7358
|
total_endpoints: nodes.length,
|
|
7121
|
-
methods_breakdown: [...
|
|
7359
|
+
methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
|
|
7122
7360
|
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
7123
7361
|
return acc;
|
|
7124
7362
|
}, {}),
|
|
@@ -7136,8 +7374,8 @@ var nextjsRoutesParser = {
|
|
|
7136
7374
|
};
|
|
7137
7375
|
|
|
7138
7376
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
7139
|
-
var
|
|
7140
|
-
var
|
|
7377
|
+
var import_node_fs5 = require("node:fs");
|
|
7378
|
+
var import_node_path5 = require("node:path");
|
|
7141
7379
|
function parseModels(content) {
|
|
7142
7380
|
const nodes = [];
|
|
7143
7381
|
const relations = [];
|
|
@@ -7228,11 +7466,11 @@ function parseEnums(content) {
|
|
|
7228
7466
|
return nodes;
|
|
7229
7467
|
}
|
|
7230
7468
|
function detect3(rootDir) {
|
|
7231
|
-
return (0,
|
|
7469
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
7232
7470
|
}
|
|
7233
7471
|
function generate3(rootDir) {
|
|
7234
|
-
const schemaPath = (0,
|
|
7235
|
-
const content = (0,
|
|
7472
|
+
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
7473
|
+
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
7236
7474
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
7237
7475
|
const enumNodes = parseEnums(content);
|
|
7238
7476
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -7288,35 +7526,654 @@ var prismaSchemaParser = {
|
|
|
7288
7526
|
generate: generate3
|
|
7289
7527
|
};
|
|
7290
7528
|
|
|
7529
|
+
// src/server/graph/core/api-route-matching.ts
|
|
7530
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
7531
|
+
const routes = [];
|
|
7532
|
+
for (const n of apiOutput.nodes) {
|
|
7533
|
+
const path9 = n.path;
|
|
7534
|
+
if (!path9 || typeof path9 !== "string") continue;
|
|
7535
|
+
routes.push({
|
|
7536
|
+
path: path9,
|
|
7537
|
+
nodeId: n.id,
|
|
7538
|
+
segments: path9.split("/").filter(Boolean)
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7541
|
+
return routes;
|
|
7542
|
+
}
|
|
7543
|
+
function buildApiPathMap(routes) {
|
|
7544
|
+
const map = /* @__PURE__ */ new Map();
|
|
7545
|
+
for (const r of routes) {
|
|
7546
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
7547
|
+
}
|
|
7548
|
+
return map;
|
|
7549
|
+
}
|
|
7550
|
+
function normalizeFetchUrl(raw) {
|
|
7551
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
7552
|
+
const qIdx = s.indexOf("?");
|
|
7553
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
7554
|
+
const hIdx = s.indexOf("#");
|
|
7555
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
7556
|
+
let hadInterpolation = false;
|
|
7557
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
7558
|
+
hadInterpolation = true;
|
|
7559
|
+
const cleaned = expr.trim();
|
|
7560
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
7561
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
7562
|
+
return ":" + name;
|
|
7563
|
+
});
|
|
7564
|
+
s = s.replace(/\/+/g, "/");
|
|
7565
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
7566
|
+
return { path: s || "/", hadInterpolation };
|
|
7567
|
+
}
|
|
7568
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
7569
|
+
if (candidate.length !== known.length) return -1;
|
|
7570
|
+
let score = 0;
|
|
7571
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
7572
|
+
const a = candidate[i];
|
|
7573
|
+
const b = known[i];
|
|
7574
|
+
if (a === b) {
|
|
7575
|
+
score += 3;
|
|
7576
|
+
continue;
|
|
7577
|
+
}
|
|
7578
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
7579
|
+
score += 2;
|
|
7580
|
+
continue;
|
|
7581
|
+
}
|
|
7582
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
7583
|
+
score += 1;
|
|
7584
|
+
continue;
|
|
7585
|
+
}
|
|
7586
|
+
return -1;
|
|
7587
|
+
}
|
|
7588
|
+
return score;
|
|
7589
|
+
}
|
|
7590
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
7591
|
+
const raw = call.url;
|
|
7592
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
7593
|
+
return { kind: "external", normalizedUrl: raw };
|
|
7594
|
+
}
|
|
7595
|
+
if (call.isConcat) {
|
|
7596
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
7597
|
+
}
|
|
7598
|
+
const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
|
|
7599
|
+
if (!path9.startsWith("/")) {
|
|
7600
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7601
|
+
}
|
|
7602
|
+
const segs = path9.split("/").filter(Boolean);
|
|
7603
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
7604
|
+
return { kind: "dynamic", normalizedUrl: path9 };
|
|
7605
|
+
}
|
|
7606
|
+
const exact = apiPathMap.get(path9);
|
|
7607
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
|
|
7608
|
+
let bestScore = -1;
|
|
7609
|
+
let bestId = null;
|
|
7610
|
+
for (const r of apiRoutes) {
|
|
7611
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
7612
|
+
if (score > bestScore) {
|
|
7613
|
+
bestScore = score;
|
|
7614
|
+
bestId = r.nodeId;
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7617
|
+
if (bestId && bestScore > 0) {
|
|
7618
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
|
|
7619
|
+
}
|
|
7620
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7621
|
+
}
|
|
7622
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
7623
|
+
const { path: path9, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
7624
|
+
if (!path9.startsWith("/")) {
|
|
7625
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7626
|
+
}
|
|
7627
|
+
const segs = path9.split("/").filter(Boolean);
|
|
7628
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
7629
|
+
return { kind: "dynamic", normalizedUrl: path9 };
|
|
7630
|
+
}
|
|
7631
|
+
const exact = apiPathMap.get(path9);
|
|
7632
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
|
|
7633
|
+
let bestScore = -1;
|
|
7634
|
+
let bestId = null;
|
|
7635
|
+
for (const r of apiRoutes) {
|
|
7636
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
7637
|
+
if (score > bestScore) {
|
|
7638
|
+
bestScore = score;
|
|
7639
|
+
bestId = r.nodeId;
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
if (bestId && bestScore > 0) {
|
|
7643
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
|
|
7644
|
+
}
|
|
7645
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7646
|
+
}
|
|
7647
|
+
|
|
7648
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
7649
|
+
var fetchResolverParser = {
|
|
7650
|
+
id: "fetch-resolver",
|
|
7651
|
+
layer: "crosslayer",
|
|
7652
|
+
detect(_rootDir) {
|
|
7653
|
+
return true;
|
|
7654
|
+
},
|
|
7655
|
+
generate(_rootDir, layerOutputs) {
|
|
7656
|
+
const uiOutput = layerOutputs.get("ui");
|
|
7657
|
+
const apiOutput = layerOutputs.get("api");
|
|
7658
|
+
if (!uiOutput || !apiOutput) {
|
|
7659
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7660
|
+
}
|
|
7661
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
7662
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7663
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
7664
|
+
if (fetchCallEntries.length === 0) {
|
|
7665
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7666
|
+
}
|
|
7667
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
7668
|
+
const crossRefs = [];
|
|
7669
|
+
const flaggedEdges = [];
|
|
7670
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7671
|
+
let resolvedCount = 0;
|
|
7672
|
+
let dynamicCount = 0;
|
|
7673
|
+
let unresolvedCount = 0;
|
|
7674
|
+
let externalCount = 0;
|
|
7675
|
+
for (const entry of fetchCallEntries) {
|
|
7676
|
+
for (const call of entry.calls) {
|
|
7677
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
7678
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
7679
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7680
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
7681
|
+
if (seen.has(key)) continue;
|
|
7682
|
+
seen.add(key);
|
|
7683
|
+
crossRefs.push({
|
|
7684
|
+
source: entry.nodeId,
|
|
7685
|
+
target: result.nodeId,
|
|
7686
|
+
type: "calls_api",
|
|
7687
|
+
layer: "api"
|
|
7688
|
+
});
|
|
7689
|
+
resolvedCount++;
|
|
7690
|
+
continue;
|
|
7691
|
+
}
|
|
7692
|
+
if (result.kind === "dynamic") {
|
|
7693
|
+
dynamicCount++;
|
|
7694
|
+
flaggedEdges.push({
|
|
7695
|
+
source: entry.nodeId,
|
|
7696
|
+
target: "DYNAMIC",
|
|
7697
|
+
type: "calls_api",
|
|
7698
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
7699
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
7700
|
+
});
|
|
7701
|
+
continue;
|
|
7702
|
+
}
|
|
7703
|
+
if (result.kind === "external") {
|
|
7704
|
+
externalCount++;
|
|
7705
|
+
if (!includeExternal) continue;
|
|
7706
|
+
flaggedEdges.push({
|
|
7707
|
+
source: entry.nodeId,
|
|
7708
|
+
target: "EXTERNAL",
|
|
7709
|
+
type: "calls_external",
|
|
7710
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
7711
|
+
confidence: "high"
|
|
7712
|
+
});
|
|
7713
|
+
continue;
|
|
7714
|
+
}
|
|
7715
|
+
unresolvedCount++;
|
|
7716
|
+
flaggedEdges.push({
|
|
7717
|
+
source: entry.nodeId,
|
|
7718
|
+
target: "UNRESOLVED",
|
|
7719
|
+
type: "calls_api",
|
|
7720
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
7721
|
+
confidence: "medium"
|
|
7722
|
+
});
|
|
7723
|
+
}
|
|
7724
|
+
}
|
|
7725
|
+
return {
|
|
7726
|
+
cross_refs: crossRefs,
|
|
7727
|
+
flagged_edges: flaggedEdges,
|
|
7728
|
+
warnings: [],
|
|
7729
|
+
patterns: {
|
|
7730
|
+
api_call_detection: {
|
|
7731
|
+
resolved: resolvedCount,
|
|
7732
|
+
dynamic: dynamicCount,
|
|
7733
|
+
unresolved: unresolvedCount,
|
|
7734
|
+
external: externalCount
|
|
7735
|
+
}
|
|
7736
|
+
}
|
|
7737
|
+
};
|
|
7738
|
+
}
|
|
7739
|
+
};
|
|
7740
|
+
|
|
7741
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
7742
|
+
var import_node_fs6 = require("node:fs");
|
|
7743
|
+
var import_node_path6 = require("node:path");
|
|
7744
|
+
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
7745
|
+
function walk3(dir, exts) {
|
|
7746
|
+
if (!(0, import_node_fs6.existsSync)(dir)) return [];
|
|
7747
|
+
const results = [];
|
|
7748
|
+
for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
|
|
7749
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
7750
|
+
const full = (0, import_node_path6.join)(dir, entry.name);
|
|
7751
|
+
if (entry.isDirectory()) {
|
|
7752
|
+
results.push(...walk3(full, exts));
|
|
7753
|
+
} else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
|
|
7754
|
+
results.push(full);
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
return results;
|
|
7758
|
+
}
|
|
7759
|
+
function toNodeId2(srcDir, absPath) {
|
|
7760
|
+
return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
7761
|
+
}
|
|
7762
|
+
var apiAnnotationsParser = {
|
|
7763
|
+
id: "api-annotations",
|
|
7764
|
+
layer: "crosslayer",
|
|
7765
|
+
detect(rootDir) {
|
|
7766
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
|
|
7767
|
+
},
|
|
7768
|
+
generate(rootDir, layerOutputs) {
|
|
7769
|
+
const apiOutput = layerOutputs.get("api");
|
|
7770
|
+
if (!apiOutput) {
|
|
7771
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7772
|
+
}
|
|
7773
|
+
const uiOutput = layerOutputs.get("ui");
|
|
7774
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
7775
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
7776
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7777
|
+
const srcDir = (0, import_node_path6.join)(rootDir, "src");
|
|
7778
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
7779
|
+
const crossRefs = [];
|
|
7780
|
+
const flaggedEdges = [];
|
|
7781
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7782
|
+
for (const absPath of files) {
|
|
7783
|
+
const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
|
|
7784
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
7785
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
7786
|
+
let match;
|
|
7787
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
7788
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
7789
|
+
const method = match[1];
|
|
7790
|
+
const urlPath = match[2];
|
|
7791
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
7792
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7793
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
7794
|
+
if (seen.has(key)) continue;
|
|
7795
|
+
seen.add(key);
|
|
7796
|
+
crossRefs.push({
|
|
7797
|
+
source: sourceId,
|
|
7798
|
+
target: result.nodeId,
|
|
7799
|
+
type: "calls_api",
|
|
7800
|
+
layer: "api"
|
|
7801
|
+
});
|
|
7802
|
+
} else {
|
|
7803
|
+
flaggedEdges.push({
|
|
7804
|
+
source: sourceId,
|
|
7805
|
+
target: "UNRESOLVED",
|
|
7806
|
+
type: "annotation_unresolved",
|
|
7807
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
7808
|
+
confidence: "high"
|
|
7809
|
+
});
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
}
|
|
7813
|
+
return {
|
|
7814
|
+
cross_refs: crossRefs,
|
|
7815
|
+
flagged_edges: flaggedEdges,
|
|
7816
|
+
warnings: [],
|
|
7817
|
+
patterns: {
|
|
7818
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
7819
|
+
annotations_resolved: crossRefs.length,
|
|
7820
|
+
annotations_unresolved: flaggedEdges.length
|
|
7821
|
+
}
|
|
7822
|
+
};
|
|
7823
|
+
}
|
|
7824
|
+
};
|
|
7825
|
+
|
|
7826
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
7827
|
+
var import_node_fs7 = require("node:fs");
|
|
7828
|
+
var import_node_path7 = require("node:path");
|
|
7829
|
+
var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
7830
|
+
function walk4(dir, exts) {
|
|
7831
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
7832
|
+
const results = [];
|
|
7833
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
7834
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
7835
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
7836
|
+
if (entry.isDirectory()) {
|
|
7837
|
+
results.push(...walk4(full, exts));
|
|
7838
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
7839
|
+
results.push(full);
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7842
|
+
return results;
|
|
7843
|
+
}
|
|
7844
|
+
function toNodeId3(srcDir, absPath) {
|
|
7845
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
7846
|
+
}
|
|
7847
|
+
var urlLiteralScannerParser = {
|
|
7848
|
+
id: "url-literal-scanner",
|
|
7849
|
+
layer: "crosslayer",
|
|
7850
|
+
detect(rootDir) {
|
|
7851
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
7852
|
+
},
|
|
7853
|
+
generate(rootDir, layerOutputs) {
|
|
7854
|
+
const apiOutput = layerOutputs.get("api");
|
|
7855
|
+
if (!apiOutput) {
|
|
7856
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7857
|
+
}
|
|
7858
|
+
const uiOutput = layerOutputs.get("ui");
|
|
7859
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
7860
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
7861
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7862
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
7863
|
+
const clientDir = (0, import_node_path7.join)(srcDir, "client");
|
|
7864
|
+
const appDir = (0, import_node_path7.join)(srcDir, "app");
|
|
7865
|
+
const files = [
|
|
7866
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
7867
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
7868
|
+
];
|
|
7869
|
+
const crossRefs = [];
|
|
7870
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7871
|
+
for (const absPath of files) {
|
|
7872
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
7873
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
7874
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
7875
|
+
let match;
|
|
7876
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
7877
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
7878
|
+
const urlPath = match[1];
|
|
7879
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
7880
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7881
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
7882
|
+
if (seen.has(key)) continue;
|
|
7883
|
+
seen.add(key);
|
|
7884
|
+
crossRefs.push({
|
|
7885
|
+
source: sourceId,
|
|
7886
|
+
target: result.nodeId,
|
|
7887
|
+
type: "references_api",
|
|
7888
|
+
layer: "api"
|
|
7889
|
+
});
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
return {
|
|
7894
|
+
cross_refs: crossRefs,
|
|
7895
|
+
flagged_edges: [],
|
|
7896
|
+
warnings: [],
|
|
7897
|
+
patterns: {
|
|
7898
|
+
url_literals_resolved: crossRefs.length
|
|
7899
|
+
}
|
|
7900
|
+
};
|
|
7901
|
+
}
|
|
7902
|
+
};
|
|
7903
|
+
|
|
7904
|
+
// src/server/graph/core/parser-registry.ts
|
|
7905
|
+
var ParserRegistry = class {
|
|
7906
|
+
constructor() {
|
|
7907
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
7908
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
7909
|
+
}
|
|
7910
|
+
register(parser) {
|
|
7911
|
+
if (this.ids.has(parser.id)) {
|
|
7912
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
7913
|
+
}
|
|
7914
|
+
this.ids.add(parser.id);
|
|
7915
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
7916
|
+
list.push(parser);
|
|
7917
|
+
this.parsers.set(parser.layer, list);
|
|
7918
|
+
}
|
|
7919
|
+
getParsers(layer) {
|
|
7920
|
+
return this.parsers.get(layer) ?? [];
|
|
7921
|
+
}
|
|
7922
|
+
getCrossLayerParsers() {
|
|
7923
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
7924
|
+
}
|
|
7925
|
+
getAll() {
|
|
7926
|
+
const all = [];
|
|
7927
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
7928
|
+
return all;
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7931
|
+
function registerBuiltins(registry, disabled) {
|
|
7932
|
+
const builtins = [
|
|
7933
|
+
reactNextjsParser,
|
|
7934
|
+
nextjsRoutesParser,
|
|
7935
|
+
prismaSchemaParser,
|
|
7936
|
+
fetchResolverParser,
|
|
7937
|
+
apiAnnotationsParser,
|
|
7938
|
+
urlLiteralScannerParser
|
|
7939
|
+
];
|
|
7940
|
+
for (const parser of builtins) {
|
|
7941
|
+
if (disabled.has(parser.id)) continue;
|
|
7942
|
+
registry.register(parser);
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
7946
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
7947
|
+
try {
|
|
7948
|
+
const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
|
|
7949
|
+
const mod = require(absPath);
|
|
7950
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
7951
|
+
if (disabled.has(parser.id)) continue;
|
|
7952
|
+
if (parser.layer !== entry.layer) {
|
|
7953
|
+
process.stderr.write(
|
|
7954
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
7955
|
+
`
|
|
7956
|
+
);
|
|
7957
|
+
}
|
|
7958
|
+
registry.register(parser);
|
|
7959
|
+
} catch (err2) {
|
|
7960
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
7961
|
+
`);
|
|
7962
|
+
}
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
function createRegistry(config, rootDir) {
|
|
7966
|
+
const registry = new ParserRegistry();
|
|
7967
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
7968
|
+
registerBuiltins(registry, disabled);
|
|
7969
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
7970
|
+
return registry;
|
|
7971
|
+
}
|
|
7972
|
+
|
|
7973
|
+
// src/server/graph/core/merge.ts
|
|
7974
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
7975
|
+
if (outputs.length === 0) {
|
|
7976
|
+
return {
|
|
7977
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
7978
|
+
nodes: [],
|
|
7979
|
+
edges: [],
|
|
7980
|
+
cross_refs: [],
|
|
7981
|
+
contradictions: [],
|
|
7982
|
+
warnings: [],
|
|
7983
|
+
flagged_edges: []
|
|
7984
|
+
};
|
|
7985
|
+
}
|
|
7986
|
+
if (outputs.length === 1) return outputs[0];
|
|
7987
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
7988
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
7989
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
7990
|
+
const mergedNodes = [];
|
|
7991
|
+
const mergedEdges = [];
|
|
7992
|
+
const mergedCrossRefs = [];
|
|
7993
|
+
const mergedContradictions = [];
|
|
7994
|
+
const mergedWarnings = [];
|
|
7995
|
+
const mergedFlagged = [];
|
|
7996
|
+
const parserIds = [];
|
|
7997
|
+
for (const output of outputs) {
|
|
7998
|
+
if (output.metadata.parser) {
|
|
7999
|
+
parserIds.push(String(output.metadata.parser));
|
|
8000
|
+
}
|
|
8001
|
+
for (const node of output.nodes) {
|
|
8002
|
+
if (seenNodes.has(node.id)) {
|
|
8003
|
+
mergedWarnings.push({
|
|
8004
|
+
type: "merge_conflict",
|
|
8005
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
8006
|
+
});
|
|
8007
|
+
continue;
|
|
8008
|
+
}
|
|
8009
|
+
seenNodes.add(node.id);
|
|
8010
|
+
mergedNodes.push(node);
|
|
8011
|
+
}
|
|
8012
|
+
for (const edge of output.edges) {
|
|
8013
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
8014
|
+
if (seenEdges.has(key)) continue;
|
|
8015
|
+
seenEdges.add(key);
|
|
8016
|
+
mergedEdges.push(edge);
|
|
8017
|
+
}
|
|
8018
|
+
for (const ref of output.cross_refs) {
|
|
8019
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
8020
|
+
if (seenCrossRefs.has(key)) continue;
|
|
8021
|
+
seenCrossRefs.add(key);
|
|
8022
|
+
mergedCrossRefs.push(ref);
|
|
8023
|
+
}
|
|
8024
|
+
mergedContradictions.push(...output.contradictions);
|
|
8025
|
+
mergedWarnings.push(...output.warnings);
|
|
8026
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
8027
|
+
}
|
|
8028
|
+
const metadata = {
|
|
8029
|
+
...outputs[0].metadata,
|
|
8030
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8031
|
+
parsers: parserIds
|
|
8032
|
+
};
|
|
8033
|
+
return {
|
|
8034
|
+
metadata,
|
|
8035
|
+
nodes: mergedNodes,
|
|
8036
|
+
edges: mergedEdges,
|
|
8037
|
+
cross_refs: mergedCrossRefs,
|
|
8038
|
+
contradictions: mergedContradictions,
|
|
8039
|
+
warnings: mergedWarnings,
|
|
8040
|
+
flagged_edges: mergedFlagged,
|
|
8041
|
+
patterns: outputs[0].patterns
|
|
8042
|
+
};
|
|
8043
|
+
}
|
|
8044
|
+
function dedupCrossRefs(refs) {
|
|
8045
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8046
|
+
const result = [];
|
|
8047
|
+
for (const ref of refs) {
|
|
8048
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
8049
|
+
if (seen.has(key)) continue;
|
|
8050
|
+
seen.add(key);
|
|
8051
|
+
result.push(ref);
|
|
8052
|
+
}
|
|
8053
|
+
return result;
|
|
8054
|
+
}
|
|
8055
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
8056
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
8057
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
8058
|
+
const allWarnings = [...uiOutput.warnings];
|
|
8059
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
8060
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
8061
|
+
if (primaryResult) {
|
|
8062
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
8063
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
8064
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
8065
|
+
}
|
|
8066
|
+
const primarySet = new Set(
|
|
8067
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
8068
|
+
);
|
|
8069
|
+
for (const sec of secondaryResults) {
|
|
8070
|
+
for (const ref of sec.output.cross_refs) {
|
|
8071
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
8072
|
+
if (primarySet.has(key)) {
|
|
8073
|
+
allCrossRefs.push(ref);
|
|
8074
|
+
} else {
|
|
8075
|
+
allFlagged.push({
|
|
8076
|
+
source: ref.source,
|
|
8077
|
+
target: ref.target,
|
|
8078
|
+
type: "out_of_pattern",
|
|
8079
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
8080
|
+
confidence: "medium"
|
|
8081
|
+
});
|
|
8082
|
+
allCrossRefs.push(ref);
|
|
8083
|
+
}
|
|
8084
|
+
}
|
|
8085
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
8086
|
+
allWarnings.push(...sec.output.warnings);
|
|
8087
|
+
}
|
|
8088
|
+
return {
|
|
8089
|
+
...uiOutput,
|
|
8090
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
8091
|
+
flagged_edges: allFlagged,
|
|
8092
|
+
warnings: allWarnings
|
|
8093
|
+
};
|
|
8094
|
+
}
|
|
8095
|
+
|
|
7291
8096
|
// src/server/graph/core/graph-builder.ts
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
8097
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
8098
|
+
const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
8099
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) return null;
|
|
8100
|
+
try {
|
|
8101
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
|
|
8102
|
+
} catch {
|
|
8103
|
+
return null;
|
|
8104
|
+
}
|
|
7299
8105
|
}
|
|
7300
8106
|
function generateLayer(rootDir, layer) {
|
|
7301
|
-
const
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
const
|
|
8107
|
+
const config = loadConfig(rootDir);
|
|
8108
|
+
const registry = createRegistry(config, rootDir);
|
|
8109
|
+
const parsers = registry.getParsers(layer);
|
|
8110
|
+
const outputs = [];
|
|
8111
|
+
for (const parser of parsers) {
|
|
8112
|
+
if (!parser.detect(rootDir)) continue;
|
|
8113
|
+
outputs.push(parser.generate(rootDir));
|
|
8114
|
+
}
|
|
8115
|
+
if (outputs.length === 0) return null;
|
|
8116
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
8117
|
+
if (layer === "ui") {
|
|
8118
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
8119
|
+
layerOutputs.set("ui", merged);
|
|
8120
|
+
for (const otherLayer of ["api", "db"]) {
|
|
8121
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
8122
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
8123
|
+
}
|
|
8124
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
8125
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
8126
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
8127
|
+
if (crossResults.length > 0) {
|
|
8128
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
8129
|
+
}
|
|
8130
|
+
}
|
|
7305
8131
|
return {
|
|
7306
8132
|
layer,
|
|
7307
|
-
output,
|
|
7308
|
-
nodeCount:
|
|
7309
|
-
edgeCount:
|
|
8133
|
+
output: merged,
|
|
8134
|
+
nodeCount: merged.nodes.length,
|
|
8135
|
+
edgeCount: merged.edges.length
|
|
7310
8136
|
};
|
|
7311
8137
|
}
|
|
7312
8138
|
function generateAll(rootDir) {
|
|
7313
|
-
const
|
|
8139
|
+
const config = loadConfig(rootDir);
|
|
8140
|
+
const registry = createRegistry(config, rootDir);
|
|
8141
|
+
const layerOrder = ["api", "db", "ui"];
|
|
8142
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
7314
8143
|
const results = [];
|
|
7315
|
-
for (const layer of
|
|
7316
|
-
const
|
|
7317
|
-
|
|
8144
|
+
for (const layer of layerOrder) {
|
|
8145
|
+
const parsers = registry.getParsers(layer);
|
|
8146
|
+
const outputs = [];
|
|
8147
|
+
for (const parser of parsers) {
|
|
8148
|
+
if (!parser.detect(rootDir)) continue;
|
|
8149
|
+
outputs.push(parser.generate(rootDir));
|
|
8150
|
+
}
|
|
8151
|
+
if (outputs.length === 0) continue;
|
|
8152
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
8153
|
+
layerOutputs.set(layer, merged);
|
|
8154
|
+
results.push({
|
|
8155
|
+
layer,
|
|
8156
|
+
output: merged,
|
|
8157
|
+
nodeCount: merged.nodes.length,
|
|
8158
|
+
edgeCount: merged.edges.length
|
|
8159
|
+
});
|
|
7318
8160
|
}
|
|
7319
|
-
|
|
8161
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
8162
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
8163
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
8164
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
8165
|
+
const uiOutput = layerOutputs.get("ui");
|
|
8166
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
8167
|
+
layerOutputs.set("ui", merged);
|
|
8168
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
8169
|
+
if (uiResult) {
|
|
8170
|
+
uiResult.output = merged;
|
|
8171
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
8172
|
+
uiResult.edgeCount = merged.edges.length;
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
8176
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
7320
8177
|
}
|
|
7321
8178
|
|
|
7322
8179
|
// src/server/graph/index.ts
|
|
@@ -7324,23 +8181,23 @@ var GRAPHS_DIR = ".launchsecure/graphs";
|
|
|
7324
8181
|
var LAYERS = ["ui", "api", "db"];
|
|
7325
8182
|
var graphCache = /* @__PURE__ */ new Map();
|
|
7326
8183
|
function graphsDir(rootDir) {
|
|
7327
|
-
return (0,
|
|
8184
|
+
return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
|
|
7328
8185
|
}
|
|
7329
8186
|
function graphFilePath(rootDir, layer) {
|
|
7330
|
-
return (0,
|
|
8187
|
+
return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
|
|
7331
8188
|
}
|
|
7332
8189
|
function invalidateCache(filePath) {
|
|
7333
8190
|
graphCache.delete(filePath);
|
|
7334
8191
|
}
|
|
7335
8192
|
function readGraph(rootDir, layer) {
|
|
7336
8193
|
const filePath = graphFilePath(rootDir, layer);
|
|
7337
|
-
if (!(0,
|
|
7338
|
-
const stat = (0,
|
|
8194
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
8195
|
+
const stat = (0, import_node_fs9.statSync)(filePath);
|
|
7339
8196
|
const cached = graphCache.get(filePath);
|
|
7340
8197
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
7341
8198
|
return cached.graph;
|
|
7342
8199
|
}
|
|
7343
|
-
const content = (0,
|
|
8200
|
+
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
7344
8201
|
const graph = JSON.parse(content);
|
|
7345
8202
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
7346
8203
|
return graph;
|
|
@@ -7355,11 +8212,11 @@ function readAllGraphs(rootDir) {
|
|
|
7355
8212
|
}
|
|
7356
8213
|
function generateGraph(rootDir, layer) {
|
|
7357
8214
|
const dir = graphsDir(rootDir);
|
|
7358
|
-
(0,
|
|
8215
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
7359
8216
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
7360
8217
|
for (const result of results) {
|
|
7361
8218
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
7362
|
-
(0,
|
|
8219
|
+
(0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
7363
8220
|
invalidateCache(filePath);
|
|
7364
8221
|
}
|
|
7365
8222
|
return results;
|
|
@@ -7421,8 +8278,77 @@ function handleGraphCommand(subcommand, args) {
|
|
|
7421
8278
|
}
|
|
7422
8279
|
|
|
7423
8280
|
// src/server/graph-mcp.ts
|
|
7424
|
-
var
|
|
7425
|
-
var
|
|
8281
|
+
var import_node_fs11 = require("node:fs");
|
|
8282
|
+
var import_node_path12 = require("node:path");
|
|
8283
|
+
var import_node_child_process2 = require("node:child_process");
|
|
8284
|
+
var import_node_os2 = require("node:os");
|
|
8285
|
+
|
|
8286
|
+
// src/server/lockfile.ts
|
|
8287
|
+
var import_node_child_process = require("node:child_process");
|
|
8288
|
+
var import_node_fs10 = require("node:fs");
|
|
8289
|
+
var import_node_os = require("node:os");
|
|
8290
|
+
var import_node_path11 = require("node:path");
|
|
8291
|
+
function lockDir() {
|
|
8292
|
+
return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
8293
|
+
}
|
|
8294
|
+
function lockPath() {
|
|
8295
|
+
return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
|
|
8296
|
+
}
|
|
8297
|
+
function readLock() {
|
|
8298
|
+
const p = lockPath();
|
|
8299
|
+
if (!(0, import_node_fs10.existsSync)(p)) return null;
|
|
8300
|
+
try {
|
|
8301
|
+
const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
|
|
8302
|
+
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
8303
|
+
return data;
|
|
8304
|
+
} catch {
|
|
8305
|
+
return null;
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
function isPidAlive(pid) {
|
|
8309
|
+
try {
|
|
8310
|
+
process.kill(pid, 0);
|
|
8311
|
+
return true;
|
|
8312
|
+
} catch {
|
|
8313
|
+
return false;
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
function getListenerPid(port) {
|
|
8317
|
+
try {
|
|
8318
|
+
const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
8319
|
+
encoding: "utf-8",
|
|
8320
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
8321
|
+
timeout: 500
|
|
8322
|
+
}).trim();
|
|
8323
|
+
if (!out) return null;
|
|
8324
|
+
const pid = parseInt(out.split("\n")[0], 10);
|
|
8325
|
+
return Number.isFinite(pid) ? pid : null;
|
|
8326
|
+
} catch {
|
|
8327
|
+
return null;
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
function getLiveLock() {
|
|
8331
|
+
const lock = readLock();
|
|
8332
|
+
if (!lock) return null;
|
|
8333
|
+
const listenerPid = getListenerPid(lock.port);
|
|
8334
|
+
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
8335
|
+
if (!live) {
|
|
8336
|
+
try {
|
|
8337
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
8338
|
+
} catch {
|
|
8339
|
+
}
|
|
8340
|
+
return null;
|
|
8341
|
+
}
|
|
8342
|
+
return lock;
|
|
8343
|
+
}
|
|
8344
|
+
function clearLock() {
|
|
8345
|
+
try {
|
|
8346
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
8347
|
+
} catch {
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
|
|
8351
|
+
// src/server/graph-mcp.ts
|
|
7426
8352
|
var SERVER_INFO = {
|
|
7427
8353
|
name: "launchsecure-graph",
|
|
7428
8354
|
version: "0.0.1"
|
|
@@ -7548,6 +8474,45 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
7548
8474
|
},
|
|
7549
8475
|
required: ["layer", "pattern"]
|
|
7550
8476
|
}
|
|
8477
|
+
},
|
|
8478
|
+
{
|
|
8479
|
+
name: "chart_server_status",
|
|
8480
|
+
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
8481
|
+
|
|
8482
|
+
Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
|
|
8483
|
+
inputSchema: {
|
|
8484
|
+
type: "object",
|
|
8485
|
+
properties: {}
|
|
8486
|
+
}
|
|
8487
|
+
},
|
|
8488
|
+
{
|
|
8489
|
+
name: "start_chart_server",
|
|
8490
|
+
description: 'Start the launch-chart UI server as a detached background process. The server serves the interactive project graph visualization at http://localhost:<port>. If the server is already running, returns the existing URL without spawning a duplicate. \n\nUse this when the user asks to "start the chart", "fire up charts", "open the graph UI", etc.',
|
|
8491
|
+
inputSchema: {
|
|
8492
|
+
type: "object",
|
|
8493
|
+
properties: {
|
|
8494
|
+
port: {
|
|
8495
|
+
type: "number",
|
|
8496
|
+
description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
|
|
8497
|
+
}
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8500
|
+
},
|
|
8501
|
+
{
|
|
8502
|
+
name: "stop_chart_server",
|
|
8503
|
+
description: 'Stop the running launch-chart UI server. Sends SIGTERM to the server process and cleans up the lock file. If no server is running, returns a no-op response. \n\nUse this when the user asks to "stop the chart", "cool down charts", "kill the graph server", etc.',
|
|
8504
|
+
inputSchema: {
|
|
8505
|
+
type: "object",
|
|
8506
|
+
properties: {}
|
|
8507
|
+
}
|
|
8508
|
+
},
|
|
8509
|
+
{
|
|
8510
|
+
name: "detect_project_stack",
|
|
8511
|
+
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify the tech stack (Next.js, Prisma, React, etc.), reports which built-in parsers are applicable, and provides cross-layer detection stats (fetch calls, @api annotations, URL literals). Returns a recommended primary parser and current .launchchart.json config if present. \n\nUse this when setting up launch-chart for a new project or reviewing parser configuration.",
|
|
8512
|
+
inputSchema: {
|
|
8513
|
+
type: "object",
|
|
8514
|
+
properties: {}
|
|
8515
|
+
}
|
|
7551
8516
|
}
|
|
7552
8517
|
];
|
|
7553
8518
|
function matchesSearch(node, query) {
|
|
@@ -7900,9 +8865,9 @@ function handleReadGraph(args) {
|
|
|
7900
8865
|
return okJson(result);
|
|
7901
8866
|
}
|
|
7902
8867
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
7903
|
-
if (layer === "ui") return (0,
|
|
7904
|
-
if (layer === "api") return (0,
|
|
7905
|
-
if (layer === "db") return (0,
|
|
8868
|
+
if (layer === "ui") return (0, import_node_path12.join)(rootDir, "src", nodeId);
|
|
8869
|
+
if (layer === "api") return (0, import_node_path12.join)(rootDir, nodeId);
|
|
8870
|
+
if (layer === "db") return (0, import_node_path12.join)(rootDir, "prisma", "schema.prisma");
|
|
7906
8871
|
return null;
|
|
7907
8872
|
}
|
|
7908
8873
|
function handleGrepNodes(args) {
|
|
@@ -7962,11 +8927,11 @@ function handleGrepNodes(args) {
|
|
|
7962
8927
|
let filesSearched = 0;
|
|
7963
8928
|
let truncated = false;
|
|
7964
8929
|
for (const [filePath, nodeId] of filePaths) {
|
|
7965
|
-
if (!(0,
|
|
8930
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) continue;
|
|
7966
8931
|
filesSearched++;
|
|
7967
8932
|
let content;
|
|
7968
8933
|
try {
|
|
7969
|
-
content = (0,
|
|
8934
|
+
content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
7970
8935
|
} catch {
|
|
7971
8936
|
continue;
|
|
7972
8937
|
}
|
|
@@ -8003,6 +8968,127 @@ function handleGrepNodes(args) {
|
|
|
8003
8968
|
truncated
|
|
8004
8969
|
});
|
|
8005
8970
|
}
|
|
8971
|
+
function handleChartServerStatus() {
|
|
8972
|
+
const lock = getLiveLock();
|
|
8973
|
+
if (!lock) {
|
|
8974
|
+
return okJson({ running: false });
|
|
8975
|
+
}
|
|
8976
|
+
return okJson({
|
|
8977
|
+
running: true,
|
|
8978
|
+
url: lock.url,
|
|
8979
|
+
port: lock.port,
|
|
8980
|
+
pid: lock.pid,
|
|
8981
|
+
cwd: lock.cwd,
|
|
8982
|
+
startedAt: lock.startedAt
|
|
8983
|
+
});
|
|
8984
|
+
}
|
|
8985
|
+
function handleStartChartServer(args) {
|
|
8986
|
+
const lock = getLiveLock();
|
|
8987
|
+
if (lock) {
|
|
8988
|
+
return okJson({
|
|
8989
|
+
started: false,
|
|
8990
|
+
reason: "already_running",
|
|
8991
|
+
url: lock.url,
|
|
8992
|
+
port: lock.port,
|
|
8993
|
+
pid: lock.pid
|
|
8994
|
+
});
|
|
8995
|
+
}
|
|
8996
|
+
const entryPath = process.argv[1];
|
|
8997
|
+
const logDir = (0, import_node_path12.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
8998
|
+
(0, import_node_fs11.mkdirSync)(logDir, { recursive: true });
|
|
8999
|
+
const logPath = (0, import_node_path12.join)(logDir, "launch-chart.log");
|
|
9000
|
+
const out = (0, import_node_fs11.openSync)(logPath, "a");
|
|
9001
|
+
const err2 = (0, import_node_fs11.openSync)(logPath, "a");
|
|
9002
|
+
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
9003
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
9004
|
+
detached: true,
|
|
9005
|
+
stdio: ["ignore", out, err2],
|
|
9006
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
9007
|
+
});
|
|
9008
|
+
child.unref();
|
|
9009
|
+
return okJson({
|
|
9010
|
+
started: true,
|
|
9011
|
+
pid: child.pid,
|
|
9012
|
+
logPath
|
|
9013
|
+
});
|
|
9014
|
+
}
|
|
9015
|
+
function handleStopChartServer() {
|
|
9016
|
+
const lock = getLiveLock();
|
|
9017
|
+
if (!lock) {
|
|
9018
|
+
return okJson({ stopped: false, reason: "not_running" });
|
|
9019
|
+
}
|
|
9020
|
+
try {
|
|
9021
|
+
process.kill(lock.pid, "SIGTERM");
|
|
9022
|
+
return okJson({ stopped: true, pid: lock.pid });
|
|
9023
|
+
} catch (e) {
|
|
9024
|
+
const code = e.code;
|
|
9025
|
+
if (code === "ESRCH") {
|
|
9026
|
+
clearLock();
|
|
9027
|
+
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
9028
|
+
}
|
|
9029
|
+
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
function handleDetectProjectStack() {
|
|
9033
|
+
const rootDir = findProjectRoot(process.cwd());
|
|
9034
|
+
const parsers = [
|
|
9035
|
+
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
9036
|
+
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
9037
|
+
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
9038
|
+
];
|
|
9039
|
+
const config = loadConfig(rootDir);
|
|
9040
|
+
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
9041
|
+
const uiGraph = readGraph(rootDir, "ui");
|
|
9042
|
+
if (uiGraph) {
|
|
9043
|
+
for (const ref of uiGraph.cross_refs ?? []) {
|
|
9044
|
+
if (ref.type === "calls_api") stats.calls_api++;
|
|
9045
|
+
if (ref.type === "references_api") stats.references_api++;
|
|
9046
|
+
}
|
|
9047
|
+
for (const f of uiGraph.flagged_edges ?? []) {
|
|
9048
|
+
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
9049
|
+
}
|
|
9050
|
+
}
|
|
9051
|
+
const srcDir = (0, import_node_path12.join)(rootDir, "src");
|
|
9052
|
+
if ((0, import_node_fs11.existsSync)(srcDir)) {
|
|
9053
|
+
const scanDir = (dir) => {
|
|
9054
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return;
|
|
9055
|
+
for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
|
|
9056
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
9057
|
+
const full = (0, import_node_path12.join)(dir, entry.name);
|
|
9058
|
+
if (entry.isDirectory()) {
|
|
9059
|
+
scanDir(full);
|
|
9060
|
+
continue;
|
|
9061
|
+
}
|
|
9062
|
+
if (![".ts", ".tsx"].includes((0, import_node_path12.extname)(entry.name))) continue;
|
|
9063
|
+
try {
|
|
9064
|
+
const content = (0, import_node_fs11.readFileSync)(full, "utf-8");
|
|
9065
|
+
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
9066
|
+
if (matches) stats.annotations += matches.length;
|
|
9067
|
+
} catch {
|
|
9068
|
+
}
|
|
9069
|
+
}
|
|
9070
|
+
};
|
|
9071
|
+
scanDir(srcDir);
|
|
9072
|
+
}
|
|
9073
|
+
let recommendedPrimary = "fetch-resolver";
|
|
9074
|
+
if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
|
|
9075
|
+
recommendedPrimary = "api-annotations";
|
|
9076
|
+
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
9077
|
+
recommendedPrimary = "url-literal-scanner";
|
|
9078
|
+
}
|
|
9079
|
+
return okJson({
|
|
9080
|
+
parsers,
|
|
9081
|
+
crosslayer_parsers: [
|
|
9082
|
+
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
9083
|
+
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
9084
|
+
{ id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
|
|
9085
|
+
],
|
|
9086
|
+
stats,
|
|
9087
|
+
recommended_primary: recommendedPrimary,
|
|
9088
|
+
current_config: Object.keys(config).length > 0 ? config : null,
|
|
9089
|
+
config_path: ".launchchart.json"
|
|
9090
|
+
});
|
|
9091
|
+
}
|
|
8006
9092
|
function send(msg) {
|
|
8007
9093
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
8008
9094
|
}
|
|
@@ -8046,6 +9132,22 @@ function handleMessage(msg) {
|
|
|
8046
9132
|
respond(id ?? null, handleGrepNodes(args));
|
|
8047
9133
|
return;
|
|
8048
9134
|
}
|
|
9135
|
+
if (toolName === "chart_server_status") {
|
|
9136
|
+
respond(id ?? null, handleChartServerStatus());
|
|
9137
|
+
return;
|
|
9138
|
+
}
|
|
9139
|
+
if (toolName === "start_chart_server") {
|
|
9140
|
+
respond(id ?? null, handleStartChartServer(args));
|
|
9141
|
+
return;
|
|
9142
|
+
}
|
|
9143
|
+
if (toolName === "stop_chart_server") {
|
|
9144
|
+
respond(id ?? null, handleStopChartServer());
|
|
9145
|
+
return;
|
|
9146
|
+
}
|
|
9147
|
+
if (toolName === "detect_project_stack") {
|
|
9148
|
+
respond(id ?? null, handleDetectProjectStack());
|
|
9149
|
+
return;
|
|
9150
|
+
}
|
|
8049
9151
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
8050
9152
|
return;
|
|
8051
9153
|
}
|
|
@@ -8143,7 +9245,7 @@ function parseArgs() {
|
|
|
8143
9245
|
return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
|
|
8144
9246
|
}
|
|
8145
9247
|
function tryListen(server, port, maxRetries = 10) {
|
|
8146
|
-
return new Promise((
|
|
9248
|
+
return new Promise((resolve2, reject) => {
|
|
8147
9249
|
let attempts = 0;
|
|
8148
9250
|
function attempt(p) {
|
|
8149
9251
|
server.once("error", (err2) => {
|
|
@@ -8154,7 +9256,7 @@ function tryListen(server, port, maxRetries = 10) {
|
|
|
8154
9256
|
reject(err2);
|
|
8155
9257
|
}
|
|
8156
9258
|
});
|
|
8157
|
-
server.listen(p, () =>
|
|
9259
|
+
server.listen(p, () => resolve2(p));
|
|
8158
9260
|
}
|
|
8159
9261
|
attempt(port);
|
|
8160
9262
|
});
|
|
@@ -8175,7 +9277,7 @@ function saveCredentials(creds) {
|
|
|
8175
9277
|
});
|
|
8176
9278
|
}
|
|
8177
9279
|
function verifyToken(serverUrl, token) {
|
|
8178
|
-
return new Promise((
|
|
9280
|
+
return new Promise((resolve2) => {
|
|
8179
9281
|
const url = new URL("/api/mcp/verify", serverUrl);
|
|
8180
9282
|
const body = JSON.stringify({ token });
|
|
8181
9283
|
const mod = url.protocol === "https:" ? import_https.default : import_http.default;
|
|
@@ -8190,30 +9292,30 @@ function verifyToken(serverUrl, token) {
|
|
|
8190
9292
|
res.on("data", (chunk) => data += chunk);
|
|
8191
9293
|
res.on("end", () => {
|
|
8192
9294
|
try {
|
|
8193
|
-
|
|
9295
|
+
resolve2(JSON.parse(data));
|
|
8194
9296
|
} catch {
|
|
8195
|
-
|
|
9297
|
+
resolve2({ valid: false, error: "Invalid response from server" });
|
|
8196
9298
|
}
|
|
8197
9299
|
});
|
|
8198
9300
|
});
|
|
8199
9301
|
req.on("error", (err2) => {
|
|
8200
|
-
|
|
9302
|
+
resolve2({ valid: false, error: `Cannot reach server: ${err2.message}` });
|
|
8201
9303
|
});
|
|
8202
9304
|
req.setTimeout(1e4, () => {
|
|
8203
9305
|
req.destroy();
|
|
8204
|
-
|
|
9306
|
+
resolve2({ valid: false, error: "Connection timed out" });
|
|
8205
9307
|
});
|
|
8206
9308
|
req.write(body);
|
|
8207
9309
|
req.end();
|
|
8208
9310
|
});
|
|
8209
9311
|
}
|
|
8210
9312
|
function httpRequest(reqUrl, options, body, timeout = 3e4) {
|
|
8211
|
-
return new Promise((
|
|
9313
|
+
return new Promise((resolve2, reject) => {
|
|
8212
9314
|
const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
|
|
8213
9315
|
const r = mod.request(reqUrl, options, (resp) => {
|
|
8214
9316
|
let data = "";
|
|
8215
9317
|
resp.on("data", (chunk) => data += chunk);
|
|
8216
|
-
resp.on("end", () =>
|
|
9318
|
+
resp.on("end", () => resolve2({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
|
|
8217
9319
|
});
|
|
8218
9320
|
r.on("error", reject);
|
|
8219
9321
|
r.setTimeout(timeout, () => {
|