@launchsecure/launch-kit 0.0.4 → 0.0.6
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-BN_N_I08.js +379 -0
- package/dist/chart-client/assets/index-DJRXEWQm.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-CCpAvTkG.css +32 -0
- package/dist/client/assets/{index-D9e81rsq.js → index-DldfczJ1.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +1388 -377
- package/dist/server/cli.js +1522 -406
- package/dist/server/graph-mcp-entry.js +1741 -415
- package/package.json +1 -1
- package/dist/chart-client/assets/index-BPR5akxH.js +0 -323
- package/dist/chart-client/assets/index-DhNl1aFF.css +0 -1
- package/dist/client/assets/index-nR-HgoHH.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((resolve3) => {
|
|
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
|
+
resolve3(entries);
|
|
1789
1789
|
});
|
|
1790
1790
|
rl.on("error", (error) => {
|
|
1791
1791
|
console.error("Error reading file:", filePath, error);
|
|
1792
|
-
|
|
1792
|
+
resolve3(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((resolve3, 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
|
+
resolve3({ code: exitCode, signal });
|
|
3613
3613
|
} else {
|
|
3614
3614
|
reject(new Error(`Script exited with code ${exitCode}`));
|
|
3615
3615
|
}
|
|
@@ -3701,6 +3701,30 @@ var require_src = __commonJS({
|
|
|
3701
3701
|
}
|
|
3702
3702
|
});
|
|
3703
3703
|
|
|
3704
|
+
// src/server/graph/core/config.ts
|
|
3705
|
+
var config_exports = {};
|
|
3706
|
+
__export(config_exports, {
|
|
3707
|
+
loadConfig: () => loadConfig
|
|
3708
|
+
});
|
|
3709
|
+
function loadConfig(rootDir) {
|
|
3710
|
+
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
3711
|
+
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
3712
|
+
try {
|
|
3713
|
+
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
3714
|
+
} catch {
|
|
3715
|
+
return {};
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
var import_node_fs, import_node_path, CONFIG_FILENAME;
|
|
3719
|
+
var init_config = __esm({
|
|
3720
|
+
"src/server/graph/core/config.ts"() {
|
|
3721
|
+
"use strict";
|
|
3722
|
+
import_node_fs = require("node:fs");
|
|
3723
|
+
import_node_path = require("node:path");
|
|
3724
|
+
CONFIG_FILENAME = ".launchchart.json";
|
|
3725
|
+
}
|
|
3726
|
+
});
|
|
3727
|
+
|
|
3704
3728
|
// src/server/cli.ts
|
|
3705
3729
|
var import_http = __toESM(require("http"));
|
|
3706
3730
|
var import_https = __toESM(require("https"));
|
|
@@ -5270,7 +5294,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5270
5294
|
return 3001;
|
|
5271
5295
|
}
|
|
5272
5296
|
startDevServer(port, databaseUrl) {
|
|
5273
|
-
return new Promise((
|
|
5297
|
+
return new Promise((resolve3) => {
|
|
5274
5298
|
const env = { ...process.env, PORT: String(port), ...databaseUrl ? { DATABASE_URL: databaseUrl } : {} };
|
|
5275
5299
|
this.devProcess = (0, import_child_process3.spawn)("npm", ["run", "dev"], {
|
|
5276
5300
|
cwd: this.workingDir,
|
|
@@ -5282,7 +5306,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5282
5306
|
const timeout = setTimeout(() => {
|
|
5283
5307
|
if (!resolved) {
|
|
5284
5308
|
resolved = true;
|
|
5285
|
-
this.healthCheck(port).then(
|
|
5309
|
+
this.healthCheck(port).then(resolve3);
|
|
5286
5310
|
}
|
|
5287
5311
|
}, 15e3);
|
|
5288
5312
|
const onData = (data) => {
|
|
@@ -5291,7 +5315,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5291
5315
|
if (!resolved) {
|
|
5292
5316
|
resolved = true;
|
|
5293
5317
|
clearTimeout(timeout);
|
|
5294
|
-
|
|
5318
|
+
resolve3(true);
|
|
5295
5319
|
}
|
|
5296
5320
|
}
|
|
5297
5321
|
};
|
|
@@ -5302,7 +5326,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5302
5326
|
if (!resolved) {
|
|
5303
5327
|
resolved = true;
|
|
5304
5328
|
clearTimeout(timeout);
|
|
5305
|
-
|
|
5329
|
+
resolve3(false);
|
|
5306
5330
|
}
|
|
5307
5331
|
});
|
|
5308
5332
|
this.devProcess.unref();
|
|
@@ -6324,16 +6348,24 @@ ${links}
|
|
|
6324
6348
|
}
|
|
6325
6349
|
|
|
6326
6350
|
// src/server/graph/index.ts
|
|
6327
|
-
var
|
|
6328
|
-
var
|
|
6351
|
+
var import_node_fs11 = require("node:fs");
|
|
6352
|
+
var import_node_path13 = require("node:path");
|
|
6353
|
+
|
|
6354
|
+
// src/server/graph/core/graph-builder.ts
|
|
6355
|
+
var import_node_fs8 = require("node:fs");
|
|
6356
|
+
var import_node_path9 = require("node:path");
|
|
6357
|
+
init_config();
|
|
6358
|
+
|
|
6359
|
+
// src/server/graph/core/parser-registry.ts
|
|
6360
|
+
var import_node_path8 = require("node:path");
|
|
6329
6361
|
|
|
6330
6362
|
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
6331
|
-
var
|
|
6332
|
-
var
|
|
6363
|
+
var import_node_fs3 = require("node:fs");
|
|
6364
|
+
var import_node_path3 = require("node:path");
|
|
6333
6365
|
|
|
6334
6366
|
// src/server/graph/core/ast-helpers.ts
|
|
6335
|
-
var
|
|
6336
|
-
var
|
|
6367
|
+
var import_node_fs2 = require("node:fs");
|
|
6368
|
+
var import_node_path2 = require("node:path");
|
|
6337
6369
|
var tsModule;
|
|
6338
6370
|
function getTs() {
|
|
6339
6371
|
if (!tsModule) {
|
|
@@ -6344,8 +6376,8 @@ function getTs() {
|
|
|
6344
6376
|
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
6345
6377
|
function parseFile(absPath) {
|
|
6346
6378
|
const ts = getTs();
|
|
6347
|
-
const content = (0,
|
|
6348
|
-
const ext = (0,
|
|
6379
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6380
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6349
6381
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
6350
6382
|
const sourceFile = ts.createSourceFile(
|
|
6351
6383
|
absPath,
|
|
@@ -6623,8 +6655,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
|
6623
6655
|
]);
|
|
6624
6656
|
function extractDbCalls(absPath) {
|
|
6625
6657
|
const ts = getTs();
|
|
6626
|
-
const content = (0,
|
|
6627
|
-
const ext = (0,
|
|
6658
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6659
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6628
6660
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
6629
6661
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
6630
6662
|
const calls = [];
|
|
@@ -6653,8 +6685,8 @@ function extractDbCalls(absPath) {
|
|
|
6653
6685
|
}
|
|
6654
6686
|
function extractAuthWrappers(absPath) {
|
|
6655
6687
|
const ts = getTs();
|
|
6656
|
-
const content = (0,
|
|
6657
|
-
const ext = (0,
|
|
6688
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
6689
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
6658
6690
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
6659
6691
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
6660
6692
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -6675,12 +6707,12 @@ function extractAuthWrappers(absPath) {
|
|
|
6675
6707
|
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
6676
6708
|
function walk(dir, exts) {
|
|
6677
6709
|
const results = [];
|
|
6678
|
-
if (!(0,
|
|
6679
|
-
for (const entry of (0,
|
|
6680
|
-
const full = (0,
|
|
6710
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
6711
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
6712
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
6681
6713
|
if (entry.isDirectory()) {
|
|
6682
6714
|
results.push(...walk(full, exts));
|
|
6683
|
-
} else if (exts.includes((0,
|
|
6715
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
6684
6716
|
results.push(full);
|
|
6685
6717
|
}
|
|
6686
6718
|
}
|
|
@@ -6688,33 +6720,33 @@ function walk(dir, exts) {
|
|
|
6688
6720
|
}
|
|
6689
6721
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
6690
6722
|
const results = [];
|
|
6691
|
-
if (!(0,
|
|
6692
|
-
for (const entry of (0,
|
|
6723
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
6724
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
6693
6725
|
if (entry.isDirectory()) {
|
|
6694
6726
|
if (ignoreDirs.has(entry.name)) continue;
|
|
6695
|
-
results.push(...walkWithIgnore((0,
|
|
6696
|
-
} else if (exts.includes((0,
|
|
6697
|
-
results.push((0,
|
|
6727
|
+
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
|
|
6728
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
6729
|
+
results.push((0, import_node_path3.join)(dir, entry.name));
|
|
6698
6730
|
}
|
|
6699
6731
|
}
|
|
6700
6732
|
return results;
|
|
6701
6733
|
}
|
|
6702
6734
|
function toNodeId(srcDir, absPath) {
|
|
6703
|
-
return (0,
|
|
6735
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
6704
6736
|
}
|
|
6705
6737
|
function resolveImport(srcDir, specifier) {
|
|
6706
6738
|
if (!specifier.startsWith("@/")) return null;
|
|
6707
6739
|
const rel = specifier.slice(2);
|
|
6708
|
-
const base = (0,
|
|
6709
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
6710
|
-
if ((0,
|
|
6740
|
+
const base = (0, import_node_path3.join)(srcDir, rel);
|
|
6741
|
+
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")]) {
|
|
6742
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
6711
6743
|
}
|
|
6712
6744
|
return null;
|
|
6713
6745
|
}
|
|
6714
6746
|
function resolveRelativeImport(fromFile, specifier) {
|
|
6715
|
-
const base = (0,
|
|
6716
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
6717
|
-
if ((0,
|
|
6747
|
+
const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
|
|
6748
|
+
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")]) {
|
|
6749
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
6718
6750
|
}
|
|
6719
6751
|
return null;
|
|
6720
6752
|
}
|
|
@@ -6735,7 +6767,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
6735
6767
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
6736
6768
|
if (!resolved) continue;
|
|
6737
6769
|
if (re.isWildcard) {
|
|
6738
|
-
const targetBn = (0,
|
|
6770
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
6739
6771
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
6740
6772
|
if (targetIsBarrel) {
|
|
6741
6773
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -6762,12 +6794,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
6762
6794
|
const barrels = /* @__PURE__ */ new Map();
|
|
6763
6795
|
const memo = /* @__PURE__ */ new Map();
|
|
6764
6796
|
for (const [absPath, parsed] of parsedByPath) {
|
|
6765
|
-
const bn = (0,
|
|
6797
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
6766
6798
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
6767
6799
|
if (parsed.reExports.length === 0) continue;
|
|
6768
6800
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
6769
6801
|
if (map.size > 0) {
|
|
6770
|
-
const barrelId = (0,
|
|
6802
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
6771
6803
|
barrels.set(barrelId, map);
|
|
6772
6804
|
}
|
|
6773
6805
|
}
|
|
@@ -6788,34 +6820,6 @@ function classifyType(id) {
|
|
|
6788
6820
|
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
6789
6821
|
return "component";
|
|
6790
6822
|
}
|
|
6791
|
-
function classifyModule(id) {
|
|
6792
|
-
if (/app\/\(auth\)\//.test(id)) return "auth";
|
|
6793
|
-
if (/app\/\(admin\)\//.test(id)) return "admin";
|
|
6794
|
-
if (/app\/\(settings\)\//.test(id)) return "settings";
|
|
6795
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
|
|
6796
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
|
|
6797
|
-
if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
|
|
6798
|
-
if (id.startsWith("app/integrations/")) return "integrations";
|
|
6799
|
-
if (id.startsWith("app/docs/")) return "admin";
|
|
6800
|
-
if (id.startsWith("client/components/ui/")) return "shared-ui";
|
|
6801
|
-
if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
|
|
6802
|
-
if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
|
|
6803
|
-
if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
|
|
6804
|
-
if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
|
|
6805
|
-
if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
|
|
6806
|
-
if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
|
|
6807
|
-
if (id.startsWith("server/auth/")) return "auth";
|
|
6808
|
-
if (id.startsWith("server/mcp/")) return "mcp";
|
|
6809
|
-
if (id.startsWith("server/lib/")) return "server-lib";
|
|
6810
|
-
if (id.startsWith("server/middleware")) return "middleware";
|
|
6811
|
-
if (id.startsWith("server/services/")) return "services";
|
|
6812
|
-
if (id.startsWith("server/db")) return "db";
|
|
6813
|
-
if (id.startsWith("server/errors")) return "errors";
|
|
6814
|
-
if (id.startsWith("server/")) return "server-lib";
|
|
6815
|
-
if (id.startsWith("config/")) return "config";
|
|
6816
|
-
if (id.startsWith("lib/")) return "lib";
|
|
6817
|
-
return "root";
|
|
6818
|
-
}
|
|
6819
6823
|
function extractRoute(id) {
|
|
6820
6824
|
if (!id.endsWith("/page.tsx")) return null;
|
|
6821
6825
|
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
@@ -6826,7 +6830,7 @@ function extractRoute(id) {
|
|
|
6826
6830
|
return route || "/";
|
|
6827
6831
|
}
|
|
6828
6832
|
function nameFromFilename(absPath) {
|
|
6829
|
-
return (0,
|
|
6833
|
+
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
6834
|
}
|
|
6831
6835
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
6832
6836
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -6907,105 +6911,6 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
6907
6911
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
6908
6912
|
return null;
|
|
6909
6913
|
}
|
|
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
6914
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
7010
6915
|
const edges = [];
|
|
7011
6916
|
const flagged = [];
|
|
@@ -7105,26 +7010,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
7105
7010
|
return { edges, flagged };
|
|
7106
7011
|
}
|
|
7107
7012
|
function detect(rootDir) {
|
|
7108
|
-
return (0,
|
|
7013
|
+
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
7014
|
}
|
|
7110
7015
|
function generate(rootDir) {
|
|
7111
|
-
const srcDir = (0,
|
|
7112
|
-
const appFiles = walk((0,
|
|
7113
|
-
(f) => (0,
|
|
7016
|
+
const srcDir = (0, import_node_path3.join)(rootDir, "src");
|
|
7017
|
+
const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
7018
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
7114
7019
|
);
|
|
7115
|
-
const clientFiles = walk((0,
|
|
7116
|
-
const serverFiles = walk((0,
|
|
7117
|
-
(f) => (0,
|
|
7020
|
+
const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
7021
|
+
const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
7022
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
7118
7023
|
);
|
|
7119
|
-
const libFiles = walk((0,
|
|
7120
|
-
const configFiles = walk((0,
|
|
7024
|
+
const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
7025
|
+
const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
7121
7026
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
7122
7027
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
7123
7028
|
for (const absPath of allDiscovered) {
|
|
7124
7029
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
7125
7030
|
}
|
|
7126
7031
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
7127
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
7032
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
7128
7033
|
const nodes = [];
|
|
7129
7034
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
7130
7035
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -7135,15 +7040,13 @@ function generate(rootDir) {
|
|
|
7135
7040
|
const parsed = parsedByPath.get(absPath);
|
|
7136
7041
|
const name = parsed.name || nameFromFilename(absPath);
|
|
7137
7042
|
const route = extractRoute(id);
|
|
7138
|
-
|
|
7139
|
-
nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
|
|
7043
|
+
nodes.push({ id, type, name, route, exports: parsed.exports });
|
|
7140
7044
|
nodeIdSet.add(id);
|
|
7141
7045
|
nodeTypeMap.set(id, type);
|
|
7142
7046
|
if (route) routeToNodeId.set(route, id);
|
|
7143
7047
|
}
|
|
7144
7048
|
const allEdges = [];
|
|
7145
7049
|
const allFlagged = [];
|
|
7146
|
-
const crossRefs = [];
|
|
7147
7050
|
for (const absPath of fileSet) {
|
|
7148
7051
|
const sourceId = toNodeId(srcDir, absPath);
|
|
7149
7052
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -7160,66 +7063,21 @@ function generate(rootDir) {
|
|
|
7160
7063
|
allEdges.push(...edges);
|
|
7161
7064
|
allFlagged.push(...flagged);
|
|
7162
7065
|
}
|
|
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;
|
|
7066
|
+
const fetchCallEntries = [];
|
|
7171
7067
|
for (const absPath of fileSet) {
|
|
7172
7068
|
const sourceId = toNodeId(srcDir, absPath);
|
|
7173
7069
|
const parsed = parsedByPath.get(absPath);
|
|
7174
7070
|
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
|
-
}
|
|
7071
|
+
fetchCallEntries.push({
|
|
7072
|
+
nodeId: sourceId,
|
|
7073
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
7074
|
+
url: c.url,
|
|
7075
|
+
method: c.method,
|
|
7076
|
+
isTemplate: c.isTemplate,
|
|
7077
|
+
isConcat: c.isConcat,
|
|
7078
|
+
kind: c.kind
|
|
7079
|
+
}))
|
|
7080
|
+
});
|
|
7223
7081
|
}
|
|
7224
7082
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
7225
7083
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -7245,7 +7103,7 @@ function generate(rootDir) {
|
|
|
7245
7103
|
} catch {
|
|
7246
7104
|
continue;
|
|
7247
7105
|
}
|
|
7248
|
-
const externalId = (0,
|
|
7106
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7249
7107
|
const edgesFromThis = [];
|
|
7250
7108
|
const seen = /* @__PURE__ */ new Set();
|
|
7251
7109
|
for (const imp of parsed.imports) {
|
|
@@ -7286,7 +7144,6 @@ function generate(rootDir) {
|
|
|
7286
7144
|
type: "external",
|
|
7287
7145
|
name: parsed.name || nameFromFilename(absPath),
|
|
7288
7146
|
route: null,
|
|
7289
|
-
module: "external",
|
|
7290
7147
|
exports: parsed.exports
|
|
7291
7148
|
});
|
|
7292
7149
|
nodeIdSet.add(externalId);
|
|
@@ -7336,20 +7193,11 @@ function generate(rootDir) {
|
|
|
7336
7193
|
layer: "ui",
|
|
7337
7194
|
parser: "react-nextjs-ast",
|
|
7338
7195
|
...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
7196
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
7349
7197
|
},
|
|
7350
7198
|
nodes,
|
|
7351
7199
|
edges: allEdges,
|
|
7352
|
-
cross_refs:
|
|
7200
|
+
cross_refs: [],
|
|
7353
7201
|
contradictions: [],
|
|
7354
7202
|
warnings: [],
|
|
7355
7203
|
flagged_edges: dedupedFlagged,
|
|
@@ -7360,7 +7208,8 @@ function generate(rootDir) {
|
|
|
7360
7208
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
7361
7209
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
7362
7210
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
7363
|
-
}
|
|
7211
|
+
},
|
|
7212
|
+
fetch_calls: fetchCallEntries
|
|
7364
7213
|
}
|
|
7365
7214
|
};
|
|
7366
7215
|
}
|
|
@@ -7372,14 +7221,14 @@ var reactNextjsParser = {
|
|
|
7372
7221
|
};
|
|
7373
7222
|
|
|
7374
7223
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
7375
|
-
var
|
|
7376
|
-
var
|
|
7224
|
+
var import_node_fs4 = require("node:fs");
|
|
7225
|
+
var import_node_path4 = require("node:path");
|
|
7377
7226
|
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
7378
7227
|
function walk2(dir) {
|
|
7379
7228
|
const results = [];
|
|
7380
|
-
if (!(0,
|
|
7381
|
-
for (const entry of (0,
|
|
7382
|
-
const full = (0,
|
|
7229
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
7230
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
7231
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
7383
7232
|
if (entry.isDirectory()) {
|
|
7384
7233
|
results.push(...walk2(full));
|
|
7385
7234
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -7389,7 +7238,7 @@ function walk2(dir) {
|
|
|
7389
7238
|
return results;
|
|
7390
7239
|
}
|
|
7391
7240
|
function filePathToRoute(apiDir, absPath) {
|
|
7392
|
-
let route = "/" + (0,
|
|
7241
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
7393
7242
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
7394
7243
|
route = route.replace(/\/+/g, "/");
|
|
7395
7244
|
if (route === "/") return "/api";
|
|
@@ -7400,10 +7249,10 @@ function camelToPascal(s) {
|
|
|
7400
7249
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
7401
7250
|
}
|
|
7402
7251
|
function detect2(rootDir) {
|
|
7403
|
-
return (0,
|
|
7252
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
7404
7253
|
}
|
|
7405
7254
|
function generate2(rootDir) {
|
|
7406
|
-
const apiDir = (0,
|
|
7255
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
7407
7256
|
const routeFiles = walk2(apiDir);
|
|
7408
7257
|
const nodes = [];
|
|
7409
7258
|
const edges = [];
|
|
@@ -7421,7 +7270,7 @@ function generate2(rootDir) {
|
|
|
7421
7270
|
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
7422
7271
|
}
|
|
7423
7272
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
7424
|
-
const relPath = (0,
|
|
7273
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7425
7274
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
7426
7275
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
7427
7276
|
const mutates = mutations.length > 0;
|
|
@@ -7506,8 +7355,8 @@ var nextjsRoutesParser = {
|
|
|
7506
7355
|
};
|
|
7507
7356
|
|
|
7508
7357
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
7509
|
-
var
|
|
7510
|
-
var
|
|
7358
|
+
var import_node_fs5 = require("node:fs");
|
|
7359
|
+
var import_node_path5 = require("node:path");
|
|
7511
7360
|
function parseModels(content) {
|
|
7512
7361
|
const nodes = [];
|
|
7513
7362
|
const relations = [];
|
|
@@ -7598,11 +7447,11 @@ function parseEnums(content) {
|
|
|
7598
7447
|
return nodes;
|
|
7599
7448
|
}
|
|
7600
7449
|
function detect3(rootDir) {
|
|
7601
|
-
return (0,
|
|
7450
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
7602
7451
|
}
|
|
7603
7452
|
function generate3(rootDir) {
|
|
7604
|
-
const schemaPath = (0,
|
|
7605
|
-
const content = (0,
|
|
7453
|
+
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
7454
|
+
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
7606
7455
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
7607
7456
|
const enumNodes = parseEnums(content);
|
|
7608
7457
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -7658,97 +7507,1088 @@ var prismaSchemaParser = {
|
|
|
7658
7507
|
generate: generate3
|
|
7659
7508
|
};
|
|
7660
7509
|
|
|
7661
|
-
// src/server/graph/core/
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
if (!parser) return null;
|
|
7673
|
-
if (!parser.detect(rootDir)) return null;
|
|
7674
|
-
const output = parser.generate(rootDir);
|
|
7675
|
-
return {
|
|
7676
|
-
layer,
|
|
7677
|
-
output,
|
|
7678
|
-
nodeCount: output.nodes.length,
|
|
7679
|
-
edgeCount: output.edges.length
|
|
7680
|
-
};
|
|
7681
|
-
}
|
|
7682
|
-
function generateAll(rootDir) {
|
|
7683
|
-
const layers = ["api", "db", "ui"];
|
|
7684
|
-
const results = [];
|
|
7685
|
-
for (const layer of layers) {
|
|
7686
|
-
const result = generateLayer(rootDir, layer);
|
|
7687
|
-
if (result) results.push(result);
|
|
7510
|
+
// src/server/graph/core/api-route-matching.ts
|
|
7511
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
7512
|
+
const routes = [];
|
|
7513
|
+
for (const n of apiOutput.nodes) {
|
|
7514
|
+
const path9 = n.path;
|
|
7515
|
+
if (!path9 || typeof path9 !== "string") continue;
|
|
7516
|
+
routes.push({
|
|
7517
|
+
path: path9,
|
|
7518
|
+
nodeId: n.id,
|
|
7519
|
+
segments: path9.split("/").filter(Boolean)
|
|
7520
|
+
});
|
|
7688
7521
|
}
|
|
7689
|
-
|
|
7690
|
-
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
7691
|
-
}
|
|
7692
|
-
|
|
7693
|
-
// src/server/graph/index.ts
|
|
7694
|
-
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
7695
|
-
var LAYERS = ["ui", "api", "db"];
|
|
7696
|
-
var graphCache = /* @__PURE__ */ new Map();
|
|
7697
|
-
function graphsDir(rootDir) {
|
|
7698
|
-
return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
|
|
7699
|
-
}
|
|
7700
|
-
function graphFilePath(rootDir, layer) {
|
|
7701
|
-
return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
|
|
7522
|
+
return routes;
|
|
7702
7523
|
}
|
|
7703
|
-
function
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
const filePath = graphFilePath(rootDir, layer);
|
|
7708
|
-
if (!(0, import_node_fs5.existsSync)(filePath)) return null;
|
|
7709
|
-
const stat = (0, import_node_fs5.statSync)(filePath);
|
|
7710
|
-
const cached = graphCache.get(filePath);
|
|
7711
|
-
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
7712
|
-
return cached.graph;
|
|
7524
|
+
function buildApiPathMap(routes) {
|
|
7525
|
+
const map = /* @__PURE__ */ new Map();
|
|
7526
|
+
for (const r of routes) {
|
|
7527
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
7713
7528
|
}
|
|
7714
|
-
|
|
7715
|
-
const graph = JSON.parse(content);
|
|
7716
|
-
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
7717
|
-
return graph;
|
|
7529
|
+
return map;
|
|
7718
7530
|
}
|
|
7719
|
-
function
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7531
|
+
function normalizeFetchUrl(raw) {
|
|
7532
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
7533
|
+
const qIdx = s.indexOf("?");
|
|
7534
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
7535
|
+
const hIdx = s.indexOf("#");
|
|
7536
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
7537
|
+
let hadInterpolation = false;
|
|
7538
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
7539
|
+
hadInterpolation = true;
|
|
7540
|
+
const cleaned = expr.trim();
|
|
7541
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
7542
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
7543
|
+
return ":" + name;
|
|
7544
|
+
});
|
|
7545
|
+
s = s.replace(/\/+/g, "/");
|
|
7546
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
7547
|
+
return { path: s || "/", hadInterpolation };
|
|
7726
7548
|
}
|
|
7727
|
-
function
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
const
|
|
7733
|
-
|
|
7734
|
-
|
|
7549
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
7550
|
+
if (candidate.length !== known.length) return -1;
|
|
7551
|
+
let score = 0;
|
|
7552
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
7553
|
+
const a = candidate[i];
|
|
7554
|
+
const b = known[i];
|
|
7555
|
+
if (a === b) {
|
|
7556
|
+
score += 3;
|
|
7557
|
+
continue;
|
|
7558
|
+
}
|
|
7559
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
7560
|
+
score += 2;
|
|
7561
|
+
continue;
|
|
7562
|
+
}
|
|
7563
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
7564
|
+
score += 1;
|
|
7565
|
+
continue;
|
|
7566
|
+
}
|
|
7567
|
+
return -1;
|
|
7735
7568
|
}
|
|
7736
|
-
return
|
|
7569
|
+
return score;
|
|
7737
7570
|
}
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
const idx = args.indexOf("--layer");
|
|
7743
|
-
if (idx < 0 || idx + 1 >= args.length) return void 0;
|
|
7744
|
-
const value = args[idx + 1];
|
|
7745
|
-
if (!VALID_LAYERS.includes(value)) {
|
|
7746
|
-
console.error(`Invalid layer "${value}". Must be one of: ${VALID_LAYERS.join(", ")}`);
|
|
7747
|
-
process.exit(1);
|
|
7571
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
7572
|
+
const raw = call.url;
|
|
7573
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
7574
|
+
return { kind: "external", normalizedUrl: raw };
|
|
7748
7575
|
}
|
|
7749
|
-
|
|
7750
|
-
}
|
|
7751
|
-
|
|
7576
|
+
if (call.isConcat) {
|
|
7577
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
7578
|
+
}
|
|
7579
|
+
const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
|
|
7580
|
+
if (!path9.startsWith("/")) {
|
|
7581
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7582
|
+
}
|
|
7583
|
+
const segs = path9.split("/").filter(Boolean);
|
|
7584
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
7585
|
+
return { kind: "dynamic", normalizedUrl: path9 };
|
|
7586
|
+
}
|
|
7587
|
+
const exact = apiPathMap.get(path9);
|
|
7588
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
|
|
7589
|
+
let bestScore = -1;
|
|
7590
|
+
let bestId = null;
|
|
7591
|
+
for (const r of apiRoutes) {
|
|
7592
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
7593
|
+
if (score > bestScore) {
|
|
7594
|
+
bestScore = score;
|
|
7595
|
+
bestId = r.nodeId;
|
|
7596
|
+
}
|
|
7597
|
+
}
|
|
7598
|
+
if (bestId && bestScore > 0) {
|
|
7599
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
|
|
7600
|
+
}
|
|
7601
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7602
|
+
}
|
|
7603
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
7604
|
+
const { path: path9, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
7605
|
+
if (!path9.startsWith("/")) {
|
|
7606
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7607
|
+
}
|
|
7608
|
+
const segs = path9.split("/").filter(Boolean);
|
|
7609
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
7610
|
+
return { kind: "dynamic", normalizedUrl: path9 };
|
|
7611
|
+
}
|
|
7612
|
+
const exact = apiPathMap.get(path9);
|
|
7613
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
|
|
7614
|
+
let bestScore = -1;
|
|
7615
|
+
let bestId = null;
|
|
7616
|
+
for (const r of apiRoutes) {
|
|
7617
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
7618
|
+
if (score > bestScore) {
|
|
7619
|
+
bestScore = score;
|
|
7620
|
+
bestId = r.nodeId;
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
if (bestId && bestScore > 0) {
|
|
7624
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
|
|
7625
|
+
}
|
|
7626
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7627
|
+
}
|
|
7628
|
+
|
|
7629
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
7630
|
+
var fetchResolverParser = {
|
|
7631
|
+
id: "fetch-resolver",
|
|
7632
|
+
layer: "crosslayer",
|
|
7633
|
+
detect(_rootDir) {
|
|
7634
|
+
return true;
|
|
7635
|
+
},
|
|
7636
|
+
generate(_rootDir, layerOutputs) {
|
|
7637
|
+
const uiOutput = layerOutputs.get("ui");
|
|
7638
|
+
const apiOutput = layerOutputs.get("api");
|
|
7639
|
+
if (!uiOutput || !apiOutput) {
|
|
7640
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7641
|
+
}
|
|
7642
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
7643
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7644
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
7645
|
+
if (fetchCallEntries.length === 0) {
|
|
7646
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7647
|
+
}
|
|
7648
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
7649
|
+
const crossRefs = [];
|
|
7650
|
+
const flaggedEdges = [];
|
|
7651
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7652
|
+
let resolvedCount = 0;
|
|
7653
|
+
let dynamicCount = 0;
|
|
7654
|
+
let unresolvedCount = 0;
|
|
7655
|
+
let externalCount = 0;
|
|
7656
|
+
for (const entry of fetchCallEntries) {
|
|
7657
|
+
for (const call of entry.calls) {
|
|
7658
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
7659
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
7660
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7661
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
7662
|
+
if (seen.has(key)) continue;
|
|
7663
|
+
seen.add(key);
|
|
7664
|
+
crossRefs.push({
|
|
7665
|
+
source: entry.nodeId,
|
|
7666
|
+
target: result.nodeId,
|
|
7667
|
+
type: "calls_api",
|
|
7668
|
+
layer: "api"
|
|
7669
|
+
});
|
|
7670
|
+
resolvedCount++;
|
|
7671
|
+
continue;
|
|
7672
|
+
}
|
|
7673
|
+
if (result.kind === "dynamic") {
|
|
7674
|
+
dynamicCount++;
|
|
7675
|
+
flaggedEdges.push({
|
|
7676
|
+
source: entry.nodeId,
|
|
7677
|
+
target: "DYNAMIC",
|
|
7678
|
+
type: "calls_api",
|
|
7679
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
7680
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
7681
|
+
});
|
|
7682
|
+
continue;
|
|
7683
|
+
}
|
|
7684
|
+
if (result.kind === "external") {
|
|
7685
|
+
externalCount++;
|
|
7686
|
+
if (!includeExternal) continue;
|
|
7687
|
+
flaggedEdges.push({
|
|
7688
|
+
source: entry.nodeId,
|
|
7689
|
+
target: "EXTERNAL",
|
|
7690
|
+
type: "calls_external",
|
|
7691
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
7692
|
+
confidence: "high"
|
|
7693
|
+
});
|
|
7694
|
+
continue;
|
|
7695
|
+
}
|
|
7696
|
+
unresolvedCount++;
|
|
7697
|
+
flaggedEdges.push({
|
|
7698
|
+
source: entry.nodeId,
|
|
7699
|
+
target: "UNRESOLVED",
|
|
7700
|
+
type: "calls_api",
|
|
7701
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
7702
|
+
confidence: "medium"
|
|
7703
|
+
});
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
return {
|
|
7707
|
+
cross_refs: crossRefs,
|
|
7708
|
+
flagged_edges: flaggedEdges,
|
|
7709
|
+
warnings: [],
|
|
7710
|
+
patterns: {
|
|
7711
|
+
api_call_detection: {
|
|
7712
|
+
resolved: resolvedCount,
|
|
7713
|
+
dynamic: dynamicCount,
|
|
7714
|
+
unresolved: unresolvedCount,
|
|
7715
|
+
external: externalCount
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
};
|
|
7719
|
+
}
|
|
7720
|
+
};
|
|
7721
|
+
|
|
7722
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
7723
|
+
var import_node_fs6 = require("node:fs");
|
|
7724
|
+
var import_node_path6 = require("node:path");
|
|
7725
|
+
var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
7726
|
+
function walk3(dir, exts) {
|
|
7727
|
+
if (!(0, import_node_fs6.existsSync)(dir)) return [];
|
|
7728
|
+
const results = [];
|
|
7729
|
+
for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
|
|
7730
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
7731
|
+
const full = (0, import_node_path6.join)(dir, entry.name);
|
|
7732
|
+
if (entry.isDirectory()) {
|
|
7733
|
+
results.push(...walk3(full, exts));
|
|
7734
|
+
} else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
|
|
7735
|
+
results.push(full);
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
return results;
|
|
7739
|
+
}
|
|
7740
|
+
function toNodeId2(srcDir, absPath) {
|
|
7741
|
+
return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
7742
|
+
}
|
|
7743
|
+
var apiAnnotationsParser = {
|
|
7744
|
+
id: "api-annotations",
|
|
7745
|
+
layer: "crosslayer",
|
|
7746
|
+
detect(rootDir) {
|
|
7747
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
|
|
7748
|
+
},
|
|
7749
|
+
generate(rootDir, layerOutputs) {
|
|
7750
|
+
const apiOutput = layerOutputs.get("api");
|
|
7751
|
+
if (!apiOutput) {
|
|
7752
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7753
|
+
}
|
|
7754
|
+
const uiOutput = layerOutputs.get("ui");
|
|
7755
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
7756
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
7757
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7758
|
+
const srcDir = (0, import_node_path6.join)(rootDir, "src");
|
|
7759
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
7760
|
+
const crossRefs = [];
|
|
7761
|
+
const flaggedEdges = [];
|
|
7762
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7763
|
+
for (const absPath of files) {
|
|
7764
|
+
const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
|
|
7765
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
7766
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
7767
|
+
let match;
|
|
7768
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
7769
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
7770
|
+
const method = match[1];
|
|
7771
|
+
const urlPath = match[2];
|
|
7772
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
7773
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7774
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
7775
|
+
if (seen.has(key)) continue;
|
|
7776
|
+
seen.add(key);
|
|
7777
|
+
crossRefs.push({
|
|
7778
|
+
source: sourceId,
|
|
7779
|
+
target: result.nodeId,
|
|
7780
|
+
type: "calls_api",
|
|
7781
|
+
layer: "api"
|
|
7782
|
+
});
|
|
7783
|
+
} else {
|
|
7784
|
+
flaggedEdges.push({
|
|
7785
|
+
source: sourceId,
|
|
7786
|
+
target: "UNRESOLVED",
|
|
7787
|
+
type: "annotation_unresolved",
|
|
7788
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
7789
|
+
confidence: "high"
|
|
7790
|
+
});
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
return {
|
|
7795
|
+
cross_refs: crossRefs,
|
|
7796
|
+
flagged_edges: flaggedEdges,
|
|
7797
|
+
warnings: [],
|
|
7798
|
+
patterns: {
|
|
7799
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
7800
|
+
annotations_resolved: crossRefs.length,
|
|
7801
|
+
annotations_unresolved: flaggedEdges.length
|
|
7802
|
+
}
|
|
7803
|
+
};
|
|
7804
|
+
}
|
|
7805
|
+
};
|
|
7806
|
+
|
|
7807
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
7808
|
+
var import_node_fs7 = require("node:fs");
|
|
7809
|
+
var import_node_path7 = require("node:path");
|
|
7810
|
+
var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
7811
|
+
function walk4(dir, exts) {
|
|
7812
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
7813
|
+
const results = [];
|
|
7814
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
7815
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
7816
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
7817
|
+
if (entry.isDirectory()) {
|
|
7818
|
+
results.push(...walk4(full, exts));
|
|
7819
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
7820
|
+
results.push(full);
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
return results;
|
|
7824
|
+
}
|
|
7825
|
+
function toNodeId3(srcDir, absPath) {
|
|
7826
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
7827
|
+
}
|
|
7828
|
+
var urlLiteralScannerParser = {
|
|
7829
|
+
id: "url-literal-scanner",
|
|
7830
|
+
layer: "crosslayer",
|
|
7831
|
+
detect(rootDir) {
|
|
7832
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
7833
|
+
},
|
|
7834
|
+
generate(rootDir, layerOutputs) {
|
|
7835
|
+
const apiOutput = layerOutputs.get("api");
|
|
7836
|
+
if (!apiOutput) {
|
|
7837
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
7838
|
+
}
|
|
7839
|
+
const uiOutput = layerOutputs.get("ui");
|
|
7840
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
7841
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
7842
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7843
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
7844
|
+
const clientDir = (0, import_node_path7.join)(srcDir, "client");
|
|
7845
|
+
const appDir = (0, import_node_path7.join)(srcDir, "app");
|
|
7846
|
+
const files = [
|
|
7847
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
7848
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
7849
|
+
];
|
|
7850
|
+
const crossRefs = [];
|
|
7851
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7852
|
+
for (const absPath of files) {
|
|
7853
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
7854
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
7855
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
7856
|
+
let match;
|
|
7857
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
7858
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
7859
|
+
const urlPath = match[1];
|
|
7860
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
7861
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7862
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
7863
|
+
if (seen.has(key)) continue;
|
|
7864
|
+
seen.add(key);
|
|
7865
|
+
crossRefs.push({
|
|
7866
|
+
source: sourceId,
|
|
7867
|
+
target: result.nodeId,
|
|
7868
|
+
type: "references_api",
|
|
7869
|
+
layer: "api"
|
|
7870
|
+
});
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7874
|
+
return {
|
|
7875
|
+
cross_refs: crossRefs,
|
|
7876
|
+
flagged_edges: [],
|
|
7877
|
+
warnings: [],
|
|
7878
|
+
patterns: {
|
|
7879
|
+
url_literals_resolved: crossRefs.length
|
|
7880
|
+
}
|
|
7881
|
+
};
|
|
7882
|
+
}
|
|
7883
|
+
};
|
|
7884
|
+
|
|
7885
|
+
// src/server/graph/core/parser-registry.ts
|
|
7886
|
+
var ParserRegistry = class {
|
|
7887
|
+
constructor() {
|
|
7888
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
7889
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
7890
|
+
}
|
|
7891
|
+
register(parser) {
|
|
7892
|
+
if (this.ids.has(parser.id)) {
|
|
7893
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
7894
|
+
}
|
|
7895
|
+
this.ids.add(parser.id);
|
|
7896
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
7897
|
+
list.push(parser);
|
|
7898
|
+
this.parsers.set(parser.layer, list);
|
|
7899
|
+
}
|
|
7900
|
+
getParsers(layer) {
|
|
7901
|
+
return this.parsers.get(layer) ?? [];
|
|
7902
|
+
}
|
|
7903
|
+
getCrossLayerParsers() {
|
|
7904
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
7905
|
+
}
|
|
7906
|
+
getAll() {
|
|
7907
|
+
const all = [];
|
|
7908
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
7909
|
+
return all;
|
|
7910
|
+
}
|
|
7911
|
+
};
|
|
7912
|
+
function registerBuiltins(registry, disabled) {
|
|
7913
|
+
const builtins = [
|
|
7914
|
+
reactNextjsParser,
|
|
7915
|
+
nextjsRoutesParser,
|
|
7916
|
+
prismaSchemaParser,
|
|
7917
|
+
fetchResolverParser,
|
|
7918
|
+
apiAnnotationsParser,
|
|
7919
|
+
urlLiteralScannerParser
|
|
7920
|
+
];
|
|
7921
|
+
for (const parser of builtins) {
|
|
7922
|
+
if (disabled.has(parser.id)) continue;
|
|
7923
|
+
registry.register(parser);
|
|
7924
|
+
}
|
|
7925
|
+
}
|
|
7926
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
7927
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
7928
|
+
try {
|
|
7929
|
+
const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
|
|
7930
|
+
const mod = require(absPath);
|
|
7931
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
7932
|
+
if (disabled.has(parser.id)) continue;
|
|
7933
|
+
if (parser.layer !== entry.layer) {
|
|
7934
|
+
process.stderr.write(
|
|
7935
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
7936
|
+
`
|
|
7937
|
+
);
|
|
7938
|
+
}
|
|
7939
|
+
registry.register(parser);
|
|
7940
|
+
} catch (err2) {
|
|
7941
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
7942
|
+
`);
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
function createRegistry(config, rootDir) {
|
|
7947
|
+
const registry = new ParserRegistry();
|
|
7948
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
7949
|
+
registerBuiltins(registry, disabled);
|
|
7950
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
7951
|
+
return registry;
|
|
7952
|
+
}
|
|
7953
|
+
|
|
7954
|
+
// src/server/graph/core/merge.ts
|
|
7955
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
7956
|
+
if (outputs.length === 0) {
|
|
7957
|
+
return {
|
|
7958
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
7959
|
+
nodes: [],
|
|
7960
|
+
edges: [],
|
|
7961
|
+
cross_refs: [],
|
|
7962
|
+
contradictions: [],
|
|
7963
|
+
warnings: [],
|
|
7964
|
+
flagged_edges: []
|
|
7965
|
+
};
|
|
7966
|
+
}
|
|
7967
|
+
if (outputs.length === 1) return outputs[0];
|
|
7968
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
7969
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
7970
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
7971
|
+
const mergedNodes = [];
|
|
7972
|
+
const mergedEdges = [];
|
|
7973
|
+
const mergedCrossRefs = [];
|
|
7974
|
+
const mergedContradictions = [];
|
|
7975
|
+
const mergedWarnings = [];
|
|
7976
|
+
const mergedFlagged = [];
|
|
7977
|
+
const parserIds = [];
|
|
7978
|
+
for (const output of outputs) {
|
|
7979
|
+
if (output.metadata.parser) {
|
|
7980
|
+
parserIds.push(String(output.metadata.parser));
|
|
7981
|
+
}
|
|
7982
|
+
for (const node of output.nodes) {
|
|
7983
|
+
if (seenNodes.has(node.id)) {
|
|
7984
|
+
mergedWarnings.push({
|
|
7985
|
+
type: "merge_conflict",
|
|
7986
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
7987
|
+
});
|
|
7988
|
+
continue;
|
|
7989
|
+
}
|
|
7990
|
+
seenNodes.add(node.id);
|
|
7991
|
+
mergedNodes.push(node);
|
|
7992
|
+
}
|
|
7993
|
+
for (const edge of output.edges) {
|
|
7994
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
7995
|
+
if (seenEdges.has(key)) continue;
|
|
7996
|
+
seenEdges.add(key);
|
|
7997
|
+
mergedEdges.push(edge);
|
|
7998
|
+
}
|
|
7999
|
+
for (const ref of output.cross_refs) {
|
|
8000
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
8001
|
+
if (seenCrossRefs.has(key)) continue;
|
|
8002
|
+
seenCrossRefs.add(key);
|
|
8003
|
+
mergedCrossRefs.push(ref);
|
|
8004
|
+
}
|
|
8005
|
+
mergedContradictions.push(...output.contradictions);
|
|
8006
|
+
mergedWarnings.push(...output.warnings);
|
|
8007
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
8008
|
+
}
|
|
8009
|
+
const metadata = {
|
|
8010
|
+
...outputs[0].metadata,
|
|
8011
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8012
|
+
parsers: parserIds
|
|
8013
|
+
};
|
|
8014
|
+
return {
|
|
8015
|
+
metadata,
|
|
8016
|
+
nodes: mergedNodes,
|
|
8017
|
+
edges: mergedEdges,
|
|
8018
|
+
cross_refs: mergedCrossRefs,
|
|
8019
|
+
contradictions: mergedContradictions,
|
|
8020
|
+
warnings: mergedWarnings,
|
|
8021
|
+
flagged_edges: mergedFlagged,
|
|
8022
|
+
patterns: outputs[0].patterns
|
|
8023
|
+
};
|
|
8024
|
+
}
|
|
8025
|
+
function dedupCrossRefs(refs) {
|
|
8026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8027
|
+
const result = [];
|
|
8028
|
+
for (const ref of refs) {
|
|
8029
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
8030
|
+
if (seen.has(key)) continue;
|
|
8031
|
+
seen.add(key);
|
|
8032
|
+
result.push(ref);
|
|
8033
|
+
}
|
|
8034
|
+
return result;
|
|
8035
|
+
}
|
|
8036
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
8037
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
8038
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
8039
|
+
const allWarnings = [...uiOutput.warnings];
|
|
8040
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
8041
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
8042
|
+
if (primaryResult) {
|
|
8043
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
8044
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
8045
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
8046
|
+
}
|
|
8047
|
+
const primarySet = new Set(
|
|
8048
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
8049
|
+
);
|
|
8050
|
+
for (const sec of secondaryResults) {
|
|
8051
|
+
for (const ref of sec.output.cross_refs) {
|
|
8052
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
8053
|
+
if (primarySet.has(key)) {
|
|
8054
|
+
allCrossRefs.push(ref);
|
|
8055
|
+
} else {
|
|
8056
|
+
allFlagged.push({
|
|
8057
|
+
source: ref.source,
|
|
8058
|
+
target: ref.target,
|
|
8059
|
+
type: "out_of_pattern",
|
|
8060
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
8061
|
+
confidence: "medium"
|
|
8062
|
+
});
|
|
8063
|
+
allCrossRefs.push(ref);
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
8067
|
+
allWarnings.push(...sec.output.warnings);
|
|
8068
|
+
}
|
|
8069
|
+
return {
|
|
8070
|
+
...uiOutput,
|
|
8071
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
8072
|
+
flagged_edges: allFlagged,
|
|
8073
|
+
warnings: allWarnings
|
|
8074
|
+
};
|
|
8075
|
+
}
|
|
8076
|
+
|
|
8077
|
+
// src/server/graph/core/graph-builder.ts
|
|
8078
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
8079
|
+
const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
8080
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) return null;
|
|
8081
|
+
try {
|
|
8082
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
|
|
8083
|
+
} catch {
|
|
8084
|
+
return null;
|
|
8085
|
+
}
|
|
8086
|
+
}
|
|
8087
|
+
function generateLayer(rootDir, layer) {
|
|
8088
|
+
const config = loadConfig(rootDir);
|
|
8089
|
+
const registry = createRegistry(config, rootDir);
|
|
8090
|
+
const parsers = registry.getParsers(layer);
|
|
8091
|
+
const outputs = [];
|
|
8092
|
+
for (const parser of parsers) {
|
|
8093
|
+
if (!parser.detect(rootDir)) continue;
|
|
8094
|
+
outputs.push(parser.generate(rootDir));
|
|
8095
|
+
}
|
|
8096
|
+
if (outputs.length === 0) return null;
|
|
8097
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
8098
|
+
if (layer === "ui") {
|
|
8099
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
8100
|
+
layerOutputs.set("ui", merged);
|
|
8101
|
+
for (const otherLayer of ["api", "db"]) {
|
|
8102
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
8103
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
8104
|
+
}
|
|
8105
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
8106
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
8107
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
8108
|
+
if (crossResults.length > 0) {
|
|
8109
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
return {
|
|
8113
|
+
layer,
|
|
8114
|
+
output: merged,
|
|
8115
|
+
nodeCount: merged.nodes.length,
|
|
8116
|
+
edgeCount: merged.edges.length
|
|
8117
|
+
};
|
|
8118
|
+
}
|
|
8119
|
+
function generateAll(rootDir) {
|
|
8120
|
+
const config = loadConfig(rootDir);
|
|
8121
|
+
const registry = createRegistry(config, rootDir);
|
|
8122
|
+
const layerOrder = ["api", "db", "ui"];
|
|
8123
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
8124
|
+
const results = [];
|
|
8125
|
+
for (const layer of layerOrder) {
|
|
8126
|
+
const parsers = registry.getParsers(layer);
|
|
8127
|
+
const outputs = [];
|
|
8128
|
+
for (const parser of parsers) {
|
|
8129
|
+
if (!parser.detect(rootDir)) continue;
|
|
8130
|
+
outputs.push(parser.generate(rootDir));
|
|
8131
|
+
}
|
|
8132
|
+
if (outputs.length === 0) continue;
|
|
8133
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
8134
|
+
layerOutputs.set(layer, merged);
|
|
8135
|
+
results.push({
|
|
8136
|
+
layer,
|
|
8137
|
+
output: merged,
|
|
8138
|
+
nodeCount: merged.nodes.length,
|
|
8139
|
+
edgeCount: merged.edges.length
|
|
8140
|
+
});
|
|
8141
|
+
}
|
|
8142
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
8143
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
8144
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
8145
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
8146
|
+
const uiOutput = layerOutputs.get("ui");
|
|
8147
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
8148
|
+
layerOutputs.set("ui", merged);
|
|
8149
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
8150
|
+
if (uiResult) {
|
|
8151
|
+
uiResult.output = merged;
|
|
8152
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
8153
|
+
uiResult.edgeCount = merged.edges.length;
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
8157
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
8158
|
+
}
|
|
8159
|
+
|
|
8160
|
+
// src/server/graph/index.ts
|
|
8161
|
+
init_config();
|
|
8162
|
+
|
|
8163
|
+
// src/server/graph/core/tagger-registry.ts
|
|
8164
|
+
var import_node_path11 = require("node:path");
|
|
8165
|
+
|
|
8166
|
+
// src/server/graph/taggers/module-tagger.ts
|
|
8167
|
+
var import_node_fs9 = require("node:fs");
|
|
8168
|
+
var import_node_path10 = require("node:path");
|
|
8169
|
+
function matchGlob(pattern, id) {
|
|
8170
|
+
const patParts = pattern.split("/");
|
|
8171
|
+
const idParts = id.split("/");
|
|
8172
|
+
return matchParts(patParts, 0, idParts, 0);
|
|
8173
|
+
}
|
|
8174
|
+
function matchParts(pat, pi, id, ii) {
|
|
8175
|
+
while (pi < pat.length && ii < id.length) {
|
|
8176
|
+
const p = pat[pi];
|
|
8177
|
+
if (p === "**") {
|
|
8178
|
+
for (let skip = ii; skip <= id.length; skip++) {
|
|
8179
|
+
if (matchParts(pat, pi + 1, id, skip)) return true;
|
|
8180
|
+
}
|
|
8181
|
+
return false;
|
|
8182
|
+
}
|
|
8183
|
+
if (p === "*") {
|
|
8184
|
+
pi++;
|
|
8185
|
+
ii++;
|
|
8186
|
+
continue;
|
|
8187
|
+
}
|
|
8188
|
+
if (p !== id[ii]) return false;
|
|
8189
|
+
pi++;
|
|
8190
|
+
ii++;
|
|
8191
|
+
}
|
|
8192
|
+
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
8193
|
+
return pi === pat.length && ii === id.length;
|
|
8194
|
+
}
|
|
8195
|
+
var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
|
|
8196
|
+
function detectConventionDirs(rootDir) {
|
|
8197
|
+
const result = /* @__PURE__ */ new Map();
|
|
8198
|
+
const searchDirs = [
|
|
8199
|
+
rootDir,
|
|
8200
|
+
(0, import_node_path10.join)(rootDir, "src"),
|
|
8201
|
+
(0, import_node_path10.join)(rootDir, "app"),
|
|
8202
|
+
(0, import_node_path10.join)(rootDir, "lib")
|
|
8203
|
+
];
|
|
8204
|
+
for (const base of searchDirs) {
|
|
8205
|
+
for (const convention of CONVENTION_DIRS) {
|
|
8206
|
+
const dir = (0, import_node_path10.join)(base, convention);
|
|
8207
|
+
if (!(0, import_node_fs9.existsSync)(dir)) continue;
|
|
8208
|
+
try {
|
|
8209
|
+
const stat = (0, import_node_fs9.statSync)(dir);
|
|
8210
|
+
if (!stat.isDirectory()) continue;
|
|
8211
|
+
const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
8212
|
+
if (entries.length > 0) {
|
|
8213
|
+
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
8214
|
+
result.set(relPath, entries);
|
|
8215
|
+
}
|
|
8216
|
+
} catch {
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
return result;
|
|
8221
|
+
}
|
|
8222
|
+
function extractRouteGroups(id) {
|
|
8223
|
+
const groups = [];
|
|
8224
|
+
const re = /\(([^)]+)\)/g;
|
|
8225
|
+
let m;
|
|
8226
|
+
while ((m = re.exec(id)) !== null) {
|
|
8227
|
+
groups.push(m[1]);
|
|
8228
|
+
}
|
|
8229
|
+
return groups;
|
|
8230
|
+
}
|
|
8231
|
+
var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
8232
|
+
"src",
|
|
8233
|
+
"app",
|
|
8234
|
+
"client",
|
|
8235
|
+
"server",
|
|
8236
|
+
"lib",
|
|
8237
|
+
"config"
|
|
8238
|
+
]);
|
|
8239
|
+
function isRouteGroup(segment) {
|
|
8240
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
8241
|
+
}
|
|
8242
|
+
function isDynamicSegment(segment) {
|
|
8243
|
+
return segment.startsWith("[") || segment.startsWith(":");
|
|
8244
|
+
}
|
|
8245
|
+
function isDomainDir(segment) {
|
|
8246
|
+
return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
|
|
8247
|
+
}
|
|
8248
|
+
var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
|
|
8249
|
+
"app",
|
|
8250
|
+
"all",
|
|
8251
|
+
"ee",
|
|
8252
|
+
"home",
|
|
8253
|
+
"root"
|
|
8254
|
+
]);
|
|
8255
|
+
function isTrivialGroup(name) {
|
|
8256
|
+
if (TRIVIAL_GROUPS.has(name)) return true;
|
|
8257
|
+
const lower = name.toLowerCase();
|
|
8258
|
+
const wrapperPatterns = [
|
|
8259
|
+
/^.*-?wrapper$/,
|
|
8260
|
+
// "page-wrapper", "use-page-wrapper"
|
|
8261
|
+
/^.*-?layout$/,
|
|
8262
|
+
// "admin-layout", "settings-layout"
|
|
8263
|
+
/^use-/,
|
|
8264
|
+
// "use-page-wrapper"
|
|
8265
|
+
/^default$/
|
|
8266
|
+
];
|
|
8267
|
+
return wrapperPatterns.some((p) => p.test(lower));
|
|
8268
|
+
}
|
|
8269
|
+
function normalizeGroupName(name) {
|
|
8270
|
+
return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
8271
|
+
}
|
|
8272
|
+
function extractModuleFromPath(id) {
|
|
8273
|
+
const segments = id.split("/");
|
|
8274
|
+
const routeGroups = extractRouteGroups(id);
|
|
8275
|
+
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
|
|
8276
|
+
if (moduleGroups.length > 0) {
|
|
8277
|
+
return moduleGroups[moduleGroups.length - 1];
|
|
8278
|
+
}
|
|
8279
|
+
const meaningful = [];
|
|
8280
|
+
for (const seg of segments) {
|
|
8281
|
+
if (seg.includes(".")) continue;
|
|
8282
|
+
if (isRouteGroup(seg)) continue;
|
|
8283
|
+
if (isDynamicSegment(seg)) continue;
|
|
8284
|
+
if (isDomainDir(seg)) continue;
|
|
8285
|
+
if (SKIP_SEGMENTS.has(seg)) continue;
|
|
8286
|
+
meaningful.push(seg);
|
|
8287
|
+
}
|
|
8288
|
+
if (meaningful.length > 0) {
|
|
8289
|
+
return meaningful[0];
|
|
8290
|
+
}
|
|
8291
|
+
return "root";
|
|
8292
|
+
}
|
|
8293
|
+
var cachedRootDir = null;
|
|
8294
|
+
var cachedConventionDirs = /* @__PURE__ */ new Map();
|
|
8295
|
+
var moduleTagger = {
|
|
8296
|
+
id: "module",
|
|
8297
|
+
tagKey: "module",
|
|
8298
|
+
trackUntagged: true,
|
|
8299
|
+
layers: null,
|
|
8300
|
+
// applies to all layers
|
|
8301
|
+
tag(nodes, layer, rootDir) {
|
|
8302
|
+
if (cachedRootDir !== rootDir) {
|
|
8303
|
+
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
8304
|
+
cachedRootDir = rootDir;
|
|
8305
|
+
}
|
|
8306
|
+
let configRules = [];
|
|
8307
|
+
try {
|
|
8308
|
+
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
8309
|
+
const config = loadConfig2(rootDir);
|
|
8310
|
+
configRules = config.taggers?.module?.rules ?? [];
|
|
8311
|
+
} catch {
|
|
8312
|
+
}
|
|
8313
|
+
const result = /* @__PURE__ */ new Map();
|
|
8314
|
+
for (const node of nodes) {
|
|
8315
|
+
const id = node.id;
|
|
8316
|
+
let matched = false;
|
|
8317
|
+
for (const rule of configRules) {
|
|
8318
|
+
if (matchGlob(rule.match, id)) {
|
|
8319
|
+
result.set(id, rule.module);
|
|
8320
|
+
matched = true;
|
|
8321
|
+
break;
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
if (matched) continue;
|
|
8325
|
+
matched = false;
|
|
8326
|
+
for (const [convDir, moduleNames] of cachedConventionDirs) {
|
|
8327
|
+
if (id.startsWith(convDir + "/")) {
|
|
8328
|
+
const rest = id.slice(convDir.length + 1);
|
|
8329
|
+
const firstSeg = rest.split("/")[0];
|
|
8330
|
+
if (moduleNames.includes(firstSeg)) {
|
|
8331
|
+
result.set(id, firstSeg);
|
|
8332
|
+
matched = true;
|
|
8333
|
+
break;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
if (matched) continue;
|
|
8338
|
+
const module2 = extractModuleFromPath(id);
|
|
8339
|
+
result.set(id, module2);
|
|
8340
|
+
}
|
|
8341
|
+
return result;
|
|
8342
|
+
}
|
|
8343
|
+
};
|
|
8344
|
+
|
|
8345
|
+
// src/server/graph/taggers/screen-tagger.ts
|
|
8346
|
+
var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
|
|
8347
|
+
var screenTagger = {
|
|
8348
|
+
id: "screen",
|
|
8349
|
+
tagKey: "screen",
|
|
8350
|
+
trackUntagged: true,
|
|
8351
|
+
layers: ["ui"],
|
|
8352
|
+
tag(nodes, layer) {
|
|
8353
|
+
if (layer !== "ui") return /* @__PURE__ */ new Map();
|
|
8354
|
+
const result = /* @__PURE__ */ new Map();
|
|
8355
|
+
for (const node of nodes) {
|
|
8356
|
+
if (SCREEN_TYPES.has(node.type)) {
|
|
8357
|
+
result.set(node.id, "true");
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
return result;
|
|
8361
|
+
}
|
|
8362
|
+
};
|
|
8363
|
+
|
|
8364
|
+
// src/server/graph/core/tagger-registry.ts
|
|
8365
|
+
var TaggerRegistry = class {
|
|
8366
|
+
constructor() {
|
|
8367
|
+
this.taggers = [];
|
|
8368
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
8369
|
+
}
|
|
8370
|
+
register(tagger) {
|
|
8371
|
+
if (this.ids.has(tagger.id)) {
|
|
8372
|
+
throw new Error(`Duplicate tagger id: ${tagger.id}`);
|
|
8373
|
+
}
|
|
8374
|
+
this.ids.add(tagger.id);
|
|
8375
|
+
this.taggers.push(tagger);
|
|
8376
|
+
}
|
|
8377
|
+
getAll() {
|
|
8378
|
+
return this.taggers;
|
|
8379
|
+
}
|
|
8380
|
+
getForLayer(layer) {
|
|
8381
|
+
return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
|
|
8382
|
+
}
|
|
8383
|
+
};
|
|
8384
|
+
var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
|
|
8385
|
+
function registerBuiltins2(registry, disabled, config) {
|
|
8386
|
+
for (const tagger of BUILTIN_TAGGERS) {
|
|
8387
|
+
if (disabled.has(tagger.id)) continue;
|
|
8388
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
8389
|
+
if (override !== void 0) {
|
|
8390
|
+
tagger.trackUntagged = override;
|
|
8391
|
+
}
|
|
8392
|
+
registry.register(tagger);
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
8396
|
+
for (const entry of config.taggers?.custom ?? []) {
|
|
8397
|
+
if (disabled.has(entry.id)) continue;
|
|
8398
|
+
try {
|
|
8399
|
+
const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
|
|
8400
|
+
const mod = require(absPath);
|
|
8401
|
+
const tagger = "default" in mod ? mod.default : mod;
|
|
8402
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
8403
|
+
if (override !== void 0) {
|
|
8404
|
+
tagger.trackUntagged = override;
|
|
8405
|
+
}
|
|
8406
|
+
registry.register(tagger);
|
|
8407
|
+
} catch (err2) {
|
|
8408
|
+
process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
|
|
8409
|
+
`);
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
function createTaggerRegistry(config, rootDir) {
|
|
8414
|
+
const registry = new TaggerRegistry();
|
|
8415
|
+
const disabled = new Set(config.taggers?.disabled ?? []);
|
|
8416
|
+
registerBuiltins2(registry, disabled, config);
|
|
8417
|
+
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
8418
|
+
return registry;
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
// src/server/graph/core/tag-store.ts
|
|
8422
|
+
var import_node_fs10 = require("node:fs");
|
|
8423
|
+
var import_node_path12 = require("node:path");
|
|
8424
|
+
var TAGS_FILENAME = "tags.json";
|
|
8425
|
+
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
8426
|
+
var tagCache = /* @__PURE__ */ new Map();
|
|
8427
|
+
function tagsFilePath(rootDir) {
|
|
8428
|
+
return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
8429
|
+
}
|
|
8430
|
+
function readTagStore(rootDir) {
|
|
8431
|
+
const filePath = tagsFilePath(rootDir);
|
|
8432
|
+
if (!(0, import_node_fs10.existsSync)(filePath)) return {};
|
|
8433
|
+
const stat = (0, import_node_fs10.statSync)(filePath);
|
|
8434
|
+
const cached = tagCache.get(filePath);
|
|
8435
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
8436
|
+
return cached.store;
|
|
8437
|
+
}
|
|
8438
|
+
try {
|
|
8439
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
8440
|
+
const store = JSON.parse(content);
|
|
8441
|
+
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
8442
|
+
return store;
|
|
8443
|
+
} catch {
|
|
8444
|
+
return {};
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
8447
|
+
function writeTagStore(rootDir, store) {
|
|
8448
|
+
const filePath = tagsFilePath(rootDir);
|
|
8449
|
+
const dir = (0, import_node_path12.dirname)(filePath);
|
|
8450
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
8451
|
+
const cleaned = {};
|
|
8452
|
+
for (const [nodeId, tags] of Object.entries(store)) {
|
|
8453
|
+
if (Object.keys(tags).length > 0) {
|
|
8454
|
+
cleaned[nodeId] = tags;
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
(0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
8458
|
+
tagCache.delete(filePath);
|
|
8459
|
+
}
|
|
8460
|
+
function setTag(rootDir, nodeId, key, value) {
|
|
8461
|
+
const store = readTagStore(rootDir);
|
|
8462
|
+
if (!store[nodeId]) store[nodeId] = {};
|
|
8463
|
+
store[nodeId][key] = value;
|
|
8464
|
+
writeTagStore(rootDir, store);
|
|
8465
|
+
}
|
|
8466
|
+
function removeTag(rootDir, nodeId, key) {
|
|
8467
|
+
const store = readTagStore(rootDir);
|
|
8468
|
+
if (!store[nodeId]) return;
|
|
8469
|
+
delete store[nodeId][key];
|
|
8470
|
+
if (Object.keys(store[nodeId]).length === 0) {
|
|
8471
|
+
delete store[nodeId];
|
|
8472
|
+
}
|
|
8473
|
+
writeTagStore(rootDir, store);
|
|
8474
|
+
}
|
|
8475
|
+
|
|
8476
|
+
// src/server/graph/index.ts
|
|
8477
|
+
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
8478
|
+
var LAYERS = ["ui", "api", "db"];
|
|
8479
|
+
var graphCache = /* @__PURE__ */ new Map();
|
|
8480
|
+
var taggedCache = /* @__PURE__ */ new Map();
|
|
8481
|
+
function graphsDir(rootDir) {
|
|
8482
|
+
return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
|
|
8483
|
+
}
|
|
8484
|
+
function graphFilePath(rootDir, layer) {
|
|
8485
|
+
return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
|
|
8486
|
+
}
|
|
8487
|
+
function tagsFilePath2(rootDir) {
|
|
8488
|
+
return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
|
|
8489
|
+
}
|
|
8490
|
+
function getMtimeMs(filePath) {
|
|
8491
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
|
|
8492
|
+
return (0, import_node_fs11.statSync)(filePath).mtimeMs;
|
|
8493
|
+
}
|
|
8494
|
+
function invalidateCache(filePath) {
|
|
8495
|
+
graphCache.delete(filePath);
|
|
8496
|
+
}
|
|
8497
|
+
function invalidateTaggedCache(rootDir, layer) {
|
|
8498
|
+
taggedCache.delete(`${rootDir}:${layer}`);
|
|
8499
|
+
}
|
|
8500
|
+
function applyTags(graph, layer, rootDir) {
|
|
8501
|
+
const config = loadConfig(rootDir);
|
|
8502
|
+
const registry = createTaggerRegistry(config, rootDir);
|
|
8503
|
+
const manualTags = readTagStore(rootDir);
|
|
8504
|
+
const taggedNodes = graph.nodes.map((n) => ({ ...n }));
|
|
8505
|
+
const taggers = registry.getForLayer(layer);
|
|
8506
|
+
for (const tagger of taggers) {
|
|
8507
|
+
const assignments = tagger.tag(taggedNodes, layer, rootDir);
|
|
8508
|
+
for (const node of taggedNodes) {
|
|
8509
|
+
if (!node.tags) node.tags = {};
|
|
8510
|
+
const tags = node.tags;
|
|
8511
|
+
const value = assignments.get(node.id);
|
|
8512
|
+
if (value !== void 0) {
|
|
8513
|
+
tags[tagger.tagKey] = value;
|
|
8514
|
+
} else if (tagger.trackUntagged) {
|
|
8515
|
+
tags[tagger.tagKey] = "untagged";
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
for (const node of taggedNodes) {
|
|
8520
|
+
const manual = manualTags[node.id];
|
|
8521
|
+
if (manual) {
|
|
8522
|
+
if (!node.tags) node.tags = {};
|
|
8523
|
+
const tags = node.tags;
|
|
8524
|
+
Object.assign(tags, manual);
|
|
8525
|
+
}
|
|
8526
|
+
}
|
|
8527
|
+
return { ...graph, nodes: taggedNodes };
|
|
8528
|
+
}
|
|
8529
|
+
function readGraphRaw(rootDir, layer) {
|
|
8530
|
+
const filePath = graphFilePath(rootDir, layer);
|
|
8531
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return null;
|
|
8532
|
+
const stat = (0, import_node_fs11.statSync)(filePath);
|
|
8533
|
+
const cached = graphCache.get(filePath);
|
|
8534
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
8535
|
+
return cached.graph;
|
|
8536
|
+
}
|
|
8537
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
8538
|
+
const graph = JSON.parse(content);
|
|
8539
|
+
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
8540
|
+
return graph;
|
|
8541
|
+
}
|
|
8542
|
+
function readGraph(rootDir, layer) {
|
|
8543
|
+
const rawFilePath = graphFilePath(rootDir, layer);
|
|
8544
|
+
if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
|
|
8545
|
+
const rawMtime = getMtimeMs(rawFilePath);
|
|
8546
|
+
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
8547
|
+
const cacheKey = `${rootDir}:${layer}`;
|
|
8548
|
+
const cached = taggedCache.get(cacheKey);
|
|
8549
|
+
if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
|
|
8550
|
+
return cached.graph;
|
|
8551
|
+
}
|
|
8552
|
+
const raw = readGraphRaw(rootDir, layer);
|
|
8553
|
+
if (!raw) return null;
|
|
8554
|
+
const tagged = applyTags(raw, layer, rootDir);
|
|
8555
|
+
taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
|
|
8556
|
+
return tagged;
|
|
8557
|
+
}
|
|
8558
|
+
function readAllGraphs(rootDir) {
|
|
8559
|
+
const result = {};
|
|
8560
|
+
for (const layer of LAYERS) {
|
|
8561
|
+
const graph = readGraph(rootDir, layer);
|
|
8562
|
+
if (graph) result[layer] = graph;
|
|
8563
|
+
}
|
|
8564
|
+
return result;
|
|
8565
|
+
}
|
|
8566
|
+
function generateGraph(rootDir, layer) {
|
|
8567
|
+
const dir = graphsDir(rootDir);
|
|
8568
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
8569
|
+
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
8570
|
+
for (const result of results) {
|
|
8571
|
+
const filePath = graphFilePath(rootDir, result.layer);
|
|
8572
|
+
(0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
8573
|
+
invalidateCache(filePath);
|
|
8574
|
+
invalidateTaggedCache(rootDir, result.layer);
|
|
8575
|
+
}
|
|
8576
|
+
return results;
|
|
8577
|
+
}
|
|
8578
|
+
|
|
8579
|
+
// src/server/graph-cli.ts
|
|
8580
|
+
var VALID_LAYERS = ["ui", "api", "db"];
|
|
8581
|
+
function parseLayerFlag(args) {
|
|
8582
|
+
const idx = args.indexOf("--layer");
|
|
8583
|
+
if (idx < 0 || idx + 1 >= args.length) return void 0;
|
|
8584
|
+
const value = args[idx + 1];
|
|
8585
|
+
if (!VALID_LAYERS.includes(value)) {
|
|
8586
|
+
console.error(`Invalid layer "${value}". Must be one of: ${VALID_LAYERS.join(", ")}`);
|
|
8587
|
+
process.exit(1);
|
|
8588
|
+
}
|
|
8589
|
+
return value;
|
|
8590
|
+
}
|
|
8591
|
+
function handleGraphCommand(subcommand, args) {
|
|
7752
8592
|
const rootDir = process.cwd();
|
|
7753
8593
|
if (subcommand === "graph:generate") {
|
|
7754
8594
|
const layer = parseLayerFlag(args);
|
|
@@ -7792,25 +8632,46 @@ function handleGraphCommand(subcommand, args) {
|
|
|
7792
8632
|
}
|
|
7793
8633
|
|
|
7794
8634
|
// src/server/graph-mcp.ts
|
|
7795
|
-
var
|
|
7796
|
-
var
|
|
8635
|
+
var import_node_fs13 = require("node:fs");
|
|
8636
|
+
var import_node_path15 = require("node:path");
|
|
8637
|
+
var import_node_child_process2 = require("node:child_process");
|
|
8638
|
+
var import_node_os2 = require("node:os");
|
|
7797
8639
|
|
|
7798
8640
|
// src/server/lockfile.ts
|
|
7799
8641
|
var import_node_child_process = require("node:child_process");
|
|
7800
|
-
var
|
|
8642
|
+
var import_node_fs12 = require("node:fs");
|
|
7801
8643
|
var import_node_os = require("node:os");
|
|
7802
|
-
var
|
|
7803
|
-
function lockDir() {
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
return (0,
|
|
7808
|
-
}
|
|
7809
|
-
function
|
|
7810
|
-
|
|
7811
|
-
|
|
8644
|
+
var import_node_path14 = require("node:path");
|
|
8645
|
+
function lockDir(projectRoot) {
|
|
8646
|
+
if (projectRoot) {
|
|
8647
|
+
return (0, import_node_path14.join)(projectRoot, ".launchsecure");
|
|
8648
|
+
}
|
|
8649
|
+
return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
8650
|
+
}
|
|
8651
|
+
function lockPath(projectRoot) {
|
|
8652
|
+
return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
8653
|
+
}
|
|
8654
|
+
var _activeProjectRoot;
|
|
8655
|
+
function readLock(projectRoot) {
|
|
8656
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
8657
|
+
const p = lockPath(root);
|
|
8658
|
+
if (!(0, import_node_fs12.existsSync)(p)) {
|
|
8659
|
+
if (root) {
|
|
8660
|
+
const globalP = lockPath();
|
|
8661
|
+
if ((0, import_node_fs12.existsSync)(globalP)) {
|
|
8662
|
+
try {
|
|
8663
|
+
const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
|
|
8664
|
+
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
8665
|
+
return data;
|
|
8666
|
+
}
|
|
8667
|
+
} catch {
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
8671
|
+
return null;
|
|
8672
|
+
}
|
|
7812
8673
|
try {
|
|
7813
|
-
const data = JSON.parse((0,
|
|
8674
|
+
const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
|
|
7814
8675
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
7815
8676
|
return data;
|
|
7816
8677
|
} catch {
|
|
@@ -7839,22 +8700,31 @@ function getListenerPid(port) {
|
|
|
7839
8700
|
return null;
|
|
7840
8701
|
}
|
|
7841
8702
|
}
|
|
7842
|
-
function getLiveLock() {
|
|
7843
|
-
const
|
|
8703
|
+
function getLiveLock(projectRoot) {
|
|
8704
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
8705
|
+
const lock = readLock(root);
|
|
7844
8706
|
if (!lock) return null;
|
|
7845
8707
|
const listenerPid = getListenerPid(lock.port);
|
|
7846
8708
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
7847
8709
|
if (!live) {
|
|
7848
8710
|
try {
|
|
7849
|
-
(0,
|
|
8711
|
+
(0, import_node_fs12.unlinkSync)(lockPath(root));
|
|
7850
8712
|
} catch {
|
|
7851
8713
|
}
|
|
7852
8714
|
return null;
|
|
7853
8715
|
}
|
|
7854
8716
|
return lock;
|
|
7855
8717
|
}
|
|
8718
|
+
function clearLock(projectRoot) {
|
|
8719
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
8720
|
+
try {
|
|
8721
|
+
(0, import_node_fs12.unlinkSync)(lockPath(root));
|
|
8722
|
+
} catch {
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
7856
8725
|
|
|
7857
8726
|
// src/server/graph-mcp.ts
|
|
8727
|
+
init_config();
|
|
7858
8728
|
var SERVER_INFO = {
|
|
7859
8729
|
name: "launchsecure-graph",
|
|
7860
8730
|
version: "0.0.1"
|
|
@@ -7876,7 +8746,7 @@ var TOOLS = [
|
|
|
7876
8746
|
},
|
|
7877
8747
|
{
|
|
7878
8748
|
name: "read_graph",
|
|
7879
|
-
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (
|
|
8749
|
+
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
7880
8750
|
inputSchema: {
|
|
7881
8751
|
type: "object",
|
|
7882
8752
|
properties: {
|
|
@@ -7895,7 +8765,15 @@ var TOOLS = [
|
|
|
7895
8765
|
},
|
|
7896
8766
|
module: {
|
|
7897
8767
|
type: "string",
|
|
7898
|
-
description: '
|
|
8768
|
+
description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
|
|
8769
|
+
},
|
|
8770
|
+
tag_key: {
|
|
8771
|
+
type: "string",
|
|
8772
|
+
description: "Filter by arbitrary tag key. Must be used with tag_value."
|
|
8773
|
+
},
|
|
8774
|
+
tag_value: {
|
|
8775
|
+
type: "string",
|
|
8776
|
+
description: "Filter by tag value for the given tag_key."
|
|
7899
8777
|
},
|
|
7900
8778
|
node_id: {
|
|
7901
8779
|
type: "string",
|
|
@@ -7982,12 +8860,83 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
7982
8860
|
}
|
|
7983
8861
|
},
|
|
7984
8862
|
{
|
|
7985
|
-
name: "
|
|
7986
|
-
description:
|
|
8863
|
+
name: "chart_server_status",
|
|
8864
|
+
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
8865
|
+
|
|
8866
|
+
Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
|
|
8867
|
+
inputSchema: {
|
|
8868
|
+
type: "object",
|
|
8869
|
+
properties: {}
|
|
8870
|
+
}
|
|
8871
|
+
},
|
|
8872
|
+
{
|
|
8873
|
+
name: "start_chart_server",
|
|
8874
|
+
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.',
|
|
8875
|
+
inputSchema: {
|
|
8876
|
+
type: "object",
|
|
8877
|
+
properties: {
|
|
8878
|
+
port: {
|
|
8879
|
+
type: "number",
|
|
8880
|
+
description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
},
|
|
8885
|
+
{
|
|
8886
|
+
name: "stop_chart_server",
|
|
8887
|
+
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.',
|
|
7987
8888
|
inputSchema: {
|
|
7988
8889
|
type: "object",
|
|
7989
8890
|
properties: {}
|
|
7990
8891
|
}
|
|
8892
|
+
},
|
|
8893
|
+
{
|
|
8894
|
+
name: "detect_project_stack",
|
|
8895
|
+
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.",
|
|
8896
|
+
inputSchema: {
|
|
8897
|
+
type: "object",
|
|
8898
|
+
properties: {}
|
|
8899
|
+
}
|
|
8900
|
+
},
|
|
8901
|
+
{
|
|
8902
|
+
name: "add_tag",
|
|
8903
|
+
description: 'Tag a graph node with a key-value pair. Tags persist in .launchsecure/graphs/tags.json and survive graph regeneration. Use for annotating nodes with arbitrary metadata (e.g. "refactor_later", "owner", "priority"). Manual tags override computed tags (like module and screen) for the same key.',
|
|
8904
|
+
inputSchema: {
|
|
8905
|
+
type: "object",
|
|
8906
|
+
properties: {
|
|
8907
|
+
node_id: {
|
|
8908
|
+
type: "string",
|
|
8909
|
+
description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
|
|
8910
|
+
},
|
|
8911
|
+
key: {
|
|
8912
|
+
type: "string",
|
|
8913
|
+
description: 'Tag key (e.g. "module", "owner", "refactor_later").'
|
|
8914
|
+
},
|
|
8915
|
+
value: {
|
|
8916
|
+
type: "string",
|
|
8917
|
+
description: 'Tag value (e.g. "auth", "alice", "true").'
|
|
8918
|
+
}
|
|
8919
|
+
},
|
|
8920
|
+
required: ["node_id", "key", "value"]
|
|
8921
|
+
}
|
|
8922
|
+
},
|
|
8923
|
+
{
|
|
8924
|
+
name: "remove_tag",
|
|
8925
|
+
description: "Remove a manual tag from a graph node. Only removes tags from tags.json \u2014 computed tags (module, screen) cannot be removed (they are re-derived at read time).",
|
|
8926
|
+
inputSchema: {
|
|
8927
|
+
type: "object",
|
|
8928
|
+
properties: {
|
|
8929
|
+
node_id: {
|
|
8930
|
+
type: "string",
|
|
8931
|
+
description: "The node id to remove the tag from."
|
|
8932
|
+
},
|
|
8933
|
+
key: {
|
|
8934
|
+
type: "string",
|
|
8935
|
+
description: "Tag key to remove."
|
|
8936
|
+
}
|
|
8937
|
+
},
|
|
8938
|
+
required: ["node_id", "key"]
|
|
8939
|
+
}
|
|
7991
8940
|
}
|
|
7992
8941
|
];
|
|
7993
8942
|
function matchesSearch(node, query) {
|
|
@@ -8001,7 +8950,7 @@ function matchesSearch(node, query) {
|
|
|
8001
8950
|
function toMinimal(nodes) {
|
|
8002
8951
|
return nodes.map((n) => {
|
|
8003
8952
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
8004
|
-
if (n.
|
|
8953
|
+
if (n.tags != null) out.tags = n.tags;
|
|
8005
8954
|
if (n.route != null) out.route = n.route;
|
|
8006
8955
|
if (n.methods != null) out.methods = n.methods;
|
|
8007
8956
|
return out;
|
|
@@ -8012,11 +8961,12 @@ var COMPACT_SCHEMA = {
|
|
|
8012
8961
|
i: "id",
|
|
8013
8962
|
t: "type",
|
|
8014
8963
|
n: "name",
|
|
8015
|
-
m: "module",
|
|
8964
|
+
m: "module (from tags)",
|
|
8016
8965
|
r: "route",
|
|
8017
8966
|
mt: "methods",
|
|
8018
8967
|
x: "exports",
|
|
8019
|
-
c: "columns"
|
|
8968
|
+
c: "columns",
|
|
8969
|
+
tg: "tags"
|
|
8020
8970
|
},
|
|
8021
8971
|
edges: {
|
|
8022
8972
|
s: "source_node_index",
|
|
@@ -8034,7 +8984,8 @@ var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
|
8034
8984
|
"route",
|
|
8035
8985
|
"methods",
|
|
8036
8986
|
"exports",
|
|
8037
|
-
"columns"
|
|
8987
|
+
"columns",
|
|
8988
|
+
"tags"
|
|
8038
8989
|
]);
|
|
8039
8990
|
var EST_CHARS_PER_NODE_FULL = {
|
|
8040
8991
|
ui: 300,
|
|
@@ -8055,11 +9006,13 @@ var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
|
8055
9006
|
var BATCH_BUDGET_CHARS = 6e4;
|
|
8056
9007
|
function toCompactNode(n) {
|
|
8057
9008
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
8058
|
-
|
|
9009
|
+
const tags = n.tags;
|
|
9010
|
+
if (tags?.module) out.m = tags.module;
|
|
8059
9011
|
if (n.route != null) out.r = n.route;
|
|
8060
9012
|
if (n.methods != null) out.mt = n.methods;
|
|
8061
9013
|
if (n.exports != null) out.x = n.exports;
|
|
8062
9014
|
if (n.columns != null) out.c = n.columns;
|
|
9015
|
+
if (tags != null) out.tg = tags;
|
|
8063
9016
|
for (const k of Object.keys(n)) {
|
|
8064
9017
|
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
8065
9018
|
}
|
|
@@ -8135,7 +9088,8 @@ function layerSummary(graph) {
|
|
|
8135
9088
|
const moduleCounts = {};
|
|
8136
9089
|
for (const n of graph.nodes) {
|
|
8137
9090
|
typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
|
|
8138
|
-
const
|
|
9091
|
+
const tags = n.tags;
|
|
9092
|
+
const mod = tags?.module;
|
|
8139
9093
|
if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
|
|
8140
9094
|
}
|
|
8141
9095
|
const edgeTypeCounts = {};
|
|
@@ -8192,12 +9146,14 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
8192
9146
|
const search = args.search;
|
|
8193
9147
|
const type = args.type;
|
|
8194
9148
|
const module_ = args.module;
|
|
9149
|
+
const tagKey = args.tag_key;
|
|
9150
|
+
const tagValue = args.tag_value;
|
|
8195
9151
|
const nodeId = args.node_id;
|
|
8196
9152
|
const hops = args.hops ?? 1;
|
|
8197
9153
|
const layerIsDb = args.layer === "db";
|
|
8198
9154
|
const minimal = args.minimal ?? layerIsDb;
|
|
8199
9155
|
const includeEdges = args.include_edges;
|
|
8200
|
-
const hasFilter = !!(search || type || module_ || nodeId);
|
|
9156
|
+
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
8201
9157
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
8202
9158
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
8203
9159
|
}
|
|
@@ -8253,7 +9209,9 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
8253
9209
|
const matched = graph.nodes.filter((n) => {
|
|
8254
9210
|
if (search && !matchesSearch(n, search)) return false;
|
|
8255
9211
|
if (type && n.type !== type) return false;
|
|
8256
|
-
|
|
9212
|
+
const nodeTags = n.tags;
|
|
9213
|
+
if (module_ && nodeTags?.module !== module_) return false;
|
|
9214
|
+
if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
|
|
8257
9215
|
return true;
|
|
8258
9216
|
});
|
|
8259
9217
|
const matchedIds = new Set(matched.map((n) => n.id));
|
|
@@ -8340,9 +9298,9 @@ function handleReadGraph(args) {
|
|
|
8340
9298
|
return okJson(result);
|
|
8341
9299
|
}
|
|
8342
9300
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
8343
|
-
if (layer === "ui") return (0,
|
|
8344
|
-
if (layer === "api") return (0,
|
|
8345
|
-
if (layer === "db") return (0,
|
|
9301
|
+
if (layer === "ui") return (0, import_node_path15.join)(rootDir, "src", nodeId);
|
|
9302
|
+
if (layer === "api") return (0, import_node_path15.join)(rootDir, nodeId);
|
|
9303
|
+
if (layer === "db") return (0, import_node_path15.join)(rootDir, "prisma", "schema.prisma");
|
|
8346
9304
|
return null;
|
|
8347
9305
|
}
|
|
8348
9306
|
function handleGrepNodes(args) {
|
|
@@ -8402,11 +9360,11 @@ function handleGrepNodes(args) {
|
|
|
8402
9360
|
let filesSearched = 0;
|
|
8403
9361
|
let truncated = false;
|
|
8404
9362
|
for (const [filePath, nodeId] of filePaths) {
|
|
8405
|
-
if (!(0,
|
|
9363
|
+
if (!(0, import_node_fs13.existsSync)(filePath)) continue;
|
|
8406
9364
|
filesSearched++;
|
|
8407
9365
|
let content;
|
|
8408
9366
|
try {
|
|
8409
|
-
content = (0,
|
|
9367
|
+
content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
|
|
8410
9368
|
} catch {
|
|
8411
9369
|
continue;
|
|
8412
9370
|
}
|
|
@@ -8443,13 +9401,11 @@ function handleGrepNodes(args) {
|
|
|
8443
9401
|
truncated
|
|
8444
9402
|
});
|
|
8445
9403
|
}
|
|
8446
|
-
function
|
|
8447
|
-
const
|
|
9404
|
+
function handleChartServerStatus() {
|
|
9405
|
+
const rootDir = process.cwd();
|
|
9406
|
+
const lock = getLiveLock(rootDir);
|
|
8448
9407
|
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
|
-
});
|
|
9408
|
+
return okJson({ running: false });
|
|
8453
9409
|
}
|
|
8454
9410
|
return okJson({
|
|
8455
9411
|
running: true,
|
|
@@ -8460,6 +9416,146 @@ function handleGetGraphUiUrl() {
|
|
|
8460
9416
|
startedAt: lock.startedAt
|
|
8461
9417
|
});
|
|
8462
9418
|
}
|
|
9419
|
+
function handleStartChartServer(args) {
|
|
9420
|
+
const rootDir = process.cwd();
|
|
9421
|
+
const lock = getLiveLock(rootDir);
|
|
9422
|
+
if (lock) {
|
|
9423
|
+
return okJson({
|
|
9424
|
+
started: false,
|
|
9425
|
+
reason: "already_running",
|
|
9426
|
+
url: lock.url,
|
|
9427
|
+
port: lock.port,
|
|
9428
|
+
pid: lock.pid
|
|
9429
|
+
});
|
|
9430
|
+
}
|
|
9431
|
+
const entryPath = process.argv[1];
|
|
9432
|
+
const logDir = (0, import_node_path15.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
9433
|
+
(0, import_node_fs13.mkdirSync)(logDir, { recursive: true });
|
|
9434
|
+
const logPath = (0, import_node_path15.join)(logDir, "launch-chart.log");
|
|
9435
|
+
const out = (0, import_node_fs13.openSync)(logPath, "a");
|
|
9436
|
+
const err2 = (0, import_node_fs13.openSync)(logPath, "a");
|
|
9437
|
+
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
9438
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
9439
|
+
detached: true,
|
|
9440
|
+
stdio: ["ignore", out, err2],
|
|
9441
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
9442
|
+
});
|
|
9443
|
+
child.unref();
|
|
9444
|
+
return okJson({
|
|
9445
|
+
started: true,
|
|
9446
|
+
pid: child.pid,
|
|
9447
|
+
logPath
|
|
9448
|
+
});
|
|
9449
|
+
}
|
|
9450
|
+
function handleStopChartServer() {
|
|
9451
|
+
const rootDir = process.cwd();
|
|
9452
|
+
const lock = getLiveLock(rootDir);
|
|
9453
|
+
if (!lock) {
|
|
9454
|
+
return okJson({ stopped: false, reason: "not_running" });
|
|
9455
|
+
}
|
|
9456
|
+
try {
|
|
9457
|
+
process.kill(lock.pid, "SIGTERM");
|
|
9458
|
+
return okJson({ stopped: true, pid: lock.pid });
|
|
9459
|
+
} catch (e) {
|
|
9460
|
+
const code = e.code;
|
|
9461
|
+
if (code === "ESRCH") {
|
|
9462
|
+
clearLock(rootDir);
|
|
9463
|
+
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
9464
|
+
}
|
|
9465
|
+
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
function handleAddTag(args) {
|
|
9469
|
+
const rootDir = process.cwd();
|
|
9470
|
+
const nodeId = args.node_id;
|
|
9471
|
+
const key = args.key;
|
|
9472
|
+
const value = args.value;
|
|
9473
|
+
if (!nodeId) return err("node_id is required");
|
|
9474
|
+
if (!key) return err("key is required");
|
|
9475
|
+
if (!value) return err("value is required");
|
|
9476
|
+
const graphs = readAllGraphs(rootDir);
|
|
9477
|
+
let found = false;
|
|
9478
|
+
for (const graph of Object.values(graphs)) {
|
|
9479
|
+
if (graph && graph.nodes.some((n) => n.id === nodeId)) {
|
|
9480
|
+
found = true;
|
|
9481
|
+
break;
|
|
9482
|
+
}
|
|
9483
|
+
}
|
|
9484
|
+
if (!found) {
|
|
9485
|
+
return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
|
|
9486
|
+
}
|
|
9487
|
+
setTag(rootDir, nodeId, key, value);
|
|
9488
|
+
return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
|
|
9489
|
+
}
|
|
9490
|
+
function handleRemoveTag(args) {
|
|
9491
|
+
const rootDir = process.cwd();
|
|
9492
|
+
const nodeId = args.node_id;
|
|
9493
|
+
const key = args.key;
|
|
9494
|
+
if (!nodeId) return err("node_id is required");
|
|
9495
|
+
if (!key) return err("key is required");
|
|
9496
|
+
removeTag(rootDir, nodeId, key);
|
|
9497
|
+
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
9498
|
+
}
|
|
9499
|
+
function handleDetectProjectStack() {
|
|
9500
|
+
const rootDir = process.cwd();
|
|
9501
|
+
const parsers = [
|
|
9502
|
+
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
9503
|
+
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
9504
|
+
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
9505
|
+
];
|
|
9506
|
+
const config = loadConfig(rootDir);
|
|
9507
|
+
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
9508
|
+
const uiGraph = readGraph(rootDir, "ui");
|
|
9509
|
+
if (uiGraph) {
|
|
9510
|
+
for (const ref of uiGraph.cross_refs ?? []) {
|
|
9511
|
+
if (ref.type === "calls_api") stats.calls_api++;
|
|
9512
|
+
if (ref.type === "references_api") stats.references_api++;
|
|
9513
|
+
}
|
|
9514
|
+
for (const f of uiGraph.flagged_edges ?? []) {
|
|
9515
|
+
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
const srcDir = (0, import_node_path15.join)(rootDir, "src");
|
|
9519
|
+
if ((0, import_node_fs13.existsSync)(srcDir)) {
|
|
9520
|
+
const scanDir = (dir) => {
|
|
9521
|
+
if (!(0, import_node_fs13.existsSync)(dir)) return;
|
|
9522
|
+
for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
|
|
9523
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
9524
|
+
const full = (0, import_node_path15.join)(dir, entry.name);
|
|
9525
|
+
if (entry.isDirectory()) {
|
|
9526
|
+
scanDir(full);
|
|
9527
|
+
continue;
|
|
9528
|
+
}
|
|
9529
|
+
if (![".ts", ".tsx"].includes((0, import_node_path15.extname)(entry.name))) continue;
|
|
9530
|
+
try {
|
|
9531
|
+
const content = (0, import_node_fs13.readFileSync)(full, "utf-8");
|
|
9532
|
+
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
9533
|
+
if (matches) stats.annotations += matches.length;
|
|
9534
|
+
} catch {
|
|
9535
|
+
}
|
|
9536
|
+
}
|
|
9537
|
+
};
|
|
9538
|
+
scanDir(srcDir);
|
|
9539
|
+
}
|
|
9540
|
+
let recommendedPrimary = "fetch-resolver";
|
|
9541
|
+
if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
|
|
9542
|
+
recommendedPrimary = "api-annotations";
|
|
9543
|
+
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
9544
|
+
recommendedPrimary = "url-literal-scanner";
|
|
9545
|
+
}
|
|
9546
|
+
return okJson({
|
|
9547
|
+
parsers,
|
|
9548
|
+
crosslayer_parsers: [
|
|
9549
|
+
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
9550
|
+
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
9551
|
+
{ id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
|
|
9552
|
+
],
|
|
9553
|
+
stats,
|
|
9554
|
+
recommended_primary: recommendedPrimary,
|
|
9555
|
+
current_config: Object.keys(config).length > 0 ? config : null,
|
|
9556
|
+
config_path: ".launchchart.json"
|
|
9557
|
+
});
|
|
9558
|
+
}
|
|
8463
9559
|
function send(msg) {
|
|
8464
9560
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
8465
9561
|
}
|
|
@@ -8503,8 +9599,28 @@ function handleMessage(msg) {
|
|
|
8503
9599
|
respond(id ?? null, handleGrepNodes(args));
|
|
8504
9600
|
return;
|
|
8505
9601
|
}
|
|
8506
|
-
if (toolName === "
|
|
8507
|
-
respond(id ?? null,
|
|
9602
|
+
if (toolName === "chart_server_status") {
|
|
9603
|
+
respond(id ?? null, handleChartServerStatus());
|
|
9604
|
+
return;
|
|
9605
|
+
}
|
|
9606
|
+
if (toolName === "start_chart_server") {
|
|
9607
|
+
respond(id ?? null, handleStartChartServer(args));
|
|
9608
|
+
return;
|
|
9609
|
+
}
|
|
9610
|
+
if (toolName === "stop_chart_server") {
|
|
9611
|
+
respond(id ?? null, handleStopChartServer());
|
|
9612
|
+
return;
|
|
9613
|
+
}
|
|
9614
|
+
if (toolName === "detect_project_stack") {
|
|
9615
|
+
respond(id ?? null, handleDetectProjectStack());
|
|
9616
|
+
return;
|
|
9617
|
+
}
|
|
9618
|
+
if (toolName === "add_tag") {
|
|
9619
|
+
respond(id ?? null, handleAddTag(args));
|
|
9620
|
+
return;
|
|
9621
|
+
}
|
|
9622
|
+
if (toolName === "remove_tag") {
|
|
9623
|
+
respond(id ?? null, handleRemoveTag(args));
|
|
8508
9624
|
return;
|
|
8509
9625
|
}
|
|
8510
9626
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
@@ -8604,7 +9720,7 @@ function parseArgs() {
|
|
|
8604
9720
|
return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
|
|
8605
9721
|
}
|
|
8606
9722
|
function tryListen(server, port, maxRetries = 10) {
|
|
8607
|
-
return new Promise((
|
|
9723
|
+
return new Promise((resolve3, reject) => {
|
|
8608
9724
|
let attempts = 0;
|
|
8609
9725
|
function attempt(p) {
|
|
8610
9726
|
server.once("error", (err2) => {
|
|
@@ -8615,7 +9731,7 @@ function tryListen(server, port, maxRetries = 10) {
|
|
|
8615
9731
|
reject(err2);
|
|
8616
9732
|
}
|
|
8617
9733
|
});
|
|
8618
|
-
server.listen(p, () =>
|
|
9734
|
+
server.listen(p, () => resolve3(p));
|
|
8619
9735
|
}
|
|
8620
9736
|
attempt(port);
|
|
8621
9737
|
});
|
|
@@ -8636,7 +9752,7 @@ function saveCredentials(creds) {
|
|
|
8636
9752
|
});
|
|
8637
9753
|
}
|
|
8638
9754
|
function verifyToken(serverUrl, token) {
|
|
8639
|
-
return new Promise((
|
|
9755
|
+
return new Promise((resolve3) => {
|
|
8640
9756
|
const url = new URL("/api/mcp/verify", serverUrl);
|
|
8641
9757
|
const body = JSON.stringify({ token });
|
|
8642
9758
|
const mod = url.protocol === "https:" ? import_https.default : import_http.default;
|
|
@@ -8651,30 +9767,30 @@ function verifyToken(serverUrl, token) {
|
|
|
8651
9767
|
res.on("data", (chunk) => data += chunk);
|
|
8652
9768
|
res.on("end", () => {
|
|
8653
9769
|
try {
|
|
8654
|
-
|
|
9770
|
+
resolve3(JSON.parse(data));
|
|
8655
9771
|
} catch {
|
|
8656
|
-
|
|
9772
|
+
resolve3({ valid: false, error: "Invalid response from server" });
|
|
8657
9773
|
}
|
|
8658
9774
|
});
|
|
8659
9775
|
});
|
|
8660
9776
|
req.on("error", (err2) => {
|
|
8661
|
-
|
|
9777
|
+
resolve3({ valid: false, error: `Cannot reach server: ${err2.message}` });
|
|
8662
9778
|
});
|
|
8663
9779
|
req.setTimeout(1e4, () => {
|
|
8664
9780
|
req.destroy();
|
|
8665
|
-
|
|
9781
|
+
resolve3({ valid: false, error: "Connection timed out" });
|
|
8666
9782
|
});
|
|
8667
9783
|
req.write(body);
|
|
8668
9784
|
req.end();
|
|
8669
9785
|
});
|
|
8670
9786
|
}
|
|
8671
9787
|
function httpRequest(reqUrl, options, body, timeout = 3e4) {
|
|
8672
|
-
return new Promise((
|
|
9788
|
+
return new Promise((resolve3, reject) => {
|
|
8673
9789
|
const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
|
|
8674
9790
|
const r = mod.request(reqUrl, options, (resp) => {
|
|
8675
9791
|
let data = "";
|
|
8676
9792
|
resp.on("data", (chunk) => data += chunk);
|
|
8677
|
-
resp.on("end", () =>
|
|
9793
|
+
resp.on("end", () => resolve3({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
|
|
8678
9794
|
});
|
|
8679
9795
|
r.on("error", reject);
|
|
8680
9796
|
r.setTimeout(timeout, () => {
|