@launchsecure/launch-kit 0.0.15 → 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/server/cli.js +616 -142
- package/dist/server/graph-mcp-entry.js +464 -38
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -5167,6 +5375,144 @@ function handleBlastPoints(args) {
|
|
|
5167
5375
|
}
|
|
5168
5376
|
});
|
|
5169
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
|
+
}
|
|
5170
5516
|
function layerSummary(graph) {
|
|
5171
5517
|
const typeCounts = {};
|
|
5172
5518
|
const moduleCounts = {};
|
|
@@ -5395,12 +5741,12 @@ function handleReadGraph(args) {
|
|
|
5395
5741
|
return okJson(result);
|
|
5396
5742
|
}
|
|
5397
5743
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
5398
|
-
if (layer === "ui" || layer === "api") return (0,
|
|
5399
|
-
if (layer === "db") return (0,
|
|
5400
|
-
const withSrc = (0,
|
|
5401
|
-
if ((0,
|
|
5402
|
-
const direct = (0,
|
|
5403
|
-
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;
|
|
5404
5750
|
return null;
|
|
5405
5751
|
}
|
|
5406
5752
|
function handleInspectNode(args) {
|
|
@@ -5543,11 +5889,11 @@ function handleGrepNodes(args) {
|
|
|
5543
5889
|
let filesSearched = 0;
|
|
5544
5890
|
let truncated = false;
|
|
5545
5891
|
for (const [filePath, nodeId] of filePaths) {
|
|
5546
|
-
if (!(0,
|
|
5892
|
+
if (!(0, import_node_fs20.existsSync)(filePath)) continue;
|
|
5547
5893
|
filesSearched++;
|
|
5548
5894
|
let content;
|
|
5549
5895
|
try {
|
|
5550
|
-
content = (0,
|
|
5896
|
+
content = (0, import_node_fs20.readFileSync)(filePath, "utf-8");
|
|
5551
5897
|
} catch {
|
|
5552
5898
|
continue;
|
|
5553
5899
|
}
|
|
@@ -5612,11 +5958,11 @@ function handleStartChartServer(args) {
|
|
|
5612
5958
|
});
|
|
5613
5959
|
}
|
|
5614
5960
|
const entryPath = process.argv[1];
|
|
5615
|
-
const logDir = (0,
|
|
5616
|
-
(0,
|
|
5617
|
-
const logPath = (0,
|
|
5618
|
-
const out = (0,
|
|
5619
|
-
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");
|
|
5620
5966
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
5621
5967
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
5622
5968
|
detached: true,
|
|
@@ -5736,20 +6082,20 @@ function handleDetectProjectStack() {
|
|
|
5736
6082
|
if (ref.type === "references_api") stats.references_api++;
|
|
5737
6083
|
}
|
|
5738
6084
|
}
|
|
5739
|
-
const srcDir = (0,
|
|
5740
|
-
if ((0,
|
|
6085
|
+
const srcDir = (0, import_node_path22.join)(rootDir, "src");
|
|
6086
|
+
if ((0, import_node_fs20.existsSync)(srcDir)) {
|
|
5741
6087
|
const scanDir = (dir) => {
|
|
5742
|
-
if (!(0,
|
|
5743
|
-
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 })) {
|
|
5744
6090
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
5745
|
-
const full = (0,
|
|
6091
|
+
const full = (0, import_node_path22.join)(dir, entry.name);
|
|
5746
6092
|
if (entry.isDirectory()) {
|
|
5747
6093
|
scanDir(full);
|
|
5748
6094
|
continue;
|
|
5749
6095
|
}
|
|
5750
|
-
if (![".ts", ".tsx"].includes((0,
|
|
6096
|
+
if (![".ts", ".tsx"].includes((0, import_node_path22.extname)(entry.name))) continue;
|
|
5751
6097
|
try {
|
|
5752
|
-
const content = (0,
|
|
6098
|
+
const content = (0, import_node_fs20.readFileSync)(full, "utf-8");
|
|
5753
6099
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
5754
6100
|
if (matches) stats.annotations += matches.length;
|
|
5755
6101
|
} catch {
|
|
@@ -5868,6 +6214,10 @@ async function handleMessage(msg) {
|
|
|
5868
6214
|
respond(id ?? null, handleBlastPoints(args));
|
|
5869
6215
|
return;
|
|
5870
6216
|
}
|
|
6217
|
+
if (toolName === "generate_blast_radius") {
|
|
6218
|
+
respond(id ?? null, handleGenerateBlastRadius(args));
|
|
6219
|
+
return;
|
|
6220
|
+
}
|
|
5871
6221
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
5872
6222
|
return;
|
|
5873
6223
|
}
|
|
@@ -5903,15 +6253,16 @@ function startGraphMcpServer() {
|
|
|
5903
6253
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
5904
6254
|
`);
|
|
5905
6255
|
}
|
|
5906
|
-
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;
|
|
5907
6257
|
var init_graph_mcp = __esm({
|
|
5908
6258
|
"src/server/graph-mcp.ts"() {
|
|
5909
6259
|
"use strict";
|
|
5910
|
-
|
|
5911
|
-
|
|
6260
|
+
import_node_fs20 = require("node:fs");
|
|
6261
|
+
import_node_path22 = require("node:path");
|
|
5912
6262
|
import_node_child_process2 = require("node:child_process");
|
|
5913
6263
|
import_node_os2 = require("node:os");
|
|
5914
6264
|
init_graph();
|
|
6265
|
+
init_blast_radius_builder();
|
|
5915
6266
|
init_lockfile();
|
|
5916
6267
|
init_config();
|
|
5917
6268
|
init_parser_registry();
|
|
@@ -6228,6 +6579,81 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
6228
6579
|
},
|
|
6229
6580
|
required: ["node_id"]
|
|
6230
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
|
+
}
|
|
6231
6657
|
}
|
|
6232
6658
|
];
|
|
6233
6659
|
COMPACT_SCHEMA = {
|
|
@@ -6294,10 +6720,10 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
|
|
|
6294
6720
|
|
|
6295
6721
|
// src/server/graph-mcp-entry.ts
|
|
6296
6722
|
var import_node_child_process3 = require("node:child_process");
|
|
6297
|
-
var import_node_fs20 = require("node:fs");
|
|
6298
|
-
var import_node_path22 = __toESM(require("node:path"));
|
|
6299
|
-
var import_node_os3 = require("node:os");
|
|
6300
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");
|
|
6301
6727
|
init_lockfile();
|
|
6302
6728
|
function logStderr(msg) {
|
|
6303
6729
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -6313,11 +6739,11 @@ function maybeAutoServe() {
|
|
|
6313
6739
|
return;
|
|
6314
6740
|
}
|
|
6315
6741
|
try {
|
|
6316
|
-
const logDir =
|
|
6317
|
-
(0,
|
|
6318
|
-
const logPath =
|
|
6319
|
-
const out = (0,
|
|
6320
|
-
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");
|
|
6321
6747
|
const entryPath = process.argv[1];
|
|
6322
6748
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
6323
6749
|
detached: true,
|
package/package.json
CHANGED