@launchsecure/launch-kit 0.0.14 → 0.0.16
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-Dm6IBkiC.js → index-BpQPtTuo.js} +89 -89
- package/dist/chart-client/assets/index-CbZ13AXL.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCYw64M7.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +49 -49
- package/dist/server/cli.js +822 -201
- package/dist/server/graph-mcp-entry.js +687 -103
- package/package.json +1 -1
- package/dist/chart-client/assets/index-l-yyLDX5.css +0 -1
- package/dist/client/assets/index-rlw8dmPR.css +0 -32
- /package/dist/client/assets/{index-CyML1UiJ.js → index-3ENenBk-.js} +0 -0
|
@@ -156,19 +156,26 @@ var init_config = __esm({
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
// src/server/graph/core/resolve-paths.ts
|
|
159
|
+
function detectDbDir(rootDir, config) {
|
|
160
|
+
if (config.paths?.dbDir) return (0, import_node_path3.join)(rootDir, config.paths.dbDir);
|
|
161
|
+
const prismaDir = (0, import_node_path3.join)(rootDir, "prisma");
|
|
162
|
+
if ((0, import_node_fs3.existsSync)(prismaDir)) return prismaDir;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
159
165
|
function resolveProjectPaths(rootDir, config) {
|
|
166
|
+
const dbDir = detectDbDir(rootDir, config);
|
|
160
167
|
if (config.paths?.appDir) {
|
|
161
168
|
const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
|
|
162
169
|
const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
|
|
163
|
-
return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api") };
|
|
170
|
+
return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api"), dbDir };
|
|
164
171
|
}
|
|
165
172
|
const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
|
|
166
173
|
if ((0, import_node_fs3.existsSync)(srcApp)) {
|
|
167
|
-
return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api") };
|
|
174
|
+
return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api"), dbDir };
|
|
168
175
|
}
|
|
169
176
|
const rootApp = (0, import_node_path3.join)(rootDir, "app");
|
|
170
177
|
if ((0, import_node_fs3.existsSync)(rootApp)) {
|
|
171
|
-
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api") };
|
|
178
|
+
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api"), dbDir };
|
|
172
179
|
}
|
|
173
180
|
return null;
|
|
174
181
|
}
|
|
@@ -2111,6 +2118,7 @@ var init_fetch_resolver = __esm({
|
|
|
2111
2118
|
fetchResolverParser = {
|
|
2112
2119
|
id: "fetch-resolver",
|
|
2113
2120
|
layer: "crosslayer",
|
|
2121
|
+
concern: "api-binding",
|
|
2114
2122
|
detect(_rootDir) {
|
|
2115
2123
|
return true;
|
|
2116
2124
|
},
|
|
@@ -2231,6 +2239,7 @@ var init_api_annotations = __esm({
|
|
|
2231
2239
|
apiAnnotationsParser = {
|
|
2232
2240
|
id: "api-annotations",
|
|
2233
2241
|
layer: "crosslayer",
|
|
2242
|
+
concern: "api-binding",
|
|
2234
2243
|
detect(rootDir) {
|
|
2235
2244
|
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
2236
2245
|
},
|
|
@@ -2325,6 +2334,7 @@ var init_url_literal_scanner = __esm({
|
|
|
2325
2334
|
urlLiteralScannerParser = {
|
|
2326
2335
|
id: "url-literal-scanner",
|
|
2327
2336
|
layer: "crosslayer",
|
|
2337
|
+
concern: "api-binding",
|
|
2328
2338
|
detect(rootDir) {
|
|
2329
2339
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2330
2340
|
return paths !== null;
|
|
@@ -2949,6 +2959,7 @@ var init_static_ref_scanner = __esm({
|
|
|
2949
2959
|
staticRefScannerParser = {
|
|
2950
2960
|
id: "static-ref-scanner",
|
|
2951
2961
|
layer: "crosslayer",
|
|
2962
|
+
concern: "static-ref",
|
|
2952
2963
|
detect(rootDir) {
|
|
2953
2964
|
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2954
2965
|
return paths !== null;
|
|
@@ -3083,6 +3094,9 @@ function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
|
3083
3094
|
`
|
|
3084
3095
|
);
|
|
3085
3096
|
}
|
|
3097
|
+
if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
|
|
3098
|
+
parser.concern = entry.concern;
|
|
3099
|
+
}
|
|
3086
3100
|
registry.register(parser);
|
|
3087
3101
|
} catch (err2) {
|
|
3088
3102
|
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
@@ -3247,44 +3261,21 @@ function dedupCrossRefs(refs) {
|
|
|
3247
3261
|
}
|
|
3248
3262
|
return result;
|
|
3249
3263
|
}
|
|
3250
|
-
function applyCrossLayerResults(uiOutput, results
|
|
3251
|
-
const allCrossRefs = [...uiOutput.cross_refs];
|
|
3252
|
-
const allFlagged = [...uiOutput.flagged_edges];
|
|
3253
|
-
const allWarnings = [...uiOutput.warnings];
|
|
3254
|
-
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
3255
|
-
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
3256
|
-
if (primaryResult) {
|
|
3257
|
-
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
3258
|
-
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
3259
|
-
allWarnings.push(...primaryResult.output.warnings);
|
|
3260
|
-
}
|
|
3261
|
-
const primarySet = new Set(
|
|
3262
|
-
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
3263
|
-
);
|
|
3264
|
-
for (const sec of secondaryResults) {
|
|
3265
|
-
for (const ref of sec.output.cross_refs) {
|
|
3266
|
-
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
3267
|
-
if (primarySet.has(key)) {
|
|
3268
|
-
allCrossRefs.push(ref);
|
|
3269
|
-
} else {
|
|
3270
|
-
allFlagged.push({
|
|
3271
|
-
source: ref.source,
|
|
3272
|
-
target: ref.target,
|
|
3273
|
-
type: "out_of_pattern",
|
|
3274
|
-
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
3275
|
-
confidence: "medium"
|
|
3276
|
-
});
|
|
3277
|
-
allCrossRefs.push(ref);
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
allFlagged.push(...sec.output.flagged_edges);
|
|
3281
|
-
allWarnings.push(...sec.output.warnings);
|
|
3282
|
-
}
|
|
3264
|
+
function applyCrossLayerResults(uiOutput, results) {
|
|
3283
3265
|
return {
|
|
3284
3266
|
...uiOutput,
|
|
3285
|
-
cross_refs: dedupCrossRefs(
|
|
3286
|
-
|
|
3287
|
-
|
|
3267
|
+
cross_refs: dedupCrossRefs([
|
|
3268
|
+
...uiOutput.cross_refs,
|
|
3269
|
+
...results.flatMap((r) => r.output.cross_refs)
|
|
3270
|
+
]),
|
|
3271
|
+
flagged_edges: [
|
|
3272
|
+
...uiOutput.flagged_edges,
|
|
3273
|
+
...results.flatMap((r) => r.output.flagged_edges)
|
|
3274
|
+
],
|
|
3275
|
+
warnings: [
|
|
3276
|
+
...uiOutput.warnings,
|
|
3277
|
+
...results.flatMap((r) => r.output.warnings)
|
|
3278
|
+
]
|
|
3288
3279
|
};
|
|
3289
3280
|
}
|
|
3290
3281
|
var init_merge = __esm({
|
|
@@ -3328,10 +3319,9 @@ function generateLayer(rootDir, layer) {
|
|
|
3328
3319
|
if (existing) layerOutputs.set(otherLayer, existing);
|
|
3329
3320
|
}
|
|
3330
3321
|
const crossParsers = registry.getCrossLayerParsers();
|
|
3331
|
-
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
3332
3322
|
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
3333
3323
|
if (crossResults.length > 0) {
|
|
3334
|
-
merged = applyCrossLayerResults(merged, crossResults
|
|
3324
|
+
merged = applyCrossLayerResults(merged, crossResults);
|
|
3335
3325
|
}
|
|
3336
3326
|
}
|
|
3337
3327
|
return {
|
|
@@ -3378,11 +3368,10 @@ function generateAll(rootDir) {
|
|
|
3378
3368
|
});
|
|
3379
3369
|
}
|
|
3380
3370
|
const crossParsers = registry.getCrossLayerParsers();
|
|
3381
|
-
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
3382
3371
|
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
3383
3372
|
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
3384
3373
|
const uiOutput = layerOutputs.get("ui");
|
|
3385
|
-
const merged = applyCrossLayerResults(uiOutput, crossResults
|
|
3374
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults);
|
|
3386
3375
|
layerOutputs.set("ui", merged);
|
|
3387
3376
|
const uiResult = results.find((r) => r.layer === "ui");
|
|
3388
3377
|
if (uiResult) {
|
|
@@ -4496,17 +4485,23 @@ async function startChartServer(opts = {}) {
|
|
|
4496
4485
|
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
4497
4486
|
const config2 = loadConfig(reqRoot);
|
|
4498
4487
|
const registry = createRegistry(config2, reqRoot);
|
|
4488
|
+
const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
4499
4489
|
const detection = [];
|
|
4500
4490
|
for (const parser of registry.getAll()) {
|
|
4501
4491
|
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
4502
4492
|
const mp = parser;
|
|
4503
|
-
detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
|
|
4493
|
+
detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
|
|
4504
4494
|
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
4505
4495
|
const sp = parser;
|
|
4506
|
-
detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
|
|
4496
|
+
detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
|
|
4507
4497
|
}
|
|
4508
4498
|
}
|
|
4509
|
-
const crosslayerParsers =
|
|
4499
|
+
const crosslayerParsers = {};
|
|
4500
|
+
for (const p of registry.getCrossLayerParsers()) {
|
|
4501
|
+
const concern = p.concern ?? "api-binding";
|
|
4502
|
+
if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
|
|
4503
|
+
crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
|
|
4504
|
+
}
|
|
4510
4505
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4511
4506
|
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
4512
4507
|
return;
|
|
@@ -4619,16 +4614,21 @@ async function startChartServer(opts = {}) {
|
|
|
4619
4614
|
if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
|
|
4620
4615
|
const config2 = loadConfig(reqRoot);
|
|
4621
4616
|
const paths = resolveProjectPaths(reqRoot, config2);
|
|
4622
|
-
const
|
|
4617
|
+
const overrides = {
|
|
4618
|
+
appDir: !!config2.paths?.appDir,
|
|
4619
|
+
dbDir: !!config2.paths?.dbDir
|
|
4620
|
+
};
|
|
4623
4621
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4624
4622
|
res.end(JSON.stringify({
|
|
4625
4623
|
projectRoot: reqRoot,
|
|
4626
4624
|
detected: paths ? {
|
|
4627
4625
|
srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
|
|
4628
4626
|
appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
|
|
4629
|
-
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
|
|
4627
|
+
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
|
|
4628
|
+
dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
|
|
4630
4629
|
} : null,
|
|
4631
|
-
|
|
4630
|
+
overrides,
|
|
4631
|
+
isOverride: overrides.appDir
|
|
4632
4632
|
}));
|
|
4633
4633
|
return;
|
|
4634
4634
|
}
|
|
@@ -4774,13 +4774,221 @@ var init_chart_serve = __esm({
|
|
|
4774
4774
|
}
|
|
4775
4775
|
});
|
|
4776
4776
|
|
|
4777
|
+
// src/server/blast-radius-builder.ts
|
|
4778
|
+
function loadDefaults(rootDir) {
|
|
4779
|
+
const filePath = (0, import_node_path20.join)(rootDir, ".launchsecure", "blast-radius-defaults.json");
|
|
4780
|
+
try {
|
|
4781
|
+
if (import_node_fs18.default.existsSync(filePath)) {
|
|
4782
|
+
const raw = import_node_fs18.default.readFileSync(filePath, "utf-8");
|
|
4783
|
+
return JSON.parse(raw);
|
|
4784
|
+
}
|
|
4785
|
+
} catch {
|
|
4786
|
+
}
|
|
4787
|
+
return FALLBACK_DEFAULTS;
|
|
4788
|
+
}
|
|
4789
|
+
function generateAcceptance(node, inspect) {
|
|
4790
|
+
const criteria = [];
|
|
4791
|
+
const t = node.type?.toLowerCase() ?? "";
|
|
4792
|
+
if (t === "endpoint" || t === "mcp-tool") {
|
|
4793
|
+
const methods = inspect?.methods ?? [];
|
|
4794
|
+
const path3 = inspect?.path ?? node.id;
|
|
4795
|
+
if (methods.length > 0) {
|
|
4796
|
+
criteria.push(`${methods.join("/")} ${path3} still returns correct responses for authorized users`);
|
|
4797
|
+
} else {
|
|
4798
|
+
criteria.push(`${path3} still responds correctly`);
|
|
4799
|
+
}
|
|
4800
|
+
if (inspect?.auth && inspect.auth.includes("withAuth")) {
|
|
4801
|
+
criteria.push("Authentication and authorization still enforced");
|
|
4802
|
+
}
|
|
4803
|
+
if (inspect?.db_models && inspect.db_models.length > 0) {
|
|
4804
|
+
criteria.push(`DB operations on ${inspect.db_models.join(", ")} still work correctly`);
|
|
4805
|
+
}
|
|
4806
|
+
} else if (t === "page" || t === "component" || t === "layout") {
|
|
4807
|
+
criteria.push(`${node.name} renders without errors`);
|
|
4808
|
+
if (inspect?.stateVars && inspect.stateVars.length > 0) {
|
|
4809
|
+
criteria.push("State management still works correctly");
|
|
4810
|
+
}
|
|
4811
|
+
if (inspect?.elements && inspect.elements.length > 5) {
|
|
4812
|
+
criteria.push("All child components render correctly");
|
|
4813
|
+
}
|
|
4814
|
+
} else if (t === "table" || t === "enum") {
|
|
4815
|
+
criteria.push(`${node.name} schema unchanged or migration applies cleanly`);
|
|
4816
|
+
criteria.push("Existing queries against this table still work");
|
|
4817
|
+
} else if (t === "hook") {
|
|
4818
|
+
criteria.push(`${node.name} returns expected shape`);
|
|
4819
|
+
if (inspect?.stateVars && inspect.stateVars.length > 0) {
|
|
4820
|
+
criteria.push(`State variables [${inspect.stateVars.map((s) => s.name).join(", ")}] still returned`);
|
|
4821
|
+
}
|
|
4822
|
+
} else if (t === "context") {
|
|
4823
|
+
criteria.push(`${node.name} provides correct context to consumers`);
|
|
4824
|
+
} else if (t === "lib" || t === "config" || t === "types") {
|
|
4825
|
+
criteria.push(`${node.name} exports still conform to expected interface`);
|
|
4826
|
+
} else if (t === "seed" || t === "seed_role" || t === "seed_permission") {
|
|
4827
|
+
criteria.push("Seed runs without errors");
|
|
4828
|
+
criteria.push("Expected rows created in database");
|
|
4829
|
+
} else {
|
|
4830
|
+
criteria.push("Verify no regression");
|
|
4831
|
+
}
|
|
4832
|
+
return criteria;
|
|
4833
|
+
}
|
|
4834
|
+
function buildManifest(input) {
|
|
4835
|
+
const { mode, title, description, subtitle, blastResults, createNodes, inspectData, defaults } = input;
|
|
4836
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
4837
|
+
const centerNodeIds = /* @__PURE__ */ new Set();
|
|
4838
|
+
for (const result of blastResults) {
|
|
4839
|
+
centerNodeIds.add(result.center.id);
|
|
4840
|
+
for (const node of result.affected) {
|
|
4841
|
+
const existing = nodeMap.get(node.id);
|
|
4842
|
+
if (!existing || node.hop < existing.hop) {
|
|
4843
|
+
nodeMap.set(node.id, node);
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
for (const id of centerNodeIds) {
|
|
4848
|
+
nodeMap.delete(id);
|
|
4849
|
+
}
|
|
4850
|
+
const manifestNodes = [];
|
|
4851
|
+
for (const result of blastResults) {
|
|
4852
|
+
const c = result.center;
|
|
4853
|
+
if (manifestNodes.some((n) => n.id === c.id)) continue;
|
|
4854
|
+
const inspect = inspectData[c.id];
|
|
4855
|
+
manifestNodes.push({
|
|
4856
|
+
id: c.id,
|
|
4857
|
+
name: c.name,
|
|
4858
|
+
layer: c.layer,
|
|
4859
|
+
ring: "modify",
|
|
4860
|
+
type: c.type,
|
|
4861
|
+
reason: `Direct change target`,
|
|
4862
|
+
acceptance: generateAcceptance(
|
|
4863
|
+
{ id: c.id, name: c.name, type: c.type, layer: c.layer, hop: 0 },
|
|
4864
|
+
inspect
|
|
4865
|
+
)
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
for (const [, node] of nodeMap) {
|
|
4869
|
+
const ring = node.hop <= 1 ? "modify" : "ripple";
|
|
4870
|
+
const inspect = inspectData[node.id];
|
|
4871
|
+
const reason = node.hop <= 1 ? `Directly depends on changed node` : `Indirect dependency (${node.hop} hops away)`;
|
|
4872
|
+
manifestNodes.push({
|
|
4873
|
+
id: node.id,
|
|
4874
|
+
name: node.name,
|
|
4875
|
+
layer: node.layer,
|
|
4876
|
+
ring,
|
|
4877
|
+
type: node.type,
|
|
4878
|
+
reason,
|
|
4879
|
+
acceptance: generateAcceptance(node, inspect)
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
for (const cn of createNodes) {
|
|
4883
|
+
manifestNodes.push({
|
|
4884
|
+
id: cn.id,
|
|
4885
|
+
name: cn.name,
|
|
4886
|
+
layer: cn.layer,
|
|
4887
|
+
ring: "create",
|
|
4888
|
+
type: cn.type ?? "unknown",
|
|
4889
|
+
reason: cn.reason,
|
|
4890
|
+
acceptance: cn.acceptance ?? ["Verify implementation matches spec"]
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4893
|
+
const layerIds = /* @__PURE__ */ new Set();
|
|
4894
|
+
for (const n of manifestNodes) {
|
|
4895
|
+
layerIds.add(n.layer);
|
|
4896
|
+
}
|
|
4897
|
+
const layers = [];
|
|
4898
|
+
for (const id of layerIds) {
|
|
4899
|
+
const def = defaults.layers[id];
|
|
4900
|
+
if (def) {
|
|
4901
|
+
layers.push({ id, name: def.name, icon: def.icon, color: def.color });
|
|
4902
|
+
} else {
|
|
4903
|
+
layers.push({ id, name: id, icon: "box", color: "#cbd5e1" });
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
const edgeSet = /* @__PURE__ */ new Set();
|
|
4907
|
+
const edges = [];
|
|
4908
|
+
const allNodeIds = new Set(manifestNodes.map((n) => n.id));
|
|
4909
|
+
for (const cId of centerNodeIds) {
|
|
4910
|
+
for (const result of blastResults) {
|
|
4911
|
+
for (const affected of result.affected) {
|
|
4912
|
+
if (affected.hop === 1 && result.center.id === cId && allNodeIds.has(affected.id)) {
|
|
4913
|
+
const key = `${cId}->${affected.id}`;
|
|
4914
|
+
if (!edgeSet.has(key)) {
|
|
4915
|
+
edgeSet.add(key);
|
|
4916
|
+
edges.push({ source: cId, target: affected.id });
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
for (const result of blastResults) {
|
|
4923
|
+
if (result.edges) {
|
|
4924
|
+
for (const edge of result.edges) {
|
|
4925
|
+
if (allNodeIds.has(edge.source) && allNodeIds.has(edge.target)) {
|
|
4926
|
+
const key = `${edge.source}->${edge.target}`;
|
|
4927
|
+
if (!edgeSet.has(key)) {
|
|
4928
|
+
edgeSet.add(key);
|
|
4929
|
+
edges.push({ source: edge.source, target: edge.target });
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
for (const cn of createNodes) {
|
|
4936
|
+
edges.push({ source: "center", target: cn.id });
|
|
4937
|
+
if (cn.connects_to) {
|
|
4938
|
+
for (const targetId of cn.connects_to) {
|
|
4939
|
+
if (allNodeIds.has(targetId) || createNodes.some((c) => c.id === targetId)) {
|
|
4940
|
+
const key = `${cn.id}->${targetId}`;
|
|
4941
|
+
if (!edgeSet.has(key)) {
|
|
4942
|
+
edgeSet.add(key);
|
|
4943
|
+
edges.push({ source: cn.id, target: targetId });
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
return {
|
|
4950
|
+
mode,
|
|
4951
|
+
title,
|
|
4952
|
+
subtitle,
|
|
4953
|
+
layers,
|
|
4954
|
+
rings: defaults.rings,
|
|
4955
|
+
center: { name: title, description },
|
|
4956
|
+
nodes: manifestNodes,
|
|
4957
|
+
edges
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
var import_node_fs18, import_node_path20, FALLBACK_DEFAULTS;
|
|
4961
|
+
var init_blast_radius_builder = __esm({
|
|
4962
|
+
"src/server/blast-radius-builder.ts"() {
|
|
4963
|
+
"use strict";
|
|
4964
|
+
import_node_fs18 = __toESM(require("node:fs"));
|
|
4965
|
+
import_node_path20 = require("node:path");
|
|
4966
|
+
FALLBACK_DEFAULTS = {
|
|
4967
|
+
rings: [
|
|
4968
|
+
{ id: "modify", name: "Modify", color: "#ff6b00" },
|
|
4969
|
+
{ id: "ripple", name: "Ripple (verify)", color: "#ffff00" },
|
|
4970
|
+
{ id: "create", name: "Create", color: "#00ff00" }
|
|
4971
|
+
],
|
|
4972
|
+
layers: {
|
|
4973
|
+
db: { name: "Database", icon: "database", color: "#cbd5e1" },
|
|
4974
|
+
api: { name: "API", icon: "server", color: "#cbd5e1" },
|
|
4975
|
+
middleware: { name: "Middleware", icon: "shield", color: "#cbd5e1" },
|
|
4976
|
+
ui: { name: "UI", icon: "layout-dashboard", color: "#cbd5e1" },
|
|
4977
|
+
config: { name: "Config / Seed", icon: "settings", color: "#cbd5e1" },
|
|
4978
|
+
shared: { name: "Shared Types", icon: "box", color: "#cbd5e1" }
|
|
4979
|
+
},
|
|
4980
|
+
center: { color: "#ff0000" }
|
|
4981
|
+
};
|
|
4982
|
+
}
|
|
4983
|
+
});
|
|
4984
|
+
|
|
4777
4985
|
// src/server/graph/core/language-detection.ts
|
|
4778
4986
|
function walkForExtensions(dir, extCounts, depth = 0) {
|
|
4779
4987
|
if (depth > 10) return;
|
|
4780
|
-
if (!(0,
|
|
4988
|
+
if (!(0, import_node_fs19.existsSync)(dir)) return;
|
|
4781
4989
|
let entries;
|
|
4782
4990
|
try {
|
|
4783
|
-
entries = (0,
|
|
4991
|
+
entries = (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true });
|
|
4784
4992
|
} catch {
|
|
4785
4993
|
return;
|
|
4786
4994
|
}
|
|
@@ -4788,9 +4996,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
|
|
|
4788
4996
|
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
4789
4997
|
if (entry.isDirectory()) {
|
|
4790
4998
|
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
4791
|
-
walkForExtensions((0,
|
|
4999
|
+
walkForExtensions((0, import_node_path21.join)(dir, entry.name), extCounts, depth + 1);
|
|
4792
5000
|
} else {
|
|
4793
|
-
const ext = (0,
|
|
5001
|
+
const ext = (0, import_node_path21.extname)(entry.name).toLowerCase();
|
|
4794
5002
|
if (ext && EXTENSION_TO_LANGUAGE[ext]) {
|
|
4795
5003
|
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
4796
5004
|
}
|
|
@@ -4829,12 +5037,12 @@ function detectLanguages(rootDir, supportedLanguages) {
|
|
|
4829
5037
|
});
|
|
4830
5038
|
return results;
|
|
4831
5039
|
}
|
|
4832
|
-
var
|
|
5040
|
+
var import_node_fs19, import_node_path21, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
|
|
4833
5041
|
var init_language_detection = __esm({
|
|
4834
5042
|
"src/server/graph/core/language-detection.ts"() {
|
|
4835
5043
|
"use strict";
|
|
4836
|
-
|
|
4837
|
-
|
|
5044
|
+
import_node_fs19 = require("node:fs");
|
|
5045
|
+
import_node_path21 = require("node:path");
|
|
4838
5046
|
EXTENSION_TO_LANGUAGE = {
|
|
4839
5047
|
// Web / Frontend
|
|
4840
5048
|
".ts": "typescript",
|
|
@@ -5048,6 +5256,263 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
5048
5256
|
const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
|
|
5049
5257
|
return { nodes, edges, budgetExceeded, stoppedAtHop };
|
|
5050
5258
|
}
|
|
5259
|
+
function reverseNeighborhood(graph, centerId, hops, direction) {
|
|
5260
|
+
const center = graph.nodes.find((n) => n.id === centerId);
|
|
5261
|
+
if (!center) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
|
|
5262
|
+
const visited = /* @__PURE__ */ new Map();
|
|
5263
|
+
visited.set(centerId, { node: center, hop: 0 });
|
|
5264
|
+
let frontier = /* @__PURE__ */ new Set([centerId]);
|
|
5265
|
+
for (let h = 0; h < hops; h++) {
|
|
5266
|
+
const next = /* @__PURE__ */ new Set();
|
|
5267
|
+
for (const edge of graph.edges) {
|
|
5268
|
+
if (direction === "reverse") {
|
|
5269
|
+
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
5270
|
+
} else {
|
|
5271
|
+
if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
|
|
5272
|
+
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
for (const id of next) {
|
|
5276
|
+
const node = graph.nodes.find((n) => n.id === id);
|
|
5277
|
+
if (node) visited.set(id, { node, hop: h + 1 });
|
|
5278
|
+
}
|
|
5279
|
+
frontier = next;
|
|
5280
|
+
if (frontier.size === 0) break;
|
|
5281
|
+
}
|
|
5282
|
+
const nodeIds = new Set(visited.keys());
|
|
5283
|
+
const edges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
5284
|
+
return { nodes: visited, edges };
|
|
5285
|
+
}
|
|
5286
|
+
function handleBlastPoints(args) {
|
|
5287
|
+
const rootDir = process.cwd();
|
|
5288
|
+
const nodeId = args.node_id;
|
|
5289
|
+
const requestedLayer = args.layer;
|
|
5290
|
+
const hops = args.hops ?? 2;
|
|
5291
|
+
const direction = args.direction ?? "reverse";
|
|
5292
|
+
let targetLayer = requestedLayer;
|
|
5293
|
+
if (!targetLayer) {
|
|
5294
|
+
const graphs = readAllGraphs(rootDir);
|
|
5295
|
+
for (const [layer, graph2] of Object.entries(graphs)) {
|
|
5296
|
+
if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
|
|
5297
|
+
targetLayer = layer;
|
|
5298
|
+
break;
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
if (!targetLayer) {
|
|
5302
|
+
return err(`Node "${nodeId}" not found in any layer. Available layers: ${getAvailableLayers(rootDir).join(", ")}`);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
const graph = readGraph(rootDir, targetLayer);
|
|
5306
|
+
if (!graph) {
|
|
5307
|
+
return err(`No graph for layer "${targetLayer}". Run generate_graph first.`);
|
|
5308
|
+
}
|
|
5309
|
+
const center = graph.nodes.find((n) => n.id === nodeId);
|
|
5310
|
+
if (!center) {
|
|
5311
|
+
return err(`Node "${nodeId}" not found in ${targetLayer} layer.`);
|
|
5312
|
+
}
|
|
5313
|
+
const result = reverseNeighborhood(graph, nodeId, hops, direction);
|
|
5314
|
+
const affected = [];
|
|
5315
|
+
for (const [id, { node, hop }] of result.nodes) {
|
|
5316
|
+
if (id === nodeId) continue;
|
|
5317
|
+
const tags = node.tags;
|
|
5318
|
+
affected.push({
|
|
5319
|
+
id: node.id,
|
|
5320
|
+
name: node.name,
|
|
5321
|
+
type: node.type,
|
|
5322
|
+
layer: targetLayer,
|
|
5323
|
+
hop,
|
|
5324
|
+
module: tags?.module
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
|
|
5328
|
+
for (const otherLayer of otherLayers) {
|
|
5329
|
+
const otherGraph = readGraph(rootDir, otherLayer);
|
|
5330
|
+
if (!otherGraph) continue;
|
|
5331
|
+
for (const edge of otherGraph.edges) {
|
|
5332
|
+
if (edge.target === nodeId || edge.source === nodeId) {
|
|
5333
|
+
const dependentId = edge.target === nodeId ? edge.source : edge.target;
|
|
5334
|
+
if (affected.some((a) => a.id === dependentId)) continue;
|
|
5335
|
+
const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
|
|
5336
|
+
if (depNode) {
|
|
5337
|
+
const tags = depNode.tags;
|
|
5338
|
+
affected.push({
|
|
5339
|
+
id: depNode.id,
|
|
5340
|
+
name: depNode.name,
|
|
5341
|
+
type: depNode.type,
|
|
5342
|
+
layer: otherLayer,
|
|
5343
|
+
hop: 1,
|
|
5344
|
+
module: tags?.module
|
|
5345
|
+
});
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
const byLayer = {};
|
|
5351
|
+
const byHop = {};
|
|
5352
|
+
const modulesSet = /* @__PURE__ */ new Set();
|
|
5353
|
+
for (const a of affected) {
|
|
5354
|
+
byLayer[a.layer] = (byLayer[a.layer] ?? 0) + 1;
|
|
5355
|
+
byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
|
|
5356
|
+
if (a.module) modulesSet.add(a.module);
|
|
5357
|
+
}
|
|
5358
|
+
const crossesLayers = Object.keys(byLayer).length > 1;
|
|
5359
|
+
const centerTags = center.tags;
|
|
5360
|
+
return okJson({
|
|
5361
|
+
center: {
|
|
5362
|
+
id: center.id,
|
|
5363
|
+
name: center.name,
|
|
5364
|
+
type: center.type,
|
|
5365
|
+
layer: targetLayer,
|
|
5366
|
+
module: centerTags?.module
|
|
5367
|
+
},
|
|
5368
|
+
affected,
|
|
5369
|
+
summary: {
|
|
5370
|
+
total: affected.length,
|
|
5371
|
+
by_layer: byLayer,
|
|
5372
|
+
by_hop: byHop,
|
|
5373
|
+
modules_touched: Array.from(modulesSet).sort(),
|
|
5374
|
+
crosses_layers: crossesLayers
|
|
5375
|
+
}
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
function handleGenerateBlastRadius(args) {
|
|
5379
|
+
const rootDir = process.cwd();
|
|
5380
|
+
const mode = args.mode ?? "structural";
|
|
5381
|
+
const title = args.title;
|
|
5382
|
+
const description = args.description ?? title;
|
|
5383
|
+
const subtitle = args.subtitle;
|
|
5384
|
+
const hops = args.hops ?? 2;
|
|
5385
|
+
const defaults = loadDefaults(rootDir);
|
|
5386
|
+
let centerNodeIds = [];
|
|
5387
|
+
if (mode === "structural") {
|
|
5388
|
+
const nodeId = args.node_id;
|
|
5389
|
+
if (!nodeId) return err("structural mode requires node_id");
|
|
5390
|
+
centerNodeIds = [nodeId];
|
|
5391
|
+
} else {
|
|
5392
|
+
centerNodeIds = args.center_nodes ?? [];
|
|
5393
|
+
if (centerNodeIds.length === 0) return err("feature mode requires center_nodes[]");
|
|
5394
|
+
}
|
|
5395
|
+
const createNodes = args.create_nodes ?? [];
|
|
5396
|
+
const blastResults = [];
|
|
5397
|
+
for (const nodeId of centerNodeIds) {
|
|
5398
|
+
let targetLayer;
|
|
5399
|
+
const graphs = readAllGraphs(rootDir);
|
|
5400
|
+
for (const [layer, graph2] of Object.entries(graphs)) {
|
|
5401
|
+
if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
|
|
5402
|
+
targetLayer = layer;
|
|
5403
|
+
break;
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
if (!targetLayer) continue;
|
|
5407
|
+
const graph = readGraph(rootDir, targetLayer);
|
|
5408
|
+
if (!graph) continue;
|
|
5409
|
+
const center = graph.nodes.find((n) => n.id === nodeId);
|
|
5410
|
+
if (!center) continue;
|
|
5411
|
+
const result2 = reverseNeighborhood(graph, nodeId, hops, "reverse");
|
|
5412
|
+
const affected = [];
|
|
5413
|
+
for (const [id, { node, hop }] of result2.nodes) {
|
|
5414
|
+
if (id === nodeId) continue;
|
|
5415
|
+
const tags = node.tags;
|
|
5416
|
+
affected.push({ id: node.id, name: node.name, type: node.type, layer: targetLayer, hop, module: tags?.module });
|
|
5417
|
+
}
|
|
5418
|
+
const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
|
|
5419
|
+
for (const otherLayer of otherLayers) {
|
|
5420
|
+
const otherGraph = readGraph(rootDir, otherLayer);
|
|
5421
|
+
if (!otherGraph) continue;
|
|
5422
|
+
for (const edge of otherGraph.edges) {
|
|
5423
|
+
if (edge.target === nodeId || edge.source === nodeId) {
|
|
5424
|
+
const dependentId = edge.target === nodeId ? edge.source : edge.target;
|
|
5425
|
+
if (affected.some((a) => a.id === dependentId)) continue;
|
|
5426
|
+
const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
|
|
5427
|
+
if (depNode) {
|
|
5428
|
+
const tags = depNode.tags;
|
|
5429
|
+
affected.push({ id: depNode.id, name: depNode.name, type: depNode.type, layer: otherLayer, hop: 1, module: tags?.module });
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
const centerTags = center.tags;
|
|
5435
|
+
const edges = result2.edges.map((e) => ({ source: e.source, target: e.target }));
|
|
5436
|
+
blastResults.push({
|
|
5437
|
+
center: { id: center.id, name: center.name, type: center.type, layer: targetLayer, module: centerTags?.module },
|
|
5438
|
+
affected,
|
|
5439
|
+
edges
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
if (blastResults.length === 0) {
|
|
5443
|
+
return err(`None of the center nodes were found in any graph layer: ${centerNodeIds.join(", ")}`);
|
|
5444
|
+
}
|
|
5445
|
+
const inspectData = {};
|
|
5446
|
+
const allAffectedIds = /* @__PURE__ */ new Set();
|
|
5447
|
+
for (const r of blastResults) {
|
|
5448
|
+
allAffectedIds.add(r.center.id);
|
|
5449
|
+
for (const a of r.affected) allAffectedIds.add(a.id);
|
|
5450
|
+
}
|
|
5451
|
+
const allGraphs = readAllGraphs(rootDir);
|
|
5452
|
+
for (const id of allAffectedIds) {
|
|
5453
|
+
for (const [, graph] of Object.entries(allGraphs)) {
|
|
5454
|
+
if (!graph) continue;
|
|
5455
|
+
const node = graph.nodes.find((n) => n.id === id);
|
|
5456
|
+
if (node) {
|
|
5457
|
+
inspectData[id] = {
|
|
5458
|
+
type: node.type,
|
|
5459
|
+
name: node.name,
|
|
5460
|
+
methods: node.methods,
|
|
5461
|
+
path: node.path ?? node.handler,
|
|
5462
|
+
auth: node.auth,
|
|
5463
|
+
db_models: node.db_models
|
|
5464
|
+
};
|
|
5465
|
+
break;
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
const manifest = buildManifest({
|
|
5470
|
+
mode,
|
|
5471
|
+
title,
|
|
5472
|
+
description,
|
|
5473
|
+
subtitle,
|
|
5474
|
+
blastResults,
|
|
5475
|
+
createNodes,
|
|
5476
|
+
inspectData,
|
|
5477
|
+
defaults
|
|
5478
|
+
});
|
|
5479
|
+
const pushToDeck = args.push_to_deck;
|
|
5480
|
+
const session = args.session;
|
|
5481
|
+
let deckResult;
|
|
5482
|
+
if (pushToDeck) {
|
|
5483
|
+
if (!session) return err("push_to_deck requires a session name");
|
|
5484
|
+
const deckLockPath = (0, import_node_path22.join)(rootDir, ".launchsecure", "launch-deck.lock");
|
|
5485
|
+
if (!(0, import_node_fs20.existsSync)(deckLockPath)) {
|
|
5486
|
+
deckResult = { pushed: false, reason: "Deck server not running (no lock file). Push manually via deck tool." };
|
|
5487
|
+
} else {
|
|
5488
|
+
try {
|
|
5489
|
+
const lock = JSON.parse((0, import_node_fs20.readFileSync)(deckLockPath, "utf-8"));
|
|
5490
|
+
const deckUrl = lock.url;
|
|
5491
|
+
const body = JSON.stringify({
|
|
5492
|
+
session,
|
|
5493
|
+
mode: "show",
|
|
5494
|
+
blocks: [{ type: "blast-radius", label: title, manifest }]
|
|
5495
|
+
});
|
|
5496
|
+
(0, import_node_child_process2.execFileSync)("curl", [
|
|
5497
|
+
"-s",
|
|
5498
|
+
"-X",
|
|
5499
|
+
"POST",
|
|
5500
|
+
deckUrl + "/api/deck",
|
|
5501
|
+
"-H",
|
|
5502
|
+
"Content-Type: application/json",
|
|
5503
|
+
"-d",
|
|
5504
|
+
body
|
|
5505
|
+
], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
5506
|
+
deckResult = { pushed: true, session, url: deckUrl };
|
|
5507
|
+
} catch (e) {
|
|
5508
|
+
deckResult = { pushed: false, reason: `Failed to push to deck: ${e}` };
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
const result = { ...manifest };
|
|
5513
|
+
if (deckResult) result._deck = deckResult;
|
|
5514
|
+
return okJson(result);
|
|
5515
|
+
}
|
|
5051
5516
|
function layerSummary(graph) {
|
|
5052
5517
|
const typeCounts = {};
|
|
5053
5518
|
const moduleCounts = {};
|
|
@@ -5276,12 +5741,12 @@ function handleReadGraph(args) {
|
|
|
5276
5741
|
return okJson(result);
|
|
5277
5742
|
}
|
|
5278
5743
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
5279
|
-
if (layer === "ui" || layer === "api") return (0,
|
|
5280
|
-
if (layer === "db") return (0,
|
|
5281
|
-
const withSrc = (0,
|
|
5282
|
-
if ((0,
|
|
5283
|
-
const direct = (0,
|
|
5284
|
-
if ((0,
|
|
5744
|
+
if (layer === "ui" || layer === "api") return (0, import_node_path22.join)(rootDir, "src", nodeId);
|
|
5745
|
+
if (layer === "db") return (0, import_node_path22.join)(rootDir, "prisma", "schema.prisma");
|
|
5746
|
+
const withSrc = (0, import_node_path22.join)(rootDir, "src", nodeId);
|
|
5747
|
+
if ((0, import_node_fs20.existsSync)(withSrc)) return withSrc;
|
|
5748
|
+
const direct = (0, import_node_path22.join)(rootDir, nodeId);
|
|
5749
|
+
if ((0, import_node_fs20.existsSync)(direct)) return direct;
|
|
5285
5750
|
return null;
|
|
5286
5751
|
}
|
|
5287
5752
|
function handleInspectNode(args) {
|
|
@@ -5424,11 +5889,11 @@ function handleGrepNodes(args) {
|
|
|
5424
5889
|
let filesSearched = 0;
|
|
5425
5890
|
let truncated = false;
|
|
5426
5891
|
for (const [filePath, nodeId] of filePaths) {
|
|
5427
|
-
if (!(0,
|
|
5892
|
+
if (!(0, import_node_fs20.existsSync)(filePath)) continue;
|
|
5428
5893
|
filesSearched++;
|
|
5429
5894
|
let content;
|
|
5430
5895
|
try {
|
|
5431
|
-
content = (0,
|
|
5896
|
+
content = (0, import_node_fs20.readFileSync)(filePath, "utf-8");
|
|
5432
5897
|
} catch {
|
|
5433
5898
|
continue;
|
|
5434
5899
|
}
|
|
@@ -5493,11 +5958,11 @@ function handleStartChartServer(args) {
|
|
|
5493
5958
|
});
|
|
5494
5959
|
}
|
|
5495
5960
|
const entryPath = process.argv[1];
|
|
5496
|
-
const logDir = (0,
|
|
5497
|
-
(0,
|
|
5498
|
-
const logPath = (0,
|
|
5499
|
-
const out = (0,
|
|
5500
|
-
const err2 = (0,
|
|
5961
|
+
const logDir = (0, import_node_path22.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
5962
|
+
(0, import_node_fs20.mkdirSync)(logDir, { recursive: true });
|
|
5963
|
+
const logPath = (0, import_node_path22.join)(logDir, "launch-chart.log");
|
|
5964
|
+
const out = (0, import_node_fs20.openSync)(logPath, "a");
|
|
5965
|
+
const err2 = (0, import_node_fs20.openSync)(logPath, "a");
|
|
5501
5966
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
5502
5967
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
5503
5968
|
detached: true,
|
|
@@ -5609,31 +6074,28 @@ function handleDetectProjectStack() {
|
|
|
5609
6074
|
for (const l of p.layers) availableLayers.add(l);
|
|
5610
6075
|
}
|
|
5611
6076
|
}
|
|
5612
|
-
|
|
6077
|
+
const stats = { calls_api: 0, references_api: 0, annotations: 0 };
|
|
5613
6078
|
const uiGraph = readGraph(rootDir, "ui");
|
|
5614
6079
|
if (uiGraph) {
|
|
5615
6080
|
for (const ref of uiGraph.cross_refs ?? []) {
|
|
5616
6081
|
if (ref.type === "calls_api") stats.calls_api++;
|
|
5617
6082
|
if (ref.type === "references_api") stats.references_api++;
|
|
5618
6083
|
}
|
|
5619
|
-
for (const f of uiGraph.flagged_edges ?? []) {
|
|
5620
|
-
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
5621
|
-
}
|
|
5622
6084
|
}
|
|
5623
|
-
const srcDir = (0,
|
|
5624
|
-
if ((0,
|
|
6085
|
+
const srcDir = (0, import_node_path22.join)(rootDir, "src");
|
|
6086
|
+
if ((0, import_node_fs20.existsSync)(srcDir)) {
|
|
5625
6087
|
const scanDir = (dir) => {
|
|
5626
|
-
if (!(0,
|
|
5627
|
-
for (const entry of (0,
|
|
6088
|
+
if (!(0, import_node_fs20.existsSync)(dir)) return;
|
|
6089
|
+
for (const entry of (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true })) {
|
|
5628
6090
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
5629
|
-
const full = (0,
|
|
6091
|
+
const full = (0, import_node_path22.join)(dir, entry.name);
|
|
5630
6092
|
if (entry.isDirectory()) {
|
|
5631
6093
|
scanDir(full);
|
|
5632
6094
|
continue;
|
|
5633
6095
|
}
|
|
5634
|
-
if (![".ts", ".tsx"].includes((0,
|
|
6096
|
+
if (![".ts", ".tsx"].includes((0, import_node_path22.extname)(entry.name))) continue;
|
|
5635
6097
|
try {
|
|
5636
|
-
const content = (0,
|
|
6098
|
+
const content = (0, import_node_fs20.readFileSync)(full, "utf-8");
|
|
5637
6099
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
5638
6100
|
if (matches) stats.annotations += matches.length;
|
|
5639
6101
|
} catch {
|
|
@@ -5642,12 +6104,6 @@ function handleDetectProjectStack() {
|
|
|
5642
6104
|
};
|
|
5643
6105
|
scanDir(srcDir);
|
|
5644
6106
|
}
|
|
5645
|
-
let recommendedPrimary = "fetch-resolver";
|
|
5646
|
-
if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
|
|
5647
|
-
recommendedPrimary = "api-annotations";
|
|
5648
|
-
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
5649
|
-
recommendedPrimary = "url-literal-scanner";
|
|
5650
|
-
}
|
|
5651
6107
|
const supportedLanguages = /* @__PURE__ */ new Map();
|
|
5652
6108
|
supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
|
|
5653
6109
|
supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
|
|
@@ -5658,13 +6114,22 @@ function handleDetectProjectStack() {
|
|
|
5658
6114
|
languages,
|
|
5659
6115
|
parsers: parserResults,
|
|
5660
6116
|
available_layers: [...availableLayers],
|
|
5661
|
-
crosslayer_parsers:
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
6117
|
+
crosslayer_parsers: (() => {
|
|
6118
|
+
const descriptions = {
|
|
6119
|
+
"fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
|
|
6120
|
+
"api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
|
|
6121
|
+
"url-literal-scanner": "Finds /api/... string literals as fallback detection",
|
|
6122
|
+
"static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
|
|
6123
|
+
};
|
|
6124
|
+
const grouped = {};
|
|
6125
|
+
for (const p of registry.getCrossLayerParsers()) {
|
|
6126
|
+
const concern = p.concern ?? "api-binding";
|
|
6127
|
+
if (!grouped[concern]) grouped[concern] = [];
|
|
6128
|
+
grouped[concern].push({ id: p.id, description: descriptions[p.id] ?? p.id });
|
|
6129
|
+
}
|
|
6130
|
+
return grouped;
|
|
6131
|
+
})(),
|
|
5666
6132
|
stats,
|
|
5667
|
-
recommended_primary: recommendedPrimary,
|
|
5668
6133
|
...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
|
|
5669
6134
|
current_config: Object.keys(config).length > 0 ? config : null,
|
|
5670
6135
|
config_path: ".launchchart.json"
|
|
@@ -5745,6 +6210,14 @@ async function handleMessage(msg) {
|
|
|
5745
6210
|
respond(id ?? null, handleAuditLayer(args));
|
|
5746
6211
|
return;
|
|
5747
6212
|
}
|
|
6213
|
+
if (toolName === "blast_points") {
|
|
6214
|
+
respond(id ?? null, handleBlastPoints(args));
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
if (toolName === "generate_blast_radius") {
|
|
6218
|
+
respond(id ?? null, handleGenerateBlastRadius(args));
|
|
6219
|
+
return;
|
|
6220
|
+
}
|
|
5748
6221
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
5749
6222
|
return;
|
|
5750
6223
|
}
|
|
@@ -5780,15 +6253,16 @@ function startGraphMcpServer() {
|
|
|
5780
6253
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
5781
6254
|
`);
|
|
5782
6255
|
}
|
|
5783
|
-
var
|
|
6256
|
+
var import_node_fs20, import_node_path22, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
5784
6257
|
var init_graph_mcp = __esm({
|
|
5785
6258
|
"src/server/graph-mcp.ts"() {
|
|
5786
6259
|
"use strict";
|
|
5787
|
-
|
|
5788
|
-
|
|
6260
|
+
import_node_fs20 = require("node:fs");
|
|
6261
|
+
import_node_path22 = require("node:path");
|
|
5789
6262
|
import_node_child_process2 = require("node:child_process");
|
|
5790
6263
|
import_node_os2 = require("node:os");
|
|
5791
6264
|
init_graph();
|
|
6265
|
+
init_blast_radius_builder();
|
|
5792
6266
|
init_lockfile();
|
|
5793
6267
|
init_config();
|
|
5794
6268
|
init_parser_registry();
|
|
@@ -6070,6 +6544,116 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
6070
6544
|
},
|
|
6071
6545
|
required: ["layer"]
|
|
6072
6546
|
}
|
|
6547
|
+
},
|
|
6548
|
+
{
|
|
6549
|
+
name: "blast_points",
|
|
6550
|
+
description: `Calculate the blast radius for a node \u2014 what depends on it across all project layers. Returns reverse dependencies aggregated with hop distance and summary stats.
|
|
6551
|
+
|
|
6552
|
+
USE THIS when assessing the impact of changing a file, table, or endpoint. Replaces multiple read_graph calls with a single query that:
|
|
6553
|
+
- Traverses REVERSE edges (who imports/depends on this node)
|
|
6554
|
+
- Searches across ALL layers if layer is omitted
|
|
6555
|
+
- Returns affected nodes with hop distance, type, layer, and module
|
|
6556
|
+
- Provides a summary with counts by layer, by hop, and risk assessment
|
|
6557
|
+
|
|
6558
|
+
Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 returns all files that import middleware.ts, and all files that import THOSE files.`,
|
|
6559
|
+
inputSchema: {
|
|
6560
|
+
type: "object",
|
|
6561
|
+
properties: {
|
|
6562
|
+
node_id: {
|
|
6563
|
+
type: "string",
|
|
6564
|
+
description: "The node to analyze (file path, table name, etc.)"
|
|
6565
|
+
},
|
|
6566
|
+
layer: {
|
|
6567
|
+
type: "string",
|
|
6568
|
+
description: "Layer the node lives in (e.g. 'ui', 'api', 'db'). Omit to auto-detect by searching all layers."
|
|
6569
|
+
},
|
|
6570
|
+
hops: {
|
|
6571
|
+
type: "number",
|
|
6572
|
+
description: "Max hops to traverse outward. Default 2."
|
|
6573
|
+
},
|
|
6574
|
+
direction: {
|
|
6575
|
+
type: "string",
|
|
6576
|
+
enum: ["reverse", "both"],
|
|
6577
|
+
description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
|
|
6578
|
+
}
|
|
6579
|
+
},
|
|
6580
|
+
required: ["node_id"]
|
|
6581
|
+
}
|
|
6582
|
+
},
|
|
6583
|
+
{
|
|
6584
|
+
name: "generate_blast_radius",
|
|
6585
|
+
description: `Generate a complete BlastRadiusManifest from graph data \u2014 ready to push to deck.
|
|
6586
|
+
|
|
6587
|
+
Two modes:
|
|
6588
|
+
- **Structural**: single node changed \u2192 auto-discover what's affected via reverse BFS
|
|
6589
|
+
Example: generate_blast_radius({ mode: "structural", node_id: "CommentChannel", title: "CommentChannel refactor" })
|
|
6590
|
+
- **Feature**: new feature \u2192 multiple starting nodes + new nodes to create
|
|
6591
|
+
Example: generate_blast_radius({ mode: "feature", title: "Client Role", description: "...", center_nodes: ["CommentChannel", "ProjectMember"], create_nodes: [{ id: "ChannelMember", name: "ChannelMember table", layer: "db", reason: "..." }] })
|
|
6592
|
+
|
|
6593
|
+
Output is a BlastRadiusManifest JSON that passes directly to the deck tool's blast-radius block.
|
|
6594
|
+
Reads ring/layer/center colors from .launchsecure/blast-radius-defaults.json.
|
|
6595
|
+
Auto-generates acceptance criteria per node using inspect_node AST data.`,
|
|
6596
|
+
inputSchema: {
|
|
6597
|
+
type: "object",
|
|
6598
|
+
properties: {
|
|
6599
|
+
mode: {
|
|
6600
|
+
type: "string",
|
|
6601
|
+
enum: ["structural", "feature"],
|
|
6602
|
+
description: '"structural" = single node changed. "feature" = new feature with multiple nodes.'
|
|
6603
|
+
},
|
|
6604
|
+
title: {
|
|
6605
|
+
type: "string",
|
|
6606
|
+
description: "Title for the blast radius (shown in center node and header)."
|
|
6607
|
+
},
|
|
6608
|
+
description: {
|
|
6609
|
+
type: "string",
|
|
6610
|
+
description: "Description of the change or feature."
|
|
6611
|
+
},
|
|
6612
|
+
subtitle: {
|
|
6613
|
+
type: "string",
|
|
6614
|
+
description: "Optional subtitle shown above title in the viz."
|
|
6615
|
+
},
|
|
6616
|
+
node_id: {
|
|
6617
|
+
type: "string",
|
|
6618
|
+
description: "Structural mode only: the node being changed."
|
|
6619
|
+
},
|
|
6620
|
+
center_nodes: {
|
|
6621
|
+
type: "array",
|
|
6622
|
+
items: { type: "string" },
|
|
6623
|
+
description: "Feature mode: existing graph node IDs that are the starting points for traversal."
|
|
6624
|
+
},
|
|
6625
|
+
create_nodes: {
|
|
6626
|
+
type: "array",
|
|
6627
|
+
items: {
|
|
6628
|
+
type: "object",
|
|
6629
|
+
properties: {
|
|
6630
|
+
id: { type: "string" },
|
|
6631
|
+
name: { type: "string" },
|
|
6632
|
+
layer: { type: "string" },
|
|
6633
|
+
type: { type: "string" },
|
|
6634
|
+
reason: { type: "string" },
|
|
6635
|
+
acceptance: { type: "array", items: { type: "string" } },
|
|
6636
|
+
connects_to: { type: "array", items: { type: "string" }, description: "IDs of existing nodes this new node has FK/relationship edges to." }
|
|
6637
|
+
},
|
|
6638
|
+
required: ["id", "name", "layer", "reason"]
|
|
6639
|
+
},
|
|
6640
|
+
description: "Feature mode: new nodes that need to be created (not in graph yet)."
|
|
6641
|
+
},
|
|
6642
|
+
hops: {
|
|
6643
|
+
type: "number",
|
|
6644
|
+
description: "Max hops for traversal. Default 2. Hop 1 = modify ring, hop 2+ = ripple ring."
|
|
6645
|
+
},
|
|
6646
|
+
push_to_deck: {
|
|
6647
|
+
type: "boolean",
|
|
6648
|
+
description: "If true, pushes the manifest directly to LaunchDeck browser (requires deck server running). Default false."
|
|
6649
|
+
},
|
|
6650
|
+
session: {
|
|
6651
|
+
type: "string",
|
|
6652
|
+
description: "Session name for the deck tab. Required when push_to_deck is true."
|
|
6653
|
+
}
|
|
6654
|
+
},
|
|
6655
|
+
required: ["title"]
|
|
6656
|
+
}
|
|
6073
6657
|
}
|
|
6074
6658
|
];
|
|
6075
6659
|
COMPACT_SCHEMA = {
|
|
@@ -6136,10 +6720,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
6136
6720
|
|
|
6137
6721
|
// src/server/graph-mcp-entry.ts
|
|
6138
6722
|
var import_node_child_process3 = require("node:child_process");
|
|
6139
|
-
var import_node_fs20 = require("node:fs");
|
|
6140
|
-
var import_node_path22 = __toESM(require("node:path"));
|
|
6141
|
-
var import_node_os3 = require("node:os");
|
|
6142
6723
|
var import_node_fs21 = require("node:fs");
|
|
6724
|
+
var import_node_path23 = __toESM(require("node:path"));
|
|
6725
|
+
var import_node_os3 = require("node:os");
|
|
6726
|
+
var import_node_fs22 = require("node:fs");
|
|
6143
6727
|
init_lockfile();
|
|
6144
6728
|
function logStderr(msg) {
|
|
6145
6729
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -6155,11 +6739,11 @@ function maybeAutoServe() {
|
|
|
6155
6739
|
return;
|
|
6156
6740
|
}
|
|
6157
6741
|
try {
|
|
6158
|
-
const logDir =
|
|
6159
|
-
(0,
|
|
6160
|
-
const logPath =
|
|
6161
|
-
const out = (0,
|
|
6162
|
-
const err2 = (0,
|
|
6742
|
+
const logDir = import_node_path23.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
6743
|
+
(0, import_node_fs22.mkdirSync)(logDir, { recursive: true });
|
|
6744
|
+
const logPath = import_node_path23.default.join(logDir, "launch-chart.log");
|
|
6745
|
+
const out = (0, import_node_fs21.openSync)(logPath, "a");
|
|
6746
|
+
const err2 = (0, import_node_fs21.openSync)(logPath, "a");
|
|
6163
6747
|
const entryPath = process.argv[1];
|
|
6164
6748
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
6165
6749
|
detached: true,
|