@launchsecure/launch-kit 0.0.4 → 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-BPR5akxH.js → index-BUih0oqR.js} +99 -64
- package/dist/chart-client/assets/index-DFslt72L.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCxRNp8I.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +796 -275
- package/dist/server/cli.js +939 -298
- package/dist/server/graph-mcp-entry.js +1028 -305
- package/package.json +1 -1
- package/dist/chart-client/assets/index-DhNl1aFF.css +0 -1
- package/dist/client/assets/index-nR-HgoHH.css +0 -32
- /package/dist/client/assets/{index-D9e81rsq.js → index-DCC--GO-.js} +0 -0
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) {
|
|
@@ -6344,8 +6365,8 @@ function getTs() {
|
|
|
6344
6365
|
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
6345
6366
|
function parseFile(absPath) {
|
|
6346
6367
|
const ts = getTs();
|
|
6347
|
-
const content = (0,
|
|
6348
|
-
const ext = (0,
|
|
6368
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6369
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6349
6370
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
6350
6371
|
const sourceFile = ts.createSourceFile(
|
|
6351
6372
|
absPath,
|
|
@@ -6623,8 +6644,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
|
6623
6644
|
]);
|
|
6624
6645
|
function extractDbCalls(absPath) {
|
|
6625
6646
|
const ts = getTs();
|
|
6626
|
-
const content = (0,
|
|
6627
|
-
const ext = (0,
|
|
6647
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6648
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6628
6649
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
6629
6650
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
6630
6651
|
const calls = [];
|
|
@@ -6653,8 +6674,8 @@ function extractDbCalls(absPath) {
|
|
|
6653
6674
|
}
|
|
6654
6675
|
function extractAuthWrappers(absPath) {
|
|
6655
6676
|
const ts = getTs();
|
|
6656
|
-
const content = (0,
|
|
6657
|
-
const ext = (0,
|
|
6677
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6678
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6658
6679
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
6659
6680
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
6660
6681
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -6675,12 +6696,12 @@ function extractAuthWrappers(absPath) {
|
|
|
6675
6696
|
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
6676
6697
|
function walk(dir, exts) {
|
|
6677
6698
|
const results = [];
|
|
6678
|
-
if (!(0,
|
|
6679
|
-
for (const entry of (0,
|
|
6680
|
-
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);
|
|
6681
6702
|
if (entry.isDirectory()) {
|
|
6682
6703
|
results.push(...walk(full, exts));
|
|
6683
|
-
} else if (exts.includes((0,
|
|
6704
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
6684
6705
|
results.push(full);
|
|
6685
6706
|
}
|
|
6686
6707
|
}
|
|
@@ -6688,33 +6709,33 @@ function walk(dir, exts) {
|
|
|
6688
6709
|
}
|
|
6689
6710
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
6690
6711
|
const results = [];
|
|
6691
|
-
if (!(0,
|
|
6692
|
-
for (const entry of (0,
|
|
6712
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
6713
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
6693
6714
|
if (entry.isDirectory()) {
|
|
6694
6715
|
if (ignoreDirs.has(entry.name)) continue;
|
|
6695
|
-
results.push(...walkWithIgnore((0,
|
|
6696
|
-
} else if (exts.includes((0,
|
|
6697
|
-
results.push((0,
|
|
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));
|
|
6698
6719
|
}
|
|
6699
6720
|
}
|
|
6700
6721
|
return results;
|
|
6701
6722
|
}
|
|
6702
6723
|
function toNodeId(srcDir, absPath) {
|
|
6703
|
-
return (0,
|
|
6724
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
6704
6725
|
}
|
|
6705
6726
|
function resolveImport(srcDir, specifier) {
|
|
6706
6727
|
if (!specifier.startsWith("@/")) return null;
|
|
6707
6728
|
const rel = specifier.slice(2);
|
|
6708
|
-
const base = (0,
|
|
6709
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
6710
|
-
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;
|
|
6711
6732
|
}
|
|
6712
6733
|
return null;
|
|
6713
6734
|
}
|
|
6714
6735
|
function resolveRelativeImport(fromFile, specifier) {
|
|
6715
|
-
const base = (0,
|
|
6716
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
6717
|
-
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;
|
|
6718
6739
|
}
|
|
6719
6740
|
return null;
|
|
6720
6741
|
}
|
|
@@ -6735,7 +6756,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
6735
6756
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
6736
6757
|
if (!resolved) continue;
|
|
6737
6758
|
if (re.isWildcard) {
|
|
6738
|
-
const targetBn = (0,
|
|
6759
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
6739
6760
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
6740
6761
|
if (targetIsBarrel) {
|
|
6741
6762
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -6762,12 +6783,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
6762
6783
|
const barrels = /* @__PURE__ */ new Map();
|
|
6763
6784
|
const memo = /* @__PURE__ */ new Map();
|
|
6764
6785
|
for (const [absPath, parsed] of parsedByPath) {
|
|
6765
|
-
const bn = (0,
|
|
6786
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
6766
6787
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
6767
6788
|
if (parsed.reExports.length === 0) continue;
|
|
6768
6789
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
6769
6790
|
if (map.size > 0) {
|
|
6770
|
-
const barrelId = (0,
|
|
6791
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
6771
6792
|
barrels.set(barrelId, map);
|
|
6772
6793
|
}
|
|
6773
6794
|
}
|
|
@@ -6826,7 +6847,7 @@ function extractRoute(id) {
|
|
|
6826
6847
|
return route || "/";
|
|
6827
6848
|
}
|
|
6828
6849
|
function nameFromFilename(absPath) {
|
|
6829
|
-
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());
|
|
6830
6851
|
}
|
|
6831
6852
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
6832
6853
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -6907,105 +6928,6 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
6907
6928
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
6908
6929
|
return null;
|
|
6909
6930
|
}
|
|
6910
|
-
function loadApiRoutes(rootDir) {
|
|
6911
|
-
const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
6912
|
-
if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
|
|
6913
|
-
try {
|
|
6914
|
-
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
|
|
6915
|
-
const routes = [];
|
|
6916
|
-
for (const n of parsed.nodes ?? []) {
|
|
6917
|
-
const path9 = n.path;
|
|
6918
|
-
if (!path9 || typeof path9 !== "string") continue;
|
|
6919
|
-
routes.push({
|
|
6920
|
-
path: path9,
|
|
6921
|
-
nodeId: n.id,
|
|
6922
|
-
segments: path9.split("/").filter(Boolean)
|
|
6923
|
-
});
|
|
6924
|
-
}
|
|
6925
|
-
return routes;
|
|
6926
|
-
} catch {
|
|
6927
|
-
return [];
|
|
6928
|
-
}
|
|
6929
|
-
}
|
|
6930
|
-
function buildApiPathMap(routes) {
|
|
6931
|
-
const map = /* @__PURE__ */ new Map();
|
|
6932
|
-
for (const r of routes) {
|
|
6933
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
6934
|
-
}
|
|
6935
|
-
return map;
|
|
6936
|
-
}
|
|
6937
|
-
function normalizeFetchUrl(raw) {
|
|
6938
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
6939
|
-
const qIdx = s.indexOf("?");
|
|
6940
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
6941
|
-
const hIdx = s.indexOf("#");
|
|
6942
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
6943
|
-
let hadInterpolation = false;
|
|
6944
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
6945
|
-
hadInterpolation = true;
|
|
6946
|
-
const cleaned = expr.trim();
|
|
6947
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
6948
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
6949
|
-
return ":" + name;
|
|
6950
|
-
});
|
|
6951
|
-
s = s.replace(/\/+/g, "/");
|
|
6952
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
6953
|
-
return { path: s || "/", hadInterpolation };
|
|
6954
|
-
}
|
|
6955
|
-
function scoreApiRouteMatch(candidate, known) {
|
|
6956
|
-
if (candidate.length !== known.length) return -1;
|
|
6957
|
-
let score = 0;
|
|
6958
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
6959
|
-
const a = candidate[i];
|
|
6960
|
-
const b = known[i];
|
|
6961
|
-
if (a === b) {
|
|
6962
|
-
score += 3;
|
|
6963
|
-
continue;
|
|
6964
|
-
}
|
|
6965
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
6966
|
-
score += 2;
|
|
6967
|
-
continue;
|
|
6968
|
-
}
|
|
6969
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
6970
|
-
score += 1;
|
|
6971
|
-
continue;
|
|
6972
|
-
}
|
|
6973
|
-
return -1;
|
|
6974
|
-
}
|
|
6975
|
-
return score;
|
|
6976
|
-
}
|
|
6977
|
-
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
6978
|
-
const raw = call.url;
|
|
6979
|
-
if (/^(https?:)?\/\//i.test(raw)) {
|
|
6980
|
-
return { kind: "external", normalizedUrl: raw };
|
|
6981
|
-
}
|
|
6982
|
-
if (call.isConcat) {
|
|
6983
|
-
return { kind: "dynamic", normalizedUrl: raw };
|
|
6984
|
-
}
|
|
6985
|
-
const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
|
|
6986
|
-
if (!path9.startsWith("/")) {
|
|
6987
|
-
return { kind: "unresolved", normalizedUrl: path9 };
|
|
6988
|
-
}
|
|
6989
|
-
const segs = path9.split("/").filter(Boolean);
|
|
6990
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
6991
|
-
return { kind: "dynamic", normalizedUrl: path9 };
|
|
6992
|
-
}
|
|
6993
|
-
const exact = apiPathMap.get(path9);
|
|
6994
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
|
|
6995
|
-
let bestScore = -1;
|
|
6996
|
-
let bestId = null;
|
|
6997
|
-
for (const r of apiRoutes) {
|
|
6998
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
6999
|
-
if (score > bestScore) {
|
|
7000
|
-
bestScore = score;
|
|
7001
|
-
bestId = r.nodeId;
|
|
7002
|
-
}
|
|
7003
|
-
}
|
|
7004
|
-
if (bestId && bestScore > 0) {
|
|
7005
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
|
|
7006
|
-
}
|
|
7007
|
-
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7008
|
-
}
|
|
7009
6931
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
7010
6932
|
const edges = [];
|
|
7011
6933
|
const flagged = [];
|
|
@@ -7105,26 +7027,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
7105
7027
|
return { edges, flagged };
|
|
7106
7028
|
}
|
|
7107
7029
|
function detect(rootDir) {
|
|
7108
|
-
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"));
|
|
7109
7031
|
}
|
|
7110
7032
|
function generate(rootDir) {
|
|
7111
|
-
const srcDir = (0,
|
|
7112
|
-
const appFiles = walk((0,
|
|
7113
|
-
(f) => (0,
|
|
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"
|
|
7114
7036
|
);
|
|
7115
|
-
const clientFiles = walk((0,
|
|
7116
|
-
const serverFiles = walk((0,
|
|
7117
|
-
(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"
|
|
7118
7040
|
);
|
|
7119
|
-
const libFiles = walk((0,
|
|
7120
|
-
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"]);
|
|
7121
7043
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
7122
7044
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
7123
7045
|
for (const absPath of allDiscovered) {
|
|
7124
7046
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
7125
7047
|
}
|
|
7126
7048
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
7127
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
7049
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
7128
7050
|
const nodes = [];
|
|
7129
7051
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
7130
7052
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -7143,7 +7065,6 @@ function generate(rootDir) {
|
|
|
7143
7065
|
}
|
|
7144
7066
|
const allEdges = [];
|
|
7145
7067
|
const allFlagged = [];
|
|
7146
|
-
const crossRefs = [];
|
|
7147
7068
|
for (const absPath of fileSet) {
|
|
7148
7069
|
const sourceId = toNodeId(srcDir, absPath);
|
|
7149
7070
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -7160,66 +7081,21 @@ function generate(rootDir) {
|
|
|
7160
7081
|
allEdges.push(...edges);
|
|
7161
7082
|
allFlagged.push(...flagged);
|
|
7162
7083
|
}
|
|
7163
|
-
const
|
|
7164
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7165
|
-
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
7166
|
-
const fetchSeen = /* @__PURE__ */ new Set();
|
|
7167
|
-
let fetchResolvedCount = 0;
|
|
7168
|
-
let fetchDynamicCount = 0;
|
|
7169
|
-
let fetchUnresolvedCount = 0;
|
|
7170
|
-
let fetchExternalCount = 0;
|
|
7084
|
+
const fetchCallEntries = [];
|
|
7171
7085
|
for (const absPath of fileSet) {
|
|
7172
7086
|
const sourceId = toNodeId(srcDir, absPath);
|
|
7173
7087
|
const parsed = parsedByPath.get(absPath);
|
|
7174
7088
|
if (parsed.fetchCalls.length === 0) continue;
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
type: "calls_api",
|
|
7186
|
-
layer: "api"
|
|
7187
|
-
});
|
|
7188
|
-
fetchResolvedCount++;
|
|
7189
|
-
continue;
|
|
7190
|
-
}
|
|
7191
|
-
if (result.kind === "dynamic") {
|
|
7192
|
-
fetchDynamicCount++;
|
|
7193
|
-
allFlagged.push({
|
|
7194
|
-
source: sourceId,
|
|
7195
|
-
target: "DYNAMIC",
|
|
7196
|
-
type: "calls_api",
|
|
7197
|
-
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
7198
|
-
confidence: call.isConcat ? "low" : "medium"
|
|
7199
|
-
});
|
|
7200
|
-
continue;
|
|
7201
|
-
}
|
|
7202
|
-
if (result.kind === "external") {
|
|
7203
|
-
fetchExternalCount++;
|
|
7204
|
-
if (!includeExternalFetches) continue;
|
|
7205
|
-
allFlagged.push({
|
|
7206
|
-
source: sourceId,
|
|
7207
|
-
target: "EXTERNAL",
|
|
7208
|
-
type: "calls_external",
|
|
7209
|
-
label: `${methodTag} external fetch: ${call.url}`,
|
|
7210
|
-
confidence: "high"
|
|
7211
|
-
});
|
|
7212
|
-
continue;
|
|
7213
|
-
}
|
|
7214
|
-
fetchUnresolvedCount++;
|
|
7215
|
-
allFlagged.push({
|
|
7216
|
-
source: sourceId,
|
|
7217
|
-
target: "UNRESOLVED",
|
|
7218
|
-
type: "calls_api",
|
|
7219
|
-
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
7220
|
-
confidence: "medium"
|
|
7221
|
-
});
|
|
7222
|
-
}
|
|
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
|
+
});
|
|
7223
7099
|
}
|
|
7224
7100
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
7225
7101
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -7245,7 +7121,7 @@ function generate(rootDir) {
|
|
|
7245
7121
|
} catch {
|
|
7246
7122
|
continue;
|
|
7247
7123
|
}
|
|
7248
|
-
const externalId = (0,
|
|
7124
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7249
7125
|
const edgesFromThis = [];
|
|
7250
7126
|
const seen = /* @__PURE__ */ new Set();
|
|
7251
7127
|
for (const imp of parsed.imports) {
|
|
@@ -7336,20 +7212,11 @@ function generate(rootDir) {
|
|
|
7336
7212
|
layer: "ui",
|
|
7337
7213
|
parser: "react-nextjs-ast",
|
|
7338
7214
|
...stats,
|
|
7339
|
-
api_call_detection: {
|
|
7340
|
-
includeExternalFetches,
|
|
7341
|
-
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
7342
|
-
apiRoutesLoaded: apiRoutes.length,
|
|
7343
|
-
resolved: fetchResolvedCount,
|
|
7344
|
-
dynamic: fetchDynamicCount,
|
|
7345
|
-
unresolved: fetchUnresolvedCount,
|
|
7346
|
-
external: fetchExternalCount
|
|
7347
|
-
},
|
|
7348
7215
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
7349
7216
|
},
|
|
7350
7217
|
nodes,
|
|
7351
7218
|
edges: allEdges,
|
|
7352
|
-
cross_refs:
|
|
7219
|
+
cross_refs: [],
|
|
7353
7220
|
contradictions: [],
|
|
7354
7221
|
warnings: [],
|
|
7355
7222
|
flagged_edges: dedupedFlagged,
|
|
@@ -7360,7 +7227,8 @@ function generate(rootDir) {
|
|
|
7360
7227
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
7361
7228
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
7362
7229
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
7363
|
-
}
|
|
7230
|
+
},
|
|
7231
|
+
fetch_calls: fetchCallEntries
|
|
7364
7232
|
}
|
|
7365
7233
|
};
|
|
7366
7234
|
}
|
|
@@ -7372,14 +7240,14 @@ var reactNextjsParser = {
|
|
|
7372
7240
|
};
|
|
7373
7241
|
|
|
7374
7242
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
7375
|
-
var
|
|
7376
|
-
var
|
|
7243
|
+
var import_node_fs4 = require("node:fs");
|
|
7244
|
+
var import_node_path4 = require("node:path");
|
|
7377
7245
|
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
7378
7246
|
function walk2(dir) {
|
|
7379
7247
|
const results = [];
|
|
7380
|
-
if (!(0,
|
|
7381
|
-
for (const entry of (0,
|
|
7382
|
-
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);
|
|
7383
7251
|
if (entry.isDirectory()) {
|
|
7384
7252
|
results.push(...walk2(full));
|
|
7385
7253
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -7389,7 +7257,7 @@ function walk2(dir) {
|
|
|
7389
7257
|
return results;
|
|
7390
7258
|
}
|
|
7391
7259
|
function filePathToRoute(apiDir, absPath) {
|
|
7392
|
-
let route = "/" + (0,
|
|
7260
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
7393
7261
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
7394
7262
|
route = route.replace(/\/+/g, "/");
|
|
7395
7263
|
if (route === "/") return "/api";
|
|
@@ -7400,10 +7268,10 @@ function camelToPascal(s) {
|
|
|
7400
7268
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
7401
7269
|
}
|
|
7402
7270
|
function detect2(rootDir) {
|
|
7403
|
-
return (0,
|
|
7271
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
7404
7272
|
}
|
|
7405
7273
|
function generate2(rootDir) {
|
|
7406
|
-
const apiDir = (0,
|
|
7274
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
7407
7275
|
const routeFiles = walk2(apiDir);
|
|
7408
7276
|
const nodes = [];
|
|
7409
7277
|
const edges = [];
|
|
@@ -7421,7 +7289,7 @@ function generate2(rootDir) {
|
|
|
7421
7289
|
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
7422
7290
|
}
|
|
7423
7291
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
7424
|
-
const relPath = (0,
|
|
7292
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7425
7293
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
7426
7294
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
7427
7295
|
const mutates = mutations.length > 0;
|
|
@@ -7506,8 +7374,8 @@ var nextjsRoutesParser = {
|
|
|
7506
7374
|
};
|
|
7507
7375
|
|
|
7508
7376
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
7509
|
-
var
|
|
7510
|
-
var
|
|
7377
|
+
var import_node_fs5 = require("node:fs");
|
|
7378
|
+
var import_node_path5 = require("node:path");
|
|
7511
7379
|
function parseModels(content) {
|
|
7512
7380
|
const nodes = [];
|
|
7513
7381
|
const relations = [];
|
|
@@ -7598,11 +7466,11 @@ function parseEnums(content) {
|
|
|
7598
7466
|
return nodes;
|
|
7599
7467
|
}
|
|
7600
7468
|
function detect3(rootDir) {
|
|
7601
|
-
return (0,
|
|
7469
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
7602
7470
|
}
|
|
7603
7471
|
function generate3(rootDir) {
|
|
7604
|
-
const schemaPath = (0,
|
|
7605
|
-
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");
|
|
7606
7474
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
7607
7475
|
const enumNodes = parseEnums(content);
|
|
7608
7476
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -7658,33 +7526,651 @@ var prismaSchemaParser = {
|
|
|
7658
7526
|
generate: generate3
|
|
7659
7527
|
};
|
|
7660
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
|
+
|
|
7661
8096
|
// src/server/graph/core/graph-builder.ts
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
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
|
+
}
|
|
7669
8105
|
}
|
|
7670
8106
|
function generateLayer(rootDir, layer) {
|
|
7671
|
-
const
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
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
|
+
}
|
|
7675
8131
|
return {
|
|
7676
8132
|
layer,
|
|
7677
|
-
output,
|
|
7678
|
-
nodeCount:
|
|
7679
|
-
edgeCount:
|
|
8133
|
+
output: merged,
|
|
8134
|
+
nodeCount: merged.nodes.length,
|
|
8135
|
+
edgeCount: merged.edges.length
|
|
7680
8136
|
};
|
|
7681
8137
|
}
|
|
7682
8138
|
function generateAll(rootDir) {
|
|
7683
|
-
const
|
|
8139
|
+
const config = loadConfig(rootDir);
|
|
8140
|
+
const registry = createRegistry(config, rootDir);
|
|
8141
|
+
const layerOrder = ["api", "db", "ui"];
|
|
8142
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
7684
8143
|
const results = [];
|
|
7685
|
-
for (const layer of
|
|
7686
|
-
const
|
|
7687
|
-
|
|
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
|
+
});
|
|
8160
|
+
}
|
|
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
|
+
}
|
|
7688
8174
|
}
|
|
7689
8175
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
7690
8176
|
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
@@ -7695,23 +8181,23 @@ var GRAPHS_DIR = ".launchsecure/graphs";
|
|
|
7695
8181
|
var LAYERS = ["ui", "api", "db"];
|
|
7696
8182
|
var graphCache = /* @__PURE__ */ new Map();
|
|
7697
8183
|
function graphsDir(rootDir) {
|
|
7698
|
-
return (0,
|
|
8184
|
+
return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
|
|
7699
8185
|
}
|
|
7700
8186
|
function graphFilePath(rootDir, layer) {
|
|
7701
|
-
return (0,
|
|
8187
|
+
return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
|
|
7702
8188
|
}
|
|
7703
8189
|
function invalidateCache(filePath) {
|
|
7704
8190
|
graphCache.delete(filePath);
|
|
7705
8191
|
}
|
|
7706
8192
|
function readGraph(rootDir, layer) {
|
|
7707
8193
|
const filePath = graphFilePath(rootDir, layer);
|
|
7708
|
-
if (!(0,
|
|
7709
|
-
const stat = (0,
|
|
8194
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
8195
|
+
const stat = (0, import_node_fs9.statSync)(filePath);
|
|
7710
8196
|
const cached = graphCache.get(filePath);
|
|
7711
8197
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
7712
8198
|
return cached.graph;
|
|
7713
8199
|
}
|
|
7714
|
-
const content = (0,
|
|
8200
|
+
const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
|
|
7715
8201
|
const graph = JSON.parse(content);
|
|
7716
8202
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
7717
8203
|
return graph;
|
|
@@ -7726,11 +8212,11 @@ function readAllGraphs(rootDir) {
|
|
|
7726
8212
|
}
|
|
7727
8213
|
function generateGraph(rootDir, layer) {
|
|
7728
8214
|
const dir = graphsDir(rootDir);
|
|
7729
|
-
(0,
|
|
8215
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
7730
8216
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
7731
8217
|
for (const result of results) {
|
|
7732
8218
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
7733
|
-
(0,
|
|
8219
|
+
(0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
7734
8220
|
invalidateCache(filePath);
|
|
7735
8221
|
}
|
|
7736
8222
|
return results;
|
|
@@ -7792,25 +8278,27 @@ function handleGraphCommand(subcommand, args) {
|
|
|
7792
8278
|
}
|
|
7793
8279
|
|
|
7794
8280
|
// src/server/graph-mcp.ts
|
|
7795
|
-
var
|
|
7796
|
-
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");
|
|
7797
8285
|
|
|
7798
8286
|
// src/server/lockfile.ts
|
|
7799
8287
|
var import_node_child_process = require("node:child_process");
|
|
7800
|
-
var
|
|
8288
|
+
var import_node_fs10 = require("node:fs");
|
|
7801
8289
|
var import_node_os = require("node:os");
|
|
7802
|
-
var
|
|
8290
|
+
var import_node_path11 = require("node:path");
|
|
7803
8291
|
function lockDir() {
|
|
7804
|
-
return (0,
|
|
8292
|
+
return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
7805
8293
|
}
|
|
7806
8294
|
function lockPath() {
|
|
7807
|
-
return (0,
|
|
8295
|
+
return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
|
|
7808
8296
|
}
|
|
7809
8297
|
function readLock() {
|
|
7810
8298
|
const p = lockPath();
|
|
7811
|
-
if (!(0,
|
|
8299
|
+
if (!(0, import_node_fs10.existsSync)(p)) return null;
|
|
7812
8300
|
try {
|
|
7813
|
-
const data = JSON.parse((0,
|
|
8301
|
+
const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
|
|
7814
8302
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
7815
8303
|
return data;
|
|
7816
8304
|
} catch {
|
|
@@ -7846,13 +8334,19 @@ function getLiveLock() {
|
|
|
7846
8334
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
7847
8335
|
if (!live) {
|
|
7848
8336
|
try {
|
|
7849
|
-
(0,
|
|
8337
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
7850
8338
|
} catch {
|
|
7851
8339
|
}
|
|
7852
8340
|
return null;
|
|
7853
8341
|
}
|
|
7854
8342
|
return lock;
|
|
7855
8343
|
}
|
|
8344
|
+
function clearLock() {
|
|
8345
|
+
try {
|
|
8346
|
+
(0, import_node_fs10.unlinkSync)(lockPath());
|
|
8347
|
+
} catch {
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
7856
8350
|
|
|
7857
8351
|
// src/server/graph-mcp.ts
|
|
7858
8352
|
var SERVER_INFO = {
|
|
@@ -7982,8 +8476,39 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
7982
8476
|
}
|
|
7983
8477
|
},
|
|
7984
8478
|
{
|
|
7985
|
-
name: "
|
|
7986
|
-
description:
|
|
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.",
|
|
7987
8512
|
inputSchema: {
|
|
7988
8513
|
type: "object",
|
|
7989
8514
|
properties: {}
|
|
@@ -8340,9 +8865,9 @@ function handleReadGraph(args) {
|
|
|
8340
8865
|
return okJson(result);
|
|
8341
8866
|
}
|
|
8342
8867
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
8343
|
-
if (layer === "ui") return (0,
|
|
8344
|
-
if (layer === "api") return (0,
|
|
8345
|
-
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");
|
|
8346
8871
|
return null;
|
|
8347
8872
|
}
|
|
8348
8873
|
function handleGrepNodes(args) {
|
|
@@ -8402,11 +8927,11 @@ function handleGrepNodes(args) {
|
|
|
8402
8927
|
let filesSearched = 0;
|
|
8403
8928
|
let truncated = false;
|
|
8404
8929
|
for (const [filePath, nodeId] of filePaths) {
|
|
8405
|
-
if (!(0,
|
|
8930
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) continue;
|
|
8406
8931
|
filesSearched++;
|
|
8407
8932
|
let content;
|
|
8408
8933
|
try {
|
|
8409
|
-
content = (0,
|
|
8934
|
+
content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
8410
8935
|
} catch {
|
|
8411
8936
|
continue;
|
|
8412
8937
|
}
|
|
@@ -8443,13 +8968,10 @@ function handleGrepNodes(args) {
|
|
|
8443
8968
|
truncated
|
|
8444
8969
|
});
|
|
8445
8970
|
}
|
|
8446
|
-
function
|
|
8971
|
+
function handleChartServerStatus() {
|
|
8447
8972
|
const lock = getLiveLock();
|
|
8448
8973
|
if (!lock) {
|
|
8449
|
-
return okJson({
|
|
8450
|
-
running: false,
|
|
8451
|
-
hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
|
|
8452
|
-
});
|
|
8974
|
+
return okJson({ running: false });
|
|
8453
8975
|
}
|
|
8454
8976
|
return okJson({
|
|
8455
8977
|
running: true,
|
|
@@ -8460,6 +8982,113 @@ function handleGetGraphUiUrl() {
|
|
|
8460
8982
|
startedAt: lock.startedAt
|
|
8461
8983
|
});
|
|
8462
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
|
+
}
|
|
8463
9092
|
function send(msg) {
|
|
8464
9093
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
8465
9094
|
}
|
|
@@ -8503,8 +9132,20 @@ function handleMessage(msg) {
|
|
|
8503
9132
|
respond(id ?? null, handleGrepNodes(args));
|
|
8504
9133
|
return;
|
|
8505
9134
|
}
|
|
8506
|
-
if (toolName === "
|
|
8507
|
-
respond(id ?? null,
|
|
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());
|
|
8508
9149
|
return;
|
|
8509
9150
|
}
|
|
8510
9151
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
@@ -8604,7 +9245,7 @@ function parseArgs() {
|
|
|
8604
9245
|
return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
|
|
8605
9246
|
}
|
|
8606
9247
|
function tryListen(server, port, maxRetries = 10) {
|
|
8607
|
-
return new Promise((
|
|
9248
|
+
return new Promise((resolve2, reject) => {
|
|
8608
9249
|
let attempts = 0;
|
|
8609
9250
|
function attempt(p) {
|
|
8610
9251
|
server.once("error", (err2) => {
|
|
@@ -8615,7 +9256,7 @@ function tryListen(server, port, maxRetries = 10) {
|
|
|
8615
9256
|
reject(err2);
|
|
8616
9257
|
}
|
|
8617
9258
|
});
|
|
8618
|
-
server.listen(p, () =>
|
|
9259
|
+
server.listen(p, () => resolve2(p));
|
|
8619
9260
|
}
|
|
8620
9261
|
attempt(port);
|
|
8621
9262
|
});
|
|
@@ -8636,7 +9277,7 @@ function saveCredentials(creds) {
|
|
|
8636
9277
|
});
|
|
8637
9278
|
}
|
|
8638
9279
|
function verifyToken(serverUrl, token) {
|
|
8639
|
-
return new Promise((
|
|
9280
|
+
return new Promise((resolve2) => {
|
|
8640
9281
|
const url = new URL("/api/mcp/verify", serverUrl);
|
|
8641
9282
|
const body = JSON.stringify({ token });
|
|
8642
9283
|
const mod = url.protocol === "https:" ? import_https.default : import_http.default;
|
|
@@ -8651,30 +9292,30 @@ function verifyToken(serverUrl, token) {
|
|
|
8651
9292
|
res.on("data", (chunk) => data += chunk);
|
|
8652
9293
|
res.on("end", () => {
|
|
8653
9294
|
try {
|
|
8654
|
-
|
|
9295
|
+
resolve2(JSON.parse(data));
|
|
8655
9296
|
} catch {
|
|
8656
|
-
|
|
9297
|
+
resolve2({ valid: false, error: "Invalid response from server" });
|
|
8657
9298
|
}
|
|
8658
9299
|
});
|
|
8659
9300
|
});
|
|
8660
9301
|
req.on("error", (err2) => {
|
|
8661
|
-
|
|
9302
|
+
resolve2({ valid: false, error: `Cannot reach server: ${err2.message}` });
|
|
8662
9303
|
});
|
|
8663
9304
|
req.setTimeout(1e4, () => {
|
|
8664
9305
|
req.destroy();
|
|
8665
|
-
|
|
9306
|
+
resolve2({ valid: false, error: "Connection timed out" });
|
|
8666
9307
|
});
|
|
8667
9308
|
req.write(body);
|
|
8668
9309
|
req.end();
|
|
8669
9310
|
});
|
|
8670
9311
|
}
|
|
8671
9312
|
function httpRequest(reqUrl, options, body, timeout = 3e4) {
|
|
8672
|
-
return new Promise((
|
|
9313
|
+
return new Promise((resolve2, reject) => {
|
|
8673
9314
|
const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
|
|
8674
9315
|
const r = mod.request(reqUrl, options, (resp) => {
|
|
8675
9316
|
let data = "";
|
|
8676
9317
|
resp.on("data", (chunk) => data += chunk);
|
|
8677
|
-
resp.on("end", () =>
|
|
9318
|
+
resp.on("end", () => resolve2({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
|
|
8678
9319
|
});
|
|
8679
9320
|
r.on("error", reject);
|
|
8680
9321
|
r.setTimeout(timeout, () => {
|