@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.
@@ -554,7 +554,7 @@ var require_claude_bridge = __commonJS({
554
554
  "use strict";
555
555
  var { spawn: spawn3 } = require("node-pty");
556
556
  var path9 = require("path");
557
- var fs9 = require("fs");
557
+ var fs10 = require("fs");
558
558
  var ClaudeBridge = class {
559
559
  constructor() {
560
560
  this.sessions = /* @__PURE__ */ new Map();
@@ -572,7 +572,7 @@ var require_claude_bridge = __commonJS({
572
572
  ];
573
573
  for (const cmd of possibleCommands) {
574
574
  try {
575
- if (fs9.existsSync(cmd) || this.commandExists(cmd)) {
575
+ if (fs10.existsSync(cmd) || this.commandExists(cmd)) {
576
576
  console.log(`Found Claude command at: ${cmd}`);
577
577
  return cmd;
578
578
  }
@@ -773,7 +773,7 @@ var require_codex_bridge = __commonJS({
773
773
  "use strict";
774
774
  var { spawn: spawn3 } = require("node-pty");
775
775
  var path9 = require("path");
776
- var fs9 = require("fs");
776
+ var fs10 = require("fs");
777
777
  var CodexBridge = class {
778
778
  constructor() {
779
779
  this.sessions = /* @__PURE__ */ new Map();
@@ -790,7 +790,7 @@ var require_codex_bridge = __commonJS({
790
790
  ];
791
791
  for (const cmd of possibleCommands) {
792
792
  try {
793
- if (fs9.existsSync(cmd) || this.commandExists(cmd)) {
793
+ if (fs10.existsSync(cmd) || this.commandExists(cmd)) {
794
794
  console.log(`Found Codex command at: ${cmd}`);
795
795
  return cmd;
796
796
  }
@@ -966,7 +966,7 @@ var require_agent_bridge = __commonJS({
966
966
  "use strict";
967
967
  var { spawn: spawn3 } = require("node-pty");
968
968
  var path9 = require("path");
969
- var fs9 = require("fs");
969
+ var fs10 = require("fs");
970
970
  var AgentBridge = class {
971
971
  constructor() {
972
972
  this.sessions = /* @__PURE__ */ new Map();
@@ -982,7 +982,7 @@ var require_agent_bridge = __commonJS({
982
982
  ];
983
983
  for (const cmd of possibleCommands) {
984
984
  try {
985
- if (fs9.existsSync(cmd) || this.commandExists(cmd)) {
985
+ if (fs10.existsSync(cmd) || this.commandExists(cmd)) {
986
986
  console.log(`Found Agent command at: ${cmd}`);
987
987
  return cmd;
988
988
  }
@@ -1306,7 +1306,7 @@ var require_script_bridge = __commonJS({
1306
1306
  var require_session_store = __commonJS({
1307
1307
  "../claude-code-web/src/utils/session-store.js"(exports2, module2) {
1308
1308
  "use strict";
1309
- var fs9 = require("fs").promises;
1309
+ var fs10 = require("fs").promises;
1310
1310
  var path9 = require("path");
1311
1311
  var os2 = require("os");
1312
1312
  var SessionStore = class {
@@ -1317,14 +1317,14 @@ var require_session_store = __commonJS({
1317
1317
  }
1318
1318
  async initializeStorage() {
1319
1319
  try {
1320
- await fs9.mkdir(this.storageDir, { recursive: true });
1320
+ await fs10.mkdir(this.storageDir, { recursive: true });
1321
1321
  } catch (error) {
1322
1322
  console.error("Failed to create storage directory:", error);
1323
1323
  }
1324
1324
  }
1325
1325
  async saveSessions(sessions) {
1326
1326
  try {
1327
- await fs9.mkdir(this.storageDir, { recursive: true });
1327
+ await fs10.mkdir(this.storageDir, { recursive: true });
1328
1328
  const sessionsArray = Array.from(sessions.entries()).map(([id, session]) => ({
1329
1329
  id,
1330
1330
  name: session.name || "Unnamed Session",
@@ -1355,9 +1355,9 @@ var require_session_store = __commonJS({
1355
1355
  sessions: sessionsArray
1356
1356
  };
1357
1357
  const tempFile = `${this.sessionsFile}.tmp`;
1358
- await fs9.writeFile(tempFile, JSON.stringify(data, null, 2));
1359
- await fs9.mkdir(this.storageDir, { recursive: true });
1360
- await fs9.rename(tempFile, this.sessionsFile);
1358
+ await fs10.writeFile(tempFile, JSON.stringify(data, null, 2));
1359
+ await fs10.mkdir(this.storageDir, { recursive: true });
1360
+ await fs10.rename(tempFile, this.sessionsFile);
1361
1361
  return true;
1362
1362
  } catch (error) {
1363
1363
  console.error("Failed to save sessions:", error.message);
@@ -1366,8 +1366,8 @@ var require_session_store = __commonJS({
1366
1366
  }
1367
1367
  async loadSessions() {
1368
1368
  try {
1369
- await fs9.access(this.sessionsFile);
1370
- const data = await fs9.readFile(this.sessionsFile, "utf8");
1369
+ await fs10.access(this.sessionsFile);
1370
+ const data = await fs10.readFile(this.sessionsFile, "utf8");
1371
1371
  if (!data || !data.trim()) {
1372
1372
  console.log("Sessions file is empty, starting fresh");
1373
1373
  return /* @__PURE__ */ new Map();
@@ -1378,7 +1378,7 @@ var require_session_store = __commonJS({
1378
1378
  } catch (parseError) {
1379
1379
  console.error("Sessions file is corrupted, starting fresh:", parseError.message);
1380
1380
  try {
1381
- await fs9.rename(this.sessionsFile, `${this.sessionsFile}.corrupted.${Date.now()}`);
1381
+ await fs10.rename(this.sessionsFile, `${this.sessionsFile}.corrupted.${Date.now()}`);
1382
1382
  } catch (renameError) {
1383
1383
  }
1384
1384
  return /* @__PURE__ */ new Map();
@@ -1422,7 +1422,7 @@ var require_session_store = __commonJS({
1422
1422
  }
1423
1423
  async clearOldSessions() {
1424
1424
  try {
1425
- await fs9.unlink(this.sessionsFile);
1425
+ await fs10.unlink(this.sessionsFile);
1426
1426
  console.log("Cleared old sessions");
1427
1427
  return true;
1428
1428
  } catch (error) {
@@ -1434,9 +1434,9 @@ var require_session_store = __commonJS({
1434
1434
  }
1435
1435
  async getSessionMetadata() {
1436
1436
  try {
1437
- await fs9.access(this.sessionsFile);
1438
- const stats = await fs9.stat(this.sessionsFile);
1439
- const data = await fs9.readFile(this.sessionsFile, "utf8");
1437
+ await fs10.access(this.sessionsFile);
1438
+ const stats = await fs10.stat(this.sessionsFile);
1439
+ const data = await fs10.readFile(this.sessionsFile, "utf8");
1440
1440
  const parsed = JSON.parse(data);
1441
1441
  return {
1442
1442
  exists: true,
@@ -1461,7 +1461,7 @@ var require_session_store = __commonJS({
1461
1461
  var require_usage_reader = __commonJS({
1462
1462
  "../claude-code-web/src/usage-reader.js"(exports2, module2) {
1463
1463
  "use strict";
1464
- var fs9 = require("fs").promises;
1464
+ var fs10 = require("fs").promises;
1465
1465
  var path9 = require("path");
1466
1466
  var readline = require("readline");
1467
1467
  var { createReadStream } = require("fs");
@@ -1669,12 +1669,12 @@ var require_usage_reader = __commonJS({
1669
1669
  const projectDirName = cwd.replace(/\//g, "-");
1670
1670
  let projectPath = path9.join(this.claudeProjectsPath, projectDirName);
1671
1671
  try {
1672
- await fs9.access(projectPath);
1672
+ await fs10.access(projectPath);
1673
1673
  } catch (err2) {
1674
1674
  console.log(`Project directory not found: ${projectPath}`);
1675
1675
  return null;
1676
1676
  }
1677
- const files = await fs9.readdir(projectPath);
1677
+ const files = await fs10.readdir(projectPath);
1678
1678
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1679
1679
  if (jsonlFiles.length === 0) {
1680
1680
  return null;
@@ -1683,7 +1683,7 @@ var require_usage_reader = __commonJS({
1683
1683
  let mostRecentTime = 0;
1684
1684
  for (const file of jsonlFiles) {
1685
1685
  const filePath = path9.join(projectPath, file);
1686
- const stat = await fs9.stat(filePath);
1686
+ const stat = await fs10.stat(filePath);
1687
1687
  if (stat.mtime.getTime() > mostRecentTime) {
1688
1688
  mostRecentTime = stat.mtime.getTime();
1689
1689
  mostRecentFile = filePath;
@@ -1698,17 +1698,17 @@ var require_usage_reader = __commonJS({
1698
1698
  async findJsonlFiles(onlyRecent = false) {
1699
1699
  const files = [];
1700
1700
  try {
1701
- const projectDirs = await fs9.readdir(this.claudeProjectsPath);
1701
+ const projectDirs = await fs10.readdir(this.claudeProjectsPath);
1702
1702
  for (const projectDir of projectDirs) {
1703
1703
  const projectPath = path9.join(this.claudeProjectsPath, projectDir);
1704
- const stat = await fs9.stat(projectPath);
1704
+ const stat = await fs10.stat(projectPath);
1705
1705
  if (stat.isDirectory()) {
1706
- const projectFiles = await fs9.readdir(projectPath);
1706
+ const projectFiles = await fs10.readdir(projectPath);
1707
1707
  const jsonlFiles = projectFiles.filter((f) => f.endsWith(".jsonl"));
1708
1708
  for (const jsonlFile of jsonlFiles) {
1709
1709
  const filePath = path9.join(projectPath, jsonlFile);
1710
1710
  if (onlyRecent) {
1711
- const fileStat = await fs9.stat(filePath);
1711
+ const fileStat = await fs10.stat(filePath);
1712
1712
  const hoursSinceModified = (Date.now() - fileStat.mtime.getTime()) / (1e3 * 60 * 60);
1713
1713
  if (hoursSinceModified <= 24) {
1714
1714
  files.push(filePath);
@@ -1871,7 +1871,7 @@ var require_usage_reader = __commonJS({
1871
1871
  }
1872
1872
  const sessionFile = path9.join(this.claudeProjectsPath, path9.basename(process.cwd()).replace(/[^a-zA-Z0-9-]/g, "-"), `${sessionId}.jsonl`);
1873
1873
  try {
1874
- await fs9.access(sessionFile);
1874
+ await fs10.access(sessionFile);
1875
1875
  } catch (err2) {
1876
1876
  return null;
1877
1877
  }
@@ -2537,7 +2537,7 @@ var require_src = __commonJS({
2537
2537
  var SessionStore = require_session_store();
2538
2538
  var UsageReader = require_usage_reader();
2539
2539
  var UsageAnalytics = require_usage_analytics();
2540
- var fs9 = require("fs");
2540
+ var fs10 = require("fs");
2541
2541
  function stripAnsi(str) {
2542
2542
  return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b[()][AB012]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/\r\n?/g, "\n").trim();
2543
2543
  }
@@ -2795,7 +2795,7 @@ var require_src = __commonJS({
2795
2795
  return true;
2796
2796
  }
2797
2797
  try {
2798
- const items = fs9.readdirSync(validation.path, { withFileTypes: true });
2798
+ const items = fs10.readdirSync(validation.path, { withFileTypes: true });
2799
2799
  const showHidden = url.searchParams.get("showHidden") === "true";
2800
2800
  const folders = items.filter((item) => item.isDirectory()).filter((item) => !item.name.startsWith(".") || showHidden).map((item) => ({
2801
2801
  name: item.name,
@@ -2830,7 +2830,7 @@ var require_src = __commonJS({
2830
2830
  res.end(JSON.stringify({ error: validation.error }));
2831
2831
  return;
2832
2832
  }
2833
- if (!fs9.existsSync(validation.path) || !fs9.statSync(validation.path).isDirectory()) {
2833
+ if (!fs10.existsSync(validation.path) || !fs10.statSync(validation.path).isDirectory()) {
2834
2834
  res.writeHead(400, { "Content-Type": "application/json" });
2835
2835
  res.end(JSON.stringify({ error: "Not a valid directory" }));
2836
2836
  return;
@@ -2857,7 +2857,7 @@ var require_src = __commonJS({
2857
2857
  res.end(JSON.stringify({ error: validation.error }));
2858
2858
  return;
2859
2859
  }
2860
- if (!fs9.existsSync(validation.path) || !fs9.statSync(validation.path).isDirectory()) {
2860
+ if (!fs10.existsSync(validation.path) || !fs10.statSync(validation.path).isDirectory()) {
2861
2861
  res.writeHead(400, { "Content-Type": "application/json" });
2862
2862
  res.end(JSON.stringify({ error: "Invalid directory path" }));
2863
2863
  return;
@@ -2892,12 +2892,12 @@ var require_src = __commonJS({
2892
2892
  res.end(JSON.stringify({ message: "Cannot create folder outside the allowed area" }));
2893
2893
  return;
2894
2894
  }
2895
- if (fs9.existsSync(fullValidation.path)) {
2895
+ if (fs10.existsSync(fullValidation.path)) {
2896
2896
  res.writeHead(409, { "Content-Type": "application/json" });
2897
2897
  res.end(JSON.stringify({ message: "Folder already exists" }));
2898
2898
  return;
2899
2899
  }
2900
- fs9.mkdirSync(fullValidation.path, { recursive: true });
2900
+ fs10.mkdirSync(fullValidation.path, { recursive: true });
2901
2901
  res.writeHead(200, { "Content-Type": "application/json" });
2902
2902
  res.end(JSON.stringify({ success: true, path: fullValidation.path }));
2903
2903
  } catch (e) {
@@ -2923,7 +2923,7 @@ var require_src = __commonJS({
2923
2923
  if (!filePath || filePath === "") filePath = "index.html";
2924
2924
  const fullPath = path9.join(__dirname, "public", filePath);
2925
2925
  try {
2926
- if (!fs9.existsSync(fullPath)) return false;
2926
+ if (!fs10.existsSync(fullPath)) return false;
2927
2927
  const ext = path9.extname(fullPath);
2928
2928
  const mimeTypes = {
2929
2929
  ".html": "text/html",
@@ -2935,7 +2935,7 @@ var require_src = __commonJS({
2935
2935
  ".ico": "image/x-icon"
2936
2936
  };
2937
2937
  const mime = mimeTypes[ext] || "application/octet-stream";
2938
- const content = fs9.readFileSync(fullPath);
2938
+ const content = fs10.readFileSync(fullPath);
2939
2939
  res.writeHead(200, { "Content-Type": mime });
2940
2940
  res.end(content);
2941
2941
  return true;
@@ -4363,6 +4363,7 @@ function shutdownTerminalBridge() {
4363
4363
  // src/server/mcp-config-writer.ts
4364
4364
  var import_fs = __toESM(require("fs"));
4365
4365
  var import_path = __toESM(require("path"));
4366
+ var MANAGED_SERVERS = ["launch-pod", "launch-chart"];
4366
4367
  var writtenPaths = [];
4367
4368
  function writeMcpConfigs(opts) {
4368
4369
  const { projectDir, token, serverUrl } = opts;
@@ -4373,7 +4374,10 @@ function writeMcpConfigs(opts) {
4373
4374
  function removeMcpConfigs() {
4374
4375
  for (const filePath of writtenPaths) {
4375
4376
  try {
4376
- if (import_fs.default.existsSync(filePath)) {
4377
+ if (!import_fs.default.existsSync(filePath)) continue;
4378
+ if (filePath.endsWith(".mcp.json")) {
4379
+ removeClaudeEntries(filePath);
4380
+ } else {
4377
4381
  import_fs.default.unlinkSync(filePath);
4378
4382
  console.log(`MCP config removed: ${filePath}`);
4379
4383
  }
@@ -4382,46 +4386,88 @@ function removeMcpConfigs() {
4382
4386
  }
4383
4387
  writtenPaths.length = 0;
4384
4388
  }
4389
+ function removeClaudeEntries(filePath) {
4390
+ try {
4391
+ const raw = import_fs.default.readFileSync(filePath, "utf-8");
4392
+ const existing = JSON.parse(raw);
4393
+ const servers = existing.mcpServers ?? {};
4394
+ for (const key of MANAGED_SERVERS) {
4395
+ delete servers[key];
4396
+ }
4397
+ if (Object.keys(servers).length === 0 && Object.keys(existing).length <= 1) {
4398
+ import_fs.default.unlinkSync(filePath);
4399
+ console.log(`MCP config removed (empty): ${filePath}`);
4400
+ } else {
4401
+ existing.mcpServers = servers;
4402
+ import_fs.default.writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
4403
+ console.log(`MCP config cleaned (LaunchPod entries removed): ${filePath}`);
4404
+ }
4405
+ } catch {
4406
+ try {
4407
+ import_fs.default.unlinkSync(filePath);
4408
+ } catch {
4409
+ }
4410
+ console.log(`MCP config removed: ${filePath}`);
4411
+ }
4412
+ }
4385
4413
  function writeClaudeConfig(projectDir, mcpUrl, token) {
4386
- const config = {
4387
- mcpServers: {
4388
- "launch-pod": {
4389
- type: "http",
4390
- url: mcpUrl,
4391
- headers: {
4392
- Authorization: `Bearer ${token}`
4393
- }
4394
- },
4395
- "launch-chart": {
4396
- command: "launch-chart",
4397
- args: []
4398
- }
4414
+ const filePath = import_path.default.join(projectDir, ".mcp.json");
4415
+ let existing = {};
4416
+ try {
4417
+ if (import_fs.default.existsSync(filePath)) {
4418
+ existing = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
4419
+ }
4420
+ } catch {
4421
+ console.warn(`Warning: existing .mcp.json was malformed, preserving as .mcp.json.bak`);
4422
+ try {
4423
+ import_fs.default.copyFileSync(filePath, filePath + ".bak");
4424
+ } catch {
4425
+ }
4426
+ }
4427
+ const servers = existing.mcpServers ?? {};
4428
+ servers["launch-pod"] = {
4429
+ type: "http",
4430
+ url: mcpUrl,
4431
+ headers: {
4432
+ Authorization: `Bearer ${token}`
4399
4433
  }
4400
4434
  };
4401
- const filePath = import_path.default.join(projectDir, ".mcp.json");
4402
- import_fs.default.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
4435
+ servers["launch-chart"] = {
4436
+ command: "launch-chart",
4437
+ args: []
4438
+ };
4439
+ existing.mcpServers = servers;
4440
+ import_fs.default.writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
4403
4441
  writtenPaths.push(filePath);
4404
- console.log(`Claude Code MCP config written to ${filePath}`);
4442
+ console.log(`Claude Code MCP config merged into ${filePath}`);
4405
4443
  }
4406
4444
  function writeCodexConfig(projectDir, mcpUrl, token) {
4407
4445
  const codexDir = import_path.default.join(projectDir, ".codex");
4408
4446
  if (!import_fs.default.existsSync(codexDir)) {
4409
4447
  import_fs.default.mkdirSync(codexDir, { recursive: true, mode: 448 });
4410
4448
  }
4411
- const toml = [
4449
+ const filePath = import_path.default.join(codexDir, "config.toml");
4450
+ let existingContent = "";
4451
+ try {
4452
+ if (import_fs.default.existsSync(filePath)) {
4453
+ existingContent = import_fs.default.readFileSync(filePath, "utf-8");
4454
+ }
4455
+ } catch {
4456
+ }
4457
+ const cleaned = existingContent.replace(/\[mcp_servers\.launch-pod\][^\[]*/, "").replace(/\[mcp_servers\.launch-chart\][^\[]*/, "").trim();
4458
+ const launchPodBlock = [
4412
4459
  `[mcp_servers.launch-pod]`,
4413
4460
  `url = "${mcpUrl}"`,
4414
4461
  `http_headers = { "Authorization" = "Bearer ${token}" }`,
4415
4462
  ``,
4416
4463
  `[mcp_servers.launch-chart]`,
4417
4464
  `command = "launch-chart"`,
4418
- `args = []`,
4419
- ``
4465
+ `args = []`
4420
4466
  ].join("\n");
4421
- const filePath = import_path.default.join(codexDir, "config.toml");
4422
- import_fs.default.writeFileSync(filePath, toml, { mode: 384 });
4467
+ const merged = cleaned ? cleaned + "\n\n" + launchPodBlock + "\n" : launchPodBlock + "\n";
4468
+ import_fs.default.writeFileSync(filePath, merged, { mode: 384 });
4423
4469
  writtenPaths.push(filePath);
4424
- console.log(`Codex MCP config written to ${filePath}`);
4470
+ console.log(`Codex MCP config merged into ${filePath}`);
4425
4471
  }
4426
4472
 
4427
4473
  // src/server/tracker-poller.ts
@@ -4528,13 +4574,18 @@ All pipeline data lives in the **database**, not on the local filesystem.
4528
4574
  ---
4529
4575
 
4530
4576
  `;
4531
- function resolvePrompt(agentId, unitId, inputs) {
4532
- const hyphenated = agentId.replace(/_/g, "-");
4533
- const filePath = import_path2.default.join(PROMPTS_DIR, `${hyphenated}.md`);
4534
- if (!import_fs2.default.existsSync(filePath)) {
4535
- throw new Error(`Prompt file not found for agent "${agentId}": ${filePath}`);
4577
+ function resolvePrompt(agentId, unitId, inputs, promptOverride) {
4578
+ let raw;
4579
+ if (promptOverride) {
4580
+ raw = promptOverride;
4581
+ } else {
4582
+ const hyphenated = agentId.replace(/_/g, "-");
4583
+ const filePath = import_path2.default.join(PROMPTS_DIR, `${hyphenated}.md`);
4584
+ if (!import_fs2.default.existsSync(filePath)) {
4585
+ throw new Error(`Prompt file not found for agent "${agentId}": ${filePath}`);
4586
+ }
4587
+ raw = import_fs2.default.readFileSync(filePath, "utf-8");
4536
4588
  }
4537
- const raw = import_fs2.default.readFileSync(filePath, "utf-8");
4538
4589
  const unitPreamble = unitId ? `
4539
4590
  ---
4540
4591
 
@@ -4653,7 +4704,7 @@ var NodeTriggerController = class {
4653
4704
  async startSession(agentId, agent, unitId) {
4654
4705
  const key = this.sessionKey(agentId, unitId);
4655
4706
  try {
4656
- const prompt = resolvePrompt(agentId, unitId, agent.in);
4707
+ const prompt = resolvePrompt(agentId, unitId, agent.in, agent.po);
4657
4708
  const sessionId = await this.adapter.start({
4658
4709
  agentId,
4659
4710
  prompt,
@@ -6906,19 +6957,26 @@ init_config();
6906
6957
  // src/server/graph/core/resolve-paths.ts
6907
6958
  var import_node_fs2 = require("node:fs");
6908
6959
  var import_node_path2 = require("node:path");
6960
+ function detectDbDir(rootDir, config) {
6961
+ if (config.paths?.dbDir) return (0, import_node_path2.join)(rootDir, config.paths.dbDir);
6962
+ const prismaDir = (0, import_node_path2.join)(rootDir, "prisma");
6963
+ if ((0, import_node_fs2.existsSync)(prismaDir)) return prismaDir;
6964
+ return null;
6965
+ }
6909
6966
  function resolveProjectPaths(rootDir, config) {
6967
+ const dbDir = detectDbDir(rootDir, config);
6910
6968
  if (config.paths?.appDir) {
6911
6969
  const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
6912
6970
  const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
6913
- return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api") };
6971
+ return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api"), dbDir };
6914
6972
  }
6915
6973
  const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
6916
6974
  if ((0, import_node_fs2.existsSync)(srcApp)) {
6917
- return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api") };
6975
+ return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api"), dbDir };
6918
6976
  }
6919
6977
  const rootApp = (0, import_node_path2.join)(rootDir, "app");
6920
6978
  if ((0, import_node_fs2.existsSync)(rootApp)) {
6921
- return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api") };
6979
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api"), dbDir };
6922
6980
  }
6923
6981
  return null;
6924
6982
  }
@@ -8282,6 +8340,7 @@ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
8282
8340
  var fetchResolverParser = {
8283
8341
  id: "fetch-resolver",
8284
8342
  layer: "crosslayer",
8343
+ concern: "api-binding",
8285
8344
  detect(_rootDir) {
8286
8345
  return true;
8287
8346
  },
@@ -8395,6 +8454,7 @@ function toNodeId2(srcDir, absPath) {
8395
8454
  var apiAnnotationsParser = {
8396
8455
  id: "api-annotations",
8397
8456
  layer: "crosslayer",
8457
+ concern: "api-binding",
8398
8458
  detect(rootDir) {
8399
8459
  return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
8400
8460
  },
@@ -8481,6 +8541,7 @@ function toNodeId3(srcDir, absPath) {
8481
8541
  var urlLiteralScannerParser = {
8482
8542
  id: "url-literal-scanner",
8483
8543
  layer: "crosslayer",
8544
+ concern: "api-binding",
8484
8545
  detect(rootDir) {
8485
8546
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
8486
8547
  return paths !== null;
@@ -9092,6 +9153,7 @@ function collectStaticRefsRegex(content, valueLookup, allValues) {
9092
9153
  var staticRefScannerParser = {
9093
9154
  id: "static-ref-scanner",
9094
9155
  layer: "crosslayer",
9156
+ concern: "static-ref",
9095
9157
  detect(rootDir) {
9096
9158
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9097
9159
  return paths !== null;
@@ -9276,6 +9338,9 @@ function loadCustomParsers(registry, config, rootDir, disabled) {
9276
9338
  `
9277
9339
  );
9278
9340
  }
9341
+ if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
9342
+ parser.concern = entry.concern;
9343
+ }
9279
9344
  registry.register(parser);
9280
9345
  } catch (err2) {
9281
9346
  process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
@@ -9373,44 +9438,21 @@ function dedupCrossRefs(refs) {
9373
9438
  }
9374
9439
  return result;
9375
9440
  }
9376
- function applyCrossLayerResults(uiOutput, results, primaryId) {
9377
- const allCrossRefs = [...uiOutput.cross_refs];
9378
- const allFlagged = [...uiOutput.flagged_edges];
9379
- const allWarnings = [...uiOutput.warnings];
9380
- const primaryResult = results.find((r) => r.parserId === primaryId);
9381
- const secondaryResults = results.filter((r) => r.parserId !== primaryId);
9382
- if (primaryResult) {
9383
- allCrossRefs.push(...primaryResult.output.cross_refs);
9384
- allFlagged.push(...primaryResult.output.flagged_edges);
9385
- allWarnings.push(...primaryResult.output.warnings);
9386
- }
9387
- const primarySet = new Set(
9388
- (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
9389
- );
9390
- for (const sec of secondaryResults) {
9391
- for (const ref of sec.output.cross_refs) {
9392
- const key = `${ref.source}|${ref.target}|${ref.type}`;
9393
- if (primarySet.has(key)) {
9394
- allCrossRefs.push(ref);
9395
- } else {
9396
- allFlagged.push({
9397
- source: ref.source,
9398
- target: ref.target,
9399
- type: "out_of_pattern",
9400
- label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
9401
- confidence: "medium"
9402
- });
9403
- allCrossRefs.push(ref);
9404
- }
9405
- }
9406
- allFlagged.push(...sec.output.flagged_edges);
9407
- allWarnings.push(...sec.output.warnings);
9408
- }
9441
+ function applyCrossLayerResults(uiOutput, results) {
9409
9442
  return {
9410
9443
  ...uiOutput,
9411
- cross_refs: dedupCrossRefs(allCrossRefs),
9412
- flagged_edges: allFlagged,
9413
- warnings: allWarnings
9444
+ cross_refs: dedupCrossRefs([
9445
+ ...uiOutput.cross_refs,
9446
+ ...results.flatMap((r) => r.output.cross_refs)
9447
+ ]),
9448
+ flagged_edges: [
9449
+ ...uiOutput.flagged_edges,
9450
+ ...results.flatMap((r) => r.output.flagged_edges)
9451
+ ],
9452
+ warnings: [
9453
+ ...uiOutput.warnings,
9454
+ ...results.flatMap((r) => r.output.warnings)
9455
+ ]
9414
9456
  };
9415
9457
  }
9416
9458
 
@@ -9449,10 +9491,9 @@ function generateLayer(rootDir, layer) {
9449
9491
  if (existing) layerOutputs.set(otherLayer, existing);
9450
9492
  }
9451
9493
  const crossParsers = registry.getCrossLayerParsers();
9452
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
9453
9494
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
9454
9495
  if (crossResults.length > 0) {
9455
- merged = applyCrossLayerResults(merged, crossResults, primaryId);
9496
+ merged = applyCrossLayerResults(merged, crossResults);
9456
9497
  }
9457
9498
  }
9458
9499
  return {
@@ -9499,11 +9540,10 @@ function generateAll(rootDir) {
9499
9540
  });
9500
9541
  }
9501
9542
  const crossParsers = registry.getCrossLayerParsers();
9502
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
9503
9543
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
9504
9544
  if (crossResults.length > 0 && layerOutputs.has("ui")) {
9505
9545
  const uiOutput = layerOutputs.get("ui");
9506
- const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
9546
+ const merged = applyCrossLayerResults(uiOutput, crossResults);
9507
9547
  layerOutputs.set("ui", merged);
9508
9548
  const uiResult = results.find((r) => r.layer === "ui");
9509
9549
  if (uiResult) {
@@ -10110,35 +10150,237 @@ async function handleGraphCommand(subcommand, args) {
10110
10150
  }
10111
10151
 
10112
10152
  // src/server/graph-mcp.ts
10113
- var import_node_fs18 = require("node:fs");
10114
- var import_node_path20 = require("node:path");
10153
+ var import_node_fs19 = require("node:fs");
10154
+ var import_node_path21 = require("node:path");
10115
10155
  var import_node_child_process2 = require("node:child_process");
10116
10156
  var import_node_os2 = require("node:os");
10117
10157
 
10158
+ // src/server/blast-radius-builder.ts
10159
+ var import_node_fs15 = __toESM(require("node:fs"));
10160
+ var import_node_path17 = require("node:path");
10161
+ var FALLBACK_DEFAULTS = {
10162
+ rings: [
10163
+ { id: "modify", name: "Modify", color: "#ff6b00" },
10164
+ { id: "ripple", name: "Ripple (verify)", color: "#ffff00" },
10165
+ { id: "create", name: "Create", color: "#00ff00" }
10166
+ ],
10167
+ layers: {
10168
+ db: { name: "Database", icon: "database", color: "#cbd5e1" },
10169
+ api: { name: "API", icon: "server", color: "#cbd5e1" },
10170
+ middleware: { name: "Middleware", icon: "shield", color: "#cbd5e1" },
10171
+ ui: { name: "UI", icon: "layout-dashboard", color: "#cbd5e1" },
10172
+ config: { name: "Config / Seed", icon: "settings", color: "#cbd5e1" },
10173
+ shared: { name: "Shared Types", icon: "box", color: "#cbd5e1" }
10174
+ },
10175
+ center: { color: "#ff0000" }
10176
+ };
10177
+ function loadDefaults(rootDir) {
10178
+ const filePath = (0, import_node_path17.join)(rootDir, ".launchsecure", "blast-radius-defaults.json");
10179
+ try {
10180
+ if (import_node_fs15.default.existsSync(filePath)) {
10181
+ const raw = import_node_fs15.default.readFileSync(filePath, "utf-8");
10182
+ return JSON.parse(raw);
10183
+ }
10184
+ } catch {
10185
+ }
10186
+ return FALLBACK_DEFAULTS;
10187
+ }
10188
+ function generateAcceptance(node, inspect) {
10189
+ const criteria = [];
10190
+ const t = node.type?.toLowerCase() ?? "";
10191
+ if (t === "endpoint" || t === "mcp-tool") {
10192
+ const methods = inspect?.methods ?? [];
10193
+ const path9 = inspect?.path ?? node.id;
10194
+ if (methods.length > 0) {
10195
+ criteria.push(`${methods.join("/")} ${path9} still returns correct responses for authorized users`);
10196
+ } else {
10197
+ criteria.push(`${path9} still responds correctly`);
10198
+ }
10199
+ if (inspect?.auth && inspect.auth.includes("withAuth")) {
10200
+ criteria.push("Authentication and authorization still enforced");
10201
+ }
10202
+ if (inspect?.db_models && inspect.db_models.length > 0) {
10203
+ criteria.push(`DB operations on ${inspect.db_models.join(", ")} still work correctly`);
10204
+ }
10205
+ } else if (t === "page" || t === "component" || t === "layout") {
10206
+ criteria.push(`${node.name} renders without errors`);
10207
+ if (inspect?.stateVars && inspect.stateVars.length > 0) {
10208
+ criteria.push("State management still works correctly");
10209
+ }
10210
+ if (inspect?.elements && inspect.elements.length > 5) {
10211
+ criteria.push("All child components render correctly");
10212
+ }
10213
+ } else if (t === "table" || t === "enum") {
10214
+ criteria.push(`${node.name} schema unchanged or migration applies cleanly`);
10215
+ criteria.push("Existing queries against this table still work");
10216
+ } else if (t === "hook") {
10217
+ criteria.push(`${node.name} returns expected shape`);
10218
+ if (inspect?.stateVars && inspect.stateVars.length > 0) {
10219
+ criteria.push(`State variables [${inspect.stateVars.map((s) => s.name).join(", ")}] still returned`);
10220
+ }
10221
+ } else if (t === "context") {
10222
+ criteria.push(`${node.name} provides correct context to consumers`);
10223
+ } else if (t === "lib" || t === "config" || t === "types") {
10224
+ criteria.push(`${node.name} exports still conform to expected interface`);
10225
+ } else if (t === "seed" || t === "seed_role" || t === "seed_permission") {
10226
+ criteria.push("Seed runs without errors");
10227
+ criteria.push("Expected rows created in database");
10228
+ } else {
10229
+ criteria.push("Verify no regression");
10230
+ }
10231
+ return criteria;
10232
+ }
10233
+ function buildManifest(input) {
10234
+ const { mode, title, description, subtitle, blastResults, createNodes, inspectData, defaults } = input;
10235
+ const nodeMap = /* @__PURE__ */ new Map();
10236
+ const centerNodeIds = /* @__PURE__ */ new Set();
10237
+ for (const result of blastResults) {
10238
+ centerNodeIds.add(result.center.id);
10239
+ for (const node of result.affected) {
10240
+ const existing = nodeMap.get(node.id);
10241
+ if (!existing || node.hop < existing.hop) {
10242
+ nodeMap.set(node.id, node);
10243
+ }
10244
+ }
10245
+ }
10246
+ for (const id of centerNodeIds) {
10247
+ nodeMap.delete(id);
10248
+ }
10249
+ const manifestNodes = [];
10250
+ for (const result of blastResults) {
10251
+ const c = result.center;
10252
+ if (manifestNodes.some((n) => n.id === c.id)) continue;
10253
+ const inspect = inspectData[c.id];
10254
+ manifestNodes.push({
10255
+ id: c.id,
10256
+ name: c.name,
10257
+ layer: c.layer,
10258
+ ring: "modify",
10259
+ type: c.type,
10260
+ reason: `Direct change target`,
10261
+ acceptance: generateAcceptance(
10262
+ { id: c.id, name: c.name, type: c.type, layer: c.layer, hop: 0 },
10263
+ inspect
10264
+ )
10265
+ });
10266
+ }
10267
+ for (const [, node] of nodeMap) {
10268
+ const ring = node.hop <= 1 ? "modify" : "ripple";
10269
+ const inspect = inspectData[node.id];
10270
+ const reason = node.hop <= 1 ? `Directly depends on changed node` : `Indirect dependency (${node.hop} hops away)`;
10271
+ manifestNodes.push({
10272
+ id: node.id,
10273
+ name: node.name,
10274
+ layer: node.layer,
10275
+ ring,
10276
+ type: node.type,
10277
+ reason,
10278
+ acceptance: generateAcceptance(node, inspect)
10279
+ });
10280
+ }
10281
+ for (const cn of createNodes) {
10282
+ manifestNodes.push({
10283
+ id: cn.id,
10284
+ name: cn.name,
10285
+ layer: cn.layer,
10286
+ ring: "create",
10287
+ type: cn.type ?? "unknown",
10288
+ reason: cn.reason,
10289
+ acceptance: cn.acceptance ?? ["Verify implementation matches spec"]
10290
+ });
10291
+ }
10292
+ const layerIds = /* @__PURE__ */ new Set();
10293
+ for (const n of manifestNodes) {
10294
+ layerIds.add(n.layer);
10295
+ }
10296
+ const layers = [];
10297
+ for (const id of layerIds) {
10298
+ const def = defaults.layers[id];
10299
+ if (def) {
10300
+ layers.push({ id, name: def.name, icon: def.icon, color: def.color });
10301
+ } else {
10302
+ layers.push({ id, name: id, icon: "box", color: "#cbd5e1" });
10303
+ }
10304
+ }
10305
+ const edgeSet = /* @__PURE__ */ new Set();
10306
+ const edges = [];
10307
+ const allNodeIds = new Set(manifestNodes.map((n) => n.id));
10308
+ for (const cId of centerNodeIds) {
10309
+ for (const result of blastResults) {
10310
+ for (const affected of result.affected) {
10311
+ if (affected.hop === 1 && result.center.id === cId && allNodeIds.has(affected.id)) {
10312
+ const key = `${cId}->${affected.id}`;
10313
+ if (!edgeSet.has(key)) {
10314
+ edgeSet.add(key);
10315
+ edges.push({ source: cId, target: affected.id });
10316
+ }
10317
+ }
10318
+ }
10319
+ }
10320
+ }
10321
+ for (const result of blastResults) {
10322
+ if (result.edges) {
10323
+ for (const edge of result.edges) {
10324
+ if (allNodeIds.has(edge.source) && allNodeIds.has(edge.target)) {
10325
+ const key = `${edge.source}->${edge.target}`;
10326
+ if (!edgeSet.has(key)) {
10327
+ edgeSet.add(key);
10328
+ edges.push({ source: edge.source, target: edge.target });
10329
+ }
10330
+ }
10331
+ }
10332
+ }
10333
+ }
10334
+ for (const cn of createNodes) {
10335
+ edges.push({ source: "center", target: cn.id });
10336
+ if (cn.connects_to) {
10337
+ for (const targetId of cn.connects_to) {
10338
+ if (allNodeIds.has(targetId) || createNodes.some((c) => c.id === targetId)) {
10339
+ const key = `${cn.id}->${targetId}`;
10340
+ if (!edgeSet.has(key)) {
10341
+ edgeSet.add(key);
10342
+ edges.push({ source: cn.id, target: targetId });
10343
+ }
10344
+ }
10345
+ }
10346
+ }
10347
+ }
10348
+ return {
10349
+ mode,
10350
+ title,
10351
+ subtitle,
10352
+ layers,
10353
+ rings: defaults.rings,
10354
+ center: { name: title, description },
10355
+ nodes: manifestNodes,
10356
+ edges
10357
+ };
10358
+ }
10359
+
10118
10360
  // src/server/lockfile.ts
10119
10361
  var import_node_child_process = require("node:child_process");
10120
- var import_node_fs15 = require("node:fs");
10362
+ var import_node_fs16 = require("node:fs");
10121
10363
  var import_node_os = require("node:os");
10122
- var import_node_path17 = require("node:path");
10364
+ var import_node_path18 = require("node:path");
10123
10365
  function lockDir(projectRoot) {
10124
10366
  if (projectRoot) {
10125
- return (0, import_node_path17.join)(projectRoot, ".launchsecure");
10367
+ return (0, import_node_path18.join)(projectRoot, ".launchsecure");
10126
10368
  }
10127
- return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
10369
+ return (0, import_node_path18.join)((0, import_node_os.homedir)(), ".launchsecure");
10128
10370
  }
10129
10371
  function lockPath(projectRoot) {
10130
- return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
10372
+ return (0, import_node_path18.join)(lockDir(projectRoot), "launch-chart.lock");
10131
10373
  }
10132
10374
  var _activeProjectRoot;
10133
10375
  function readLock(projectRoot) {
10134
10376
  const root = projectRoot ?? _activeProjectRoot;
10135
10377
  const p = lockPath(root);
10136
- if (!(0, import_node_fs15.existsSync)(p)) {
10378
+ if (!(0, import_node_fs16.existsSync)(p)) {
10137
10379
  if (root) {
10138
10380
  const globalP = lockPath();
10139
- if ((0, import_node_fs15.existsSync)(globalP)) {
10381
+ if ((0, import_node_fs16.existsSync)(globalP)) {
10140
10382
  try {
10141
- const data = JSON.parse((0, import_node_fs15.readFileSync)(globalP, "utf-8"));
10383
+ const data = JSON.parse((0, import_node_fs16.readFileSync)(globalP, "utf-8"));
10142
10384
  if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
10143
10385
  return data;
10144
10386
  }
@@ -10149,7 +10391,7 @@ function readLock(projectRoot) {
10149
10391
  return null;
10150
10392
  }
10151
10393
  try {
10152
- const data = JSON.parse((0, import_node_fs15.readFileSync)(p, "utf-8"));
10394
+ const data = JSON.parse((0, import_node_fs16.readFileSync)(p, "utf-8"));
10153
10395
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
10154
10396
  return data;
10155
10397
  } catch {
@@ -10186,7 +10428,7 @@ function getLiveLock(projectRoot) {
10186
10428
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
10187
10429
  if (!live) {
10188
10430
  try {
10189
- (0, import_node_fs15.unlinkSync)(lockPath(root));
10431
+ (0, import_node_fs16.unlinkSync)(lockPath(root));
10190
10432
  } catch {
10191
10433
  }
10192
10434
  return null;
@@ -10196,7 +10438,7 @@ function getLiveLock(projectRoot) {
10196
10438
  function clearLock(projectRoot) {
10197
10439
  const root = projectRoot ?? _activeProjectRoot;
10198
10440
  try {
10199
- (0, import_node_fs15.unlinkSync)(lockPath(root));
10441
+ (0, import_node_fs16.unlinkSync)(lockPath(root));
10200
10442
  } catch {
10201
10443
  }
10202
10444
  }
@@ -10205,8 +10447,8 @@ function clearLock(projectRoot) {
10205
10447
  init_config();
10206
10448
 
10207
10449
  // src/server/graph/core/language-detection.ts
10208
- var import_node_fs16 = require("node:fs");
10209
- var import_node_path18 = require("node:path");
10450
+ var import_node_fs17 = require("node:fs");
10451
+ var import_node_path19 = require("node:path");
10210
10452
  var EXTENSION_TO_LANGUAGE = {
10211
10453
  // Web / Frontend
10212
10454
  ".ts": "typescript",
@@ -10318,10 +10560,10 @@ var AUXILIARY_LANGUAGES = /* @__PURE__ */ new Set([
10318
10560
  ]);
10319
10561
  function walkForExtensions(dir, extCounts, depth = 0) {
10320
10562
  if (depth > 10) return;
10321
- if (!(0, import_node_fs16.existsSync)(dir)) return;
10563
+ if (!(0, import_node_fs17.existsSync)(dir)) return;
10322
10564
  let entries;
10323
10565
  try {
10324
- entries = (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true });
10566
+ entries = (0, import_node_fs17.readdirSync)(dir, { withFileTypes: true });
10325
10567
  } catch {
10326
10568
  return;
10327
10569
  }
@@ -10329,9 +10571,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
10329
10571
  if (entry.name.startsWith(".") && entry.isDirectory()) continue;
10330
10572
  if (entry.isDirectory()) {
10331
10573
  if (IGNORE_DIRS.has(entry.name)) continue;
10332
- walkForExtensions((0, import_node_path18.join)(dir, entry.name), extCounts, depth + 1);
10574
+ walkForExtensions((0, import_node_path19.join)(dir, entry.name), extCounts, depth + 1);
10333
10575
  } else {
10334
- const ext = (0, import_node_path18.extname)(entry.name).toLowerCase();
10576
+ const ext = (0, import_node_path19.extname)(entry.name).toLowerCase();
10335
10577
  if (ext && EXTENSION_TO_LANGUAGE[ext]) {
10336
10578
  extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
10337
10579
  }
@@ -10372,13 +10614,13 @@ function detectLanguages(rootDir, supportedLanguages) {
10372
10614
  }
10373
10615
 
10374
10616
  // src/server/graph/core/audit-core.ts
10375
- var import_node_fs17 = require("node:fs");
10376
- var import_node_path19 = require("node:path");
10617
+ var import_node_fs18 = require("node:fs");
10618
+ var import_node_path20 = require("node:path");
10377
10619
  function readGraphFile(rootDir, layer) {
10378
- const filePath = (0, import_node_path19.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
10379
- if (!(0, import_node_fs17.existsSync)(filePath)) return null;
10620
+ const filePath = (0, import_node_path20.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
10621
+ if (!(0, import_node_fs18.existsSync)(filePath)) return null;
10380
10622
  try {
10381
- return JSON.parse((0, import_node_fs17.readFileSync)(filePath, "utf-8"));
10623
+ return JSON.parse((0, import_node_fs18.readFileSync)(filePath, "utf-8"));
10382
10624
  } catch {
10383
10625
  return null;
10384
10626
  }
@@ -10422,10 +10664,10 @@ function checkUnprotectedRoutes(rootDir) {
10422
10664
  const api = readGraphFile(rootDir, "api");
10423
10665
  const staticGraph = readGraphFile(rootDir, "static");
10424
10666
  if (!api) return buildReport("api", "unprotected_routes", findings);
10425
- const routePermsPath = (0, import_node_path19.join)(rootDir, "src", "config", "route-permissions.ts");
10667
+ const routePermsPath = (0, import_node_path20.join)(rootDir, "src", "config", "route-permissions.ts");
10426
10668
  let routePermsContent = "";
10427
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
10428
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
10669
+ if ((0, import_node_fs18.existsSync)(routePermsPath)) {
10670
+ routePermsContent = (0, import_node_fs18.readFileSync)(routePermsPath, "utf-8");
10429
10671
  }
10430
10672
  const registeredRoutes = /* @__PURE__ */ new Set();
10431
10673
  const routeEntryRe = /path:\s*'([^']+)'/g;
@@ -10502,10 +10744,10 @@ function checkUnenforcedPermissions(rootDir) {
10502
10744
  const staticGraph = readGraphFile(rootDir, "static");
10503
10745
  if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
10504
10746
  const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
10505
- const routePermsPath = (0, import_node_path19.join)(rootDir, "src", "config", "route-permissions.ts");
10747
+ const routePermsPath = (0, import_node_path20.join)(rootDir, "src", "config", "route-permissions.ts");
10506
10748
  let routePermsContent = "";
10507
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
10508
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
10749
+ if ((0, import_node_fs18.existsSync)(routePermsPath)) {
10750
+ routePermsContent = (0, import_node_fs18.readFileSync)(routePermsPath, "utf-8");
10509
10751
  }
10510
10752
  for (const perm of permissions) {
10511
10753
  const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
@@ -10535,9 +10777,9 @@ function checkHardcodedValues(rootDir) {
10535
10777
  const seen = /* @__PURE__ */ new Set();
10536
10778
  for (const node of api.nodes) {
10537
10779
  if (node.type !== "endpoint") continue;
10538
- const filePath = (0, import_node_path19.join)(rootDir, "src", node.id);
10539
- if (!(0, import_node_fs17.existsSync)(filePath)) continue;
10540
- const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
10780
+ const filePath = (0, import_node_path20.join)(rootDir, "src", node.id);
10781
+ if (!(0, import_node_fs18.existsSync)(filePath)) continue;
10782
+ const content = (0, import_node_fs18.readFileSync)(filePath, "utf-8");
10541
10783
  let m;
10542
10784
  allCapsRe.lastIndex = 0;
10543
10785
  while ((m = allCapsRe.exec(content)) !== null) {
@@ -10901,6 +11143,116 @@ Use this when the user asks "is the chart running", "show me the project graph U
10901
11143
  },
10902
11144
  required: ["layer"]
10903
11145
  }
11146
+ },
11147
+ {
11148
+ name: "blast_points",
11149
+ 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.
11150
+
11151
+ USE THIS when assessing the impact of changing a file, table, or endpoint. Replaces multiple read_graph calls with a single query that:
11152
+ - Traverses REVERSE edges (who imports/depends on this node)
11153
+ - Searches across ALL layers if layer is omitted
11154
+ - Returns affected nodes with hop distance, type, layer, and module
11155
+ - Provides a summary with counts by layer, by hop, and risk assessment
11156
+
11157
+ 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.`,
11158
+ inputSchema: {
11159
+ type: "object",
11160
+ properties: {
11161
+ node_id: {
11162
+ type: "string",
11163
+ description: "The node to analyze (file path, table name, etc.)"
11164
+ },
11165
+ layer: {
11166
+ type: "string",
11167
+ description: "Layer the node lives in (e.g. 'ui', 'api', 'db'). Omit to auto-detect by searching all layers."
11168
+ },
11169
+ hops: {
11170
+ type: "number",
11171
+ description: "Max hops to traverse outward. Default 2."
11172
+ },
11173
+ direction: {
11174
+ type: "string",
11175
+ enum: ["reverse", "both"],
11176
+ description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
11177
+ }
11178
+ },
11179
+ required: ["node_id"]
11180
+ }
11181
+ },
11182
+ {
11183
+ name: "generate_blast_radius",
11184
+ description: `Generate a complete BlastRadiusManifest from graph data \u2014 ready to push to deck.
11185
+
11186
+ Two modes:
11187
+ - **Structural**: single node changed \u2192 auto-discover what's affected via reverse BFS
11188
+ Example: generate_blast_radius({ mode: "structural", node_id: "CommentChannel", title: "CommentChannel refactor" })
11189
+ - **Feature**: new feature \u2192 multiple starting nodes + new nodes to create
11190
+ Example: generate_blast_radius({ mode: "feature", title: "Client Role", description: "...", center_nodes: ["CommentChannel", "ProjectMember"], create_nodes: [{ id: "ChannelMember", name: "ChannelMember table", layer: "db", reason: "..." }] })
11191
+
11192
+ Output is a BlastRadiusManifest JSON that passes directly to the deck tool's blast-radius block.
11193
+ Reads ring/layer/center colors from .launchsecure/blast-radius-defaults.json.
11194
+ Auto-generates acceptance criteria per node using inspect_node AST data.`,
11195
+ inputSchema: {
11196
+ type: "object",
11197
+ properties: {
11198
+ mode: {
11199
+ type: "string",
11200
+ enum: ["structural", "feature"],
11201
+ description: '"structural" = single node changed. "feature" = new feature with multiple nodes.'
11202
+ },
11203
+ title: {
11204
+ type: "string",
11205
+ description: "Title for the blast radius (shown in center node and header)."
11206
+ },
11207
+ description: {
11208
+ type: "string",
11209
+ description: "Description of the change or feature."
11210
+ },
11211
+ subtitle: {
11212
+ type: "string",
11213
+ description: "Optional subtitle shown above title in the viz."
11214
+ },
11215
+ node_id: {
11216
+ type: "string",
11217
+ description: "Structural mode only: the node being changed."
11218
+ },
11219
+ center_nodes: {
11220
+ type: "array",
11221
+ items: { type: "string" },
11222
+ description: "Feature mode: existing graph node IDs that are the starting points for traversal."
11223
+ },
11224
+ create_nodes: {
11225
+ type: "array",
11226
+ items: {
11227
+ type: "object",
11228
+ properties: {
11229
+ id: { type: "string" },
11230
+ name: { type: "string" },
11231
+ layer: { type: "string" },
11232
+ type: { type: "string" },
11233
+ reason: { type: "string" },
11234
+ acceptance: { type: "array", items: { type: "string" } },
11235
+ connects_to: { type: "array", items: { type: "string" }, description: "IDs of existing nodes this new node has FK/relationship edges to." }
11236
+ },
11237
+ required: ["id", "name", "layer", "reason"]
11238
+ },
11239
+ description: "Feature mode: new nodes that need to be created (not in graph yet)."
11240
+ },
11241
+ hops: {
11242
+ type: "number",
11243
+ description: "Max hops for traversal. Default 2. Hop 1 = modify ring, hop 2+ = ripple ring."
11244
+ },
11245
+ push_to_deck: {
11246
+ type: "boolean",
11247
+ description: "If true, pushes the manifest directly to LaunchDeck browser (requires deck server running). Default false."
11248
+ },
11249
+ session: {
11250
+ type: "string",
11251
+ description: "Session name for the deck tab. Required when push_to_deck is true."
11252
+ }
11253
+ },
11254
+ required: ["title"]
11255
+ }
10904
11256
  }
10905
11257
  ];
10906
11258
  function matchesSearch(node, query) {
@@ -11058,6 +11410,263 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
11058
11410
  const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
11059
11411
  return { nodes, edges, budgetExceeded, stoppedAtHop };
11060
11412
  }
11413
+ function reverseNeighborhood(graph, centerId, hops, direction) {
11414
+ const center = graph.nodes.find((n) => n.id === centerId);
11415
+ if (!center) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
11416
+ const visited = /* @__PURE__ */ new Map();
11417
+ visited.set(centerId, { node: center, hop: 0 });
11418
+ let frontier = /* @__PURE__ */ new Set([centerId]);
11419
+ for (let h = 0; h < hops; h++) {
11420
+ const next = /* @__PURE__ */ new Set();
11421
+ for (const edge of graph.edges) {
11422
+ if (direction === "reverse") {
11423
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
11424
+ } else {
11425
+ if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
11426
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
11427
+ }
11428
+ }
11429
+ for (const id of next) {
11430
+ const node = graph.nodes.find((n) => n.id === id);
11431
+ if (node) visited.set(id, { node, hop: h + 1 });
11432
+ }
11433
+ frontier = next;
11434
+ if (frontier.size === 0) break;
11435
+ }
11436
+ const nodeIds = new Set(visited.keys());
11437
+ const edges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
11438
+ return { nodes: visited, edges };
11439
+ }
11440
+ function handleBlastPoints(args) {
11441
+ const rootDir = process.cwd();
11442
+ const nodeId = args.node_id;
11443
+ const requestedLayer = args.layer;
11444
+ const hops = args.hops ?? 2;
11445
+ const direction = args.direction ?? "reverse";
11446
+ let targetLayer = requestedLayer;
11447
+ if (!targetLayer) {
11448
+ const graphs = readAllGraphs(rootDir);
11449
+ for (const [layer, graph2] of Object.entries(graphs)) {
11450
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
11451
+ targetLayer = layer;
11452
+ break;
11453
+ }
11454
+ }
11455
+ if (!targetLayer) {
11456
+ return err(`Node "${nodeId}" not found in any layer. Available layers: ${getAvailableLayers(rootDir).join(", ")}`);
11457
+ }
11458
+ }
11459
+ const graph = readGraph(rootDir, targetLayer);
11460
+ if (!graph) {
11461
+ return err(`No graph for layer "${targetLayer}". Run generate_graph first.`);
11462
+ }
11463
+ const center = graph.nodes.find((n) => n.id === nodeId);
11464
+ if (!center) {
11465
+ return err(`Node "${nodeId}" not found in ${targetLayer} layer.`);
11466
+ }
11467
+ const result = reverseNeighborhood(graph, nodeId, hops, direction);
11468
+ const affected = [];
11469
+ for (const [id, { node, hop }] of result.nodes) {
11470
+ if (id === nodeId) continue;
11471
+ const tags = node.tags;
11472
+ affected.push({
11473
+ id: node.id,
11474
+ name: node.name,
11475
+ type: node.type,
11476
+ layer: targetLayer,
11477
+ hop,
11478
+ module: tags?.module
11479
+ });
11480
+ }
11481
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
11482
+ for (const otherLayer of otherLayers) {
11483
+ const otherGraph = readGraph(rootDir, otherLayer);
11484
+ if (!otherGraph) continue;
11485
+ for (const edge of otherGraph.edges) {
11486
+ if (edge.target === nodeId || edge.source === nodeId) {
11487
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
11488
+ if (affected.some((a) => a.id === dependentId)) continue;
11489
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
11490
+ if (depNode) {
11491
+ const tags = depNode.tags;
11492
+ affected.push({
11493
+ id: depNode.id,
11494
+ name: depNode.name,
11495
+ type: depNode.type,
11496
+ layer: otherLayer,
11497
+ hop: 1,
11498
+ module: tags?.module
11499
+ });
11500
+ }
11501
+ }
11502
+ }
11503
+ }
11504
+ const byLayer = {};
11505
+ const byHop = {};
11506
+ const modulesSet = /* @__PURE__ */ new Set();
11507
+ for (const a of affected) {
11508
+ byLayer[a.layer] = (byLayer[a.layer] ?? 0) + 1;
11509
+ byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
11510
+ if (a.module) modulesSet.add(a.module);
11511
+ }
11512
+ const crossesLayers = Object.keys(byLayer).length > 1;
11513
+ const centerTags = center.tags;
11514
+ return okJson({
11515
+ center: {
11516
+ id: center.id,
11517
+ name: center.name,
11518
+ type: center.type,
11519
+ layer: targetLayer,
11520
+ module: centerTags?.module
11521
+ },
11522
+ affected,
11523
+ summary: {
11524
+ total: affected.length,
11525
+ by_layer: byLayer,
11526
+ by_hop: byHop,
11527
+ modules_touched: Array.from(modulesSet).sort(),
11528
+ crosses_layers: crossesLayers
11529
+ }
11530
+ });
11531
+ }
11532
+ function handleGenerateBlastRadius(args) {
11533
+ const rootDir = process.cwd();
11534
+ const mode = args.mode ?? "structural";
11535
+ const title = args.title;
11536
+ const description = args.description ?? title;
11537
+ const subtitle = args.subtitle;
11538
+ const hops = args.hops ?? 2;
11539
+ const defaults = loadDefaults(rootDir);
11540
+ let centerNodeIds = [];
11541
+ if (mode === "structural") {
11542
+ const nodeId = args.node_id;
11543
+ if (!nodeId) return err("structural mode requires node_id");
11544
+ centerNodeIds = [nodeId];
11545
+ } else {
11546
+ centerNodeIds = args.center_nodes ?? [];
11547
+ if (centerNodeIds.length === 0) return err("feature mode requires center_nodes[]");
11548
+ }
11549
+ const createNodes = args.create_nodes ?? [];
11550
+ const blastResults = [];
11551
+ for (const nodeId of centerNodeIds) {
11552
+ let targetLayer;
11553
+ const graphs = readAllGraphs(rootDir);
11554
+ for (const [layer, graph2] of Object.entries(graphs)) {
11555
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
11556
+ targetLayer = layer;
11557
+ break;
11558
+ }
11559
+ }
11560
+ if (!targetLayer) continue;
11561
+ const graph = readGraph(rootDir, targetLayer);
11562
+ if (!graph) continue;
11563
+ const center = graph.nodes.find((n) => n.id === nodeId);
11564
+ if (!center) continue;
11565
+ const result2 = reverseNeighborhood(graph, nodeId, hops, "reverse");
11566
+ const affected = [];
11567
+ for (const [id, { node, hop }] of result2.nodes) {
11568
+ if (id === nodeId) continue;
11569
+ const tags = node.tags;
11570
+ affected.push({ id: node.id, name: node.name, type: node.type, layer: targetLayer, hop, module: tags?.module });
11571
+ }
11572
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
11573
+ for (const otherLayer of otherLayers) {
11574
+ const otherGraph = readGraph(rootDir, otherLayer);
11575
+ if (!otherGraph) continue;
11576
+ for (const edge of otherGraph.edges) {
11577
+ if (edge.target === nodeId || edge.source === nodeId) {
11578
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
11579
+ if (affected.some((a) => a.id === dependentId)) continue;
11580
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
11581
+ if (depNode) {
11582
+ const tags = depNode.tags;
11583
+ affected.push({ id: depNode.id, name: depNode.name, type: depNode.type, layer: otherLayer, hop: 1, module: tags?.module });
11584
+ }
11585
+ }
11586
+ }
11587
+ }
11588
+ const centerTags = center.tags;
11589
+ const edges = result2.edges.map((e) => ({ source: e.source, target: e.target }));
11590
+ blastResults.push({
11591
+ center: { id: center.id, name: center.name, type: center.type, layer: targetLayer, module: centerTags?.module },
11592
+ affected,
11593
+ edges
11594
+ });
11595
+ }
11596
+ if (blastResults.length === 0) {
11597
+ return err(`None of the center nodes were found in any graph layer: ${centerNodeIds.join(", ")}`);
11598
+ }
11599
+ const inspectData = {};
11600
+ const allAffectedIds = /* @__PURE__ */ new Set();
11601
+ for (const r of blastResults) {
11602
+ allAffectedIds.add(r.center.id);
11603
+ for (const a of r.affected) allAffectedIds.add(a.id);
11604
+ }
11605
+ const allGraphs = readAllGraphs(rootDir);
11606
+ for (const id of allAffectedIds) {
11607
+ for (const [, graph] of Object.entries(allGraphs)) {
11608
+ if (!graph) continue;
11609
+ const node = graph.nodes.find((n) => n.id === id);
11610
+ if (node) {
11611
+ inspectData[id] = {
11612
+ type: node.type,
11613
+ name: node.name,
11614
+ methods: node.methods,
11615
+ path: node.path ?? node.handler,
11616
+ auth: node.auth,
11617
+ db_models: node.db_models
11618
+ };
11619
+ break;
11620
+ }
11621
+ }
11622
+ }
11623
+ const manifest = buildManifest({
11624
+ mode,
11625
+ title,
11626
+ description,
11627
+ subtitle,
11628
+ blastResults,
11629
+ createNodes,
11630
+ inspectData,
11631
+ defaults
11632
+ });
11633
+ const pushToDeck = args.push_to_deck;
11634
+ const session = args.session;
11635
+ let deckResult;
11636
+ if (pushToDeck) {
11637
+ if (!session) return err("push_to_deck requires a session name");
11638
+ const deckLockPath = (0, import_node_path21.join)(rootDir, ".launchsecure", "launch-deck.lock");
11639
+ if (!(0, import_node_fs19.existsSync)(deckLockPath)) {
11640
+ deckResult = { pushed: false, reason: "Deck server not running (no lock file). Push manually via deck tool." };
11641
+ } else {
11642
+ try {
11643
+ const lock = JSON.parse((0, import_node_fs19.readFileSync)(deckLockPath, "utf-8"));
11644
+ const deckUrl = lock.url;
11645
+ const body = JSON.stringify({
11646
+ session,
11647
+ mode: "show",
11648
+ blocks: [{ type: "blast-radius", label: title, manifest }]
11649
+ });
11650
+ (0, import_node_child_process2.execFileSync)("curl", [
11651
+ "-s",
11652
+ "-X",
11653
+ "POST",
11654
+ deckUrl + "/api/deck",
11655
+ "-H",
11656
+ "Content-Type: application/json",
11657
+ "-d",
11658
+ body
11659
+ ], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
11660
+ deckResult = { pushed: true, session, url: deckUrl };
11661
+ } catch (e) {
11662
+ deckResult = { pushed: false, reason: `Failed to push to deck: ${e}` };
11663
+ }
11664
+ }
11665
+ }
11666
+ const result = { ...manifest };
11667
+ if (deckResult) result._deck = deckResult;
11668
+ return okJson(result);
11669
+ }
11061
11670
  function layerSummary(graph) {
11062
11671
  const typeCounts = {};
11063
11672
  const moduleCounts = {};
@@ -11286,12 +11895,12 @@ function handleReadGraph(args) {
11286
11895
  return okJson(result);
11287
11896
  }
11288
11897
  function nodeToFilePath(rootDir, layer, nodeId) {
11289
- if (layer === "ui" || layer === "api") return (0, import_node_path20.join)(rootDir, "src", nodeId);
11290
- if (layer === "db") return (0, import_node_path20.join)(rootDir, "prisma", "schema.prisma");
11291
- const withSrc = (0, import_node_path20.join)(rootDir, "src", nodeId);
11292
- if ((0, import_node_fs18.existsSync)(withSrc)) return withSrc;
11293
- const direct = (0, import_node_path20.join)(rootDir, nodeId);
11294
- if ((0, import_node_fs18.existsSync)(direct)) return direct;
11898
+ if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
11899
+ if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
11900
+ const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
11901
+ if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
11902
+ const direct = (0, import_node_path21.join)(rootDir, nodeId);
11903
+ if ((0, import_node_fs19.existsSync)(direct)) return direct;
11295
11904
  return null;
11296
11905
  }
11297
11906
  function handleInspectNode(args) {
@@ -11434,11 +12043,11 @@ function handleGrepNodes(args) {
11434
12043
  let filesSearched = 0;
11435
12044
  let truncated = false;
11436
12045
  for (const [filePath, nodeId] of filePaths) {
11437
- if (!(0, import_node_fs18.existsSync)(filePath)) continue;
12046
+ if (!(0, import_node_fs19.existsSync)(filePath)) continue;
11438
12047
  filesSearched++;
11439
12048
  let content;
11440
12049
  try {
11441
- content = (0, import_node_fs18.readFileSync)(filePath, "utf-8");
12050
+ content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
11442
12051
  } catch {
11443
12052
  continue;
11444
12053
  }
@@ -11503,11 +12112,11 @@ function handleStartChartServer(args) {
11503
12112
  });
11504
12113
  }
11505
12114
  const entryPath = process.argv[1];
11506
- const logDir = (0, import_node_path20.join)((0, import_node_os2.homedir)(), ".launchsecure");
11507
- (0, import_node_fs18.mkdirSync)(logDir, { recursive: true });
11508
- const logPath = (0, import_node_path20.join)(logDir, "launch-chart.log");
11509
- const out = (0, import_node_fs18.openSync)(logPath, "a");
11510
- const err2 = (0, import_node_fs18.openSync)(logPath, "a");
12115
+ const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
12116
+ (0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
12117
+ const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
12118
+ const out = (0, import_node_fs19.openSync)(logPath, "a");
12119
+ const err2 = (0, import_node_fs19.openSync)(logPath, "a");
11511
12120
  const portArgs = args.port ? ["--port", String(args.port)] : [];
11512
12121
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
11513
12122
  detached: true,
@@ -11619,31 +12228,28 @@ function handleDetectProjectStack() {
11619
12228
  for (const l of p.layers) availableLayers.add(l);
11620
12229
  }
11621
12230
  }
11622
- let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
12231
+ const stats = { calls_api: 0, references_api: 0, annotations: 0 };
11623
12232
  const uiGraph = readGraph(rootDir, "ui");
11624
12233
  if (uiGraph) {
11625
12234
  for (const ref of uiGraph.cross_refs ?? []) {
11626
12235
  if (ref.type === "calls_api") stats.calls_api++;
11627
12236
  if (ref.type === "references_api") stats.references_api++;
11628
12237
  }
11629
- for (const f of uiGraph.flagged_edges ?? []) {
11630
- if (f.type === "out_of_pattern") stats.out_of_pattern++;
11631
- }
11632
12238
  }
11633
- const srcDir = (0, import_node_path20.join)(rootDir, "src");
11634
- if ((0, import_node_fs18.existsSync)(srcDir)) {
12239
+ const srcDir = (0, import_node_path21.join)(rootDir, "src");
12240
+ if ((0, import_node_fs19.existsSync)(srcDir)) {
11635
12241
  const scanDir = (dir) => {
11636
- if (!(0, import_node_fs18.existsSync)(dir)) return;
11637
- for (const entry of (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true })) {
12242
+ if (!(0, import_node_fs19.existsSync)(dir)) return;
12243
+ for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
11638
12244
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
11639
- const full = (0, import_node_path20.join)(dir, entry.name);
12245
+ const full = (0, import_node_path21.join)(dir, entry.name);
11640
12246
  if (entry.isDirectory()) {
11641
12247
  scanDir(full);
11642
12248
  continue;
11643
12249
  }
11644
- if (![".ts", ".tsx"].includes((0, import_node_path20.extname)(entry.name))) continue;
12250
+ if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
11645
12251
  try {
11646
- const content = (0, import_node_fs18.readFileSync)(full, "utf-8");
12252
+ const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
11647
12253
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
11648
12254
  if (matches) stats.annotations += matches.length;
11649
12255
  } catch {
@@ -11652,12 +12258,6 @@ function handleDetectProjectStack() {
11652
12258
  };
11653
12259
  scanDir(srcDir);
11654
12260
  }
11655
- let recommendedPrimary = "fetch-resolver";
11656
- if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
11657
- recommendedPrimary = "api-annotations";
11658
- } else if (stats.calls_api === 0 && stats.references_api > 0) {
11659
- recommendedPrimary = "url-literal-scanner";
11660
- }
11661
12261
  const supportedLanguages = /* @__PURE__ */ new Map();
11662
12262
  supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
11663
12263
  supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
@@ -11668,13 +12268,22 @@ function handleDetectProjectStack() {
11668
12268
  languages,
11669
12269
  parsers: parserResults,
11670
12270
  available_layers: [...availableLayers],
11671
- crosslayer_parsers: [
11672
- { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
11673
- { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
11674
- { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
11675
- ],
12271
+ crosslayer_parsers: (() => {
12272
+ const descriptions = {
12273
+ "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
12274
+ "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
12275
+ "url-literal-scanner": "Finds /api/... string literals as fallback detection",
12276
+ "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
12277
+ };
12278
+ const grouped = {};
12279
+ for (const p of registry.getCrossLayerParsers()) {
12280
+ const concern = p.concern ?? "api-binding";
12281
+ if (!grouped[concern]) grouped[concern] = [];
12282
+ grouped[concern].push({ id: p.id, description: descriptions[p.id] ?? p.id });
12283
+ }
12284
+ return grouped;
12285
+ })(),
11676
12286
  stats,
11677
- recommended_primary: recommendedPrimary,
11678
12287
  ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
11679
12288
  current_config: Object.keys(config).length > 0 ? config : null,
11680
12289
  config_path: ".launchchart.json"
@@ -11755,6 +12364,14 @@ async function handleMessage(msg) {
11755
12364
  respond(id ?? null, handleAuditLayer(args));
11756
12365
  return;
11757
12366
  }
12367
+ if (toolName === "blast_points") {
12368
+ respond(id ?? null, handleBlastPoints(args));
12369
+ return;
12370
+ }
12371
+ if (toolName === "generate_blast_radius") {
12372
+ respond(id ?? null, handleGenerateBlastRadius(args));
12373
+ return;
12374
+ }
11758
12375
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
11759
12376
  return;
11760
12377
  }
@@ -11806,7 +12423,8 @@ if (import_fs8.default.existsSync(envPath)) {
11806
12423
  }
11807
12424
  }
11808
12425
  var LAUNCHSECURE_URL = process.env.LAUNCHSECURE_URL || "http://localhost:4177";
11809
- var CONFIG_FILE = "launchpod.config.json";
12426
+ var CONFIG_FILE = ".launchpod.json";
12427
+ var CONFIG_FILE_LEGACY = "launchpod.config.json";
11810
12428
  var REPO_ROOT = process.cwd();
11811
12429
  var PROJECT_DIR = REPO_ROOT;
11812
12430
  var CREDENTIALS_DIR = import_path8.default.join(PROJECT_DIR, ".launchpod");
@@ -11842,8 +12460,10 @@ function parseArgs() {
11842
12460
  port = parseInt(process.env.PORT, 10);
11843
12461
  } else {
11844
12462
  const configPath = import_path8.default.join(REPO_ROOT, CONFIG_FILE);
11845
- if (import_fs8.default.existsSync(configPath)) {
11846
- const config = JSON.parse(import_fs8.default.readFileSync(configPath, "utf-8"));
12463
+ const legacyPath = import_path8.default.join(REPO_ROOT, CONFIG_FILE_LEGACY);
12464
+ const resolvedPath = import_fs8.default.existsSync(configPath) ? configPath : import_fs8.default.existsSync(legacyPath) ? legacyPath : null;
12465
+ if (resolvedPath) {
12466
+ const config = JSON.parse(import_fs8.default.readFileSync(resolvedPath, "utf-8"));
11847
12467
  if (config.port) port = config.port;
11848
12468
  }
11849
12469
  }
@@ -12072,21 +12692,21 @@ function ensureGitRepo(runId) {
12072
12692
  console.log("Git repo initialized");
12073
12693
  }
12074
12694
  const branch = `pipeline/${runId}`;
12075
- try {
12076
- const currentBranch = (0, import_child_process4.execSync)("git rev-parse --abbrev-ref HEAD", { cwd: REPO_ROOT, stdio: "pipe" }).toString().trim();
12077
- if (currentBranch === branch) {
12078
- console.log(`Already on branch ${branch}`);
12079
- return;
12080
- }
12081
- } catch {
12695
+ const worktreeDir = import_path8.default.join(REPO_ROOT, ".launchpod", "worktrees", runId);
12696
+ if (import_fs8.default.existsSync(worktreeDir)) {
12697
+ PROJECT_DIR = worktreeDir;
12698
+ console.log(`Resuming in existing worktree ${worktreeDir}`);
12699
+ return;
12082
12700
  }
12701
+ import_fs8.default.mkdirSync(import_path8.default.join(REPO_ROOT, ".launchpod", "worktrees"), { recursive: true });
12083
12702
  try {
12084
- (0, import_child_process4.execSync)(`git checkout "${branch}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12085
- console.log(`Checked out existing branch ${branch}`);
12703
+ (0, import_child_process4.execSync)(`git worktree add "${worktreeDir}" "${branch}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12704
+ console.log(`Created worktree from existing branch ${branch}`);
12086
12705
  } catch {
12087
- (0, import_child_process4.execSync)(`git checkout -b "${branch}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12088
- console.log(`Created branch ${branch}`);
12706
+ (0, import_child_process4.execSync)(`git worktree add -b "${branch}" "${worktreeDir}"`, { cwd: REPO_ROOT, stdio: "pipe" });
12707
+ console.log(`Created worktree with new branch ${branch}`);
12089
12708
  }
12709
+ PROJECT_DIR = worktreeDir;
12090
12710
  }
12091
12711
  function normalizeGitUrl(url) {
12092
12712
  return url.replace(/\.git$/, "").replace(/^git@github\.com:/, "https://github.com/").replace(/\/$/, "").toLowerCase();
@@ -12189,7 +12809,8 @@ function parseTrackerData(result) {
12189
12809
  const runStatus = typeof run?.status === "string" ? run.status : void 0;
12190
12810
  return { agents, interventions, runStatus };
12191
12811
  }
12192
- async function startPipelineSystem(creds) {
12812
+ async function startPipelineSystem(creds, capabilities) {
12813
+ const activeCapabilities = capabilities ?? { ...DEFAULT_CAPABILITIES };
12193
12814
  try {
12194
12815
  writeMcpConfigs({
12195
12816
  projectDir: PROJECT_DIR,
@@ -12455,7 +13076,7 @@ if (!__isMcpMode) {
12455
13076
  });
12456
13077
  authenticated = true;
12457
13078
  const savedCreds = loadCredentials();
12458
- if (savedCreds) startPipelineSystem(savedCreds);
13079
+ if (savedCreds) startPipelineSystem(savedCreds, activeCapabilities);
12459
13080
  res.writeHead(200, { "Content-Type": "application/json" });
12460
13081
  res.end(JSON.stringify({ success: true }));
12461
13082
  } catch {
@@ -12673,7 +13294,7 @@ if (!__isMcpMode) {
12673
13294
  }
12674
13295
  serveIndex(res);
12675
13296
  });
12676
- let activeCapabilities2 = { ...DEFAULT_CAPABILITIES };
13297
+ let activeCapabilities = { ...DEFAULT_CAPABILITIES };
12677
13298
  async function main() {
12678
13299
  const { token, serverUrl } = parsedArgs;
12679
13300
  if (token) {
@@ -12690,8 +13311,8 @@ if (!__isMcpMode) {
12690
13311
  process.exit(1);
12691
13312
  }
12692
13313
  activeRunId = runId;
12693
- activeCapabilities2 = await fetchCapabilities(serverUrl, token);
12694
- console.log(`Capabilities: workspace_setup=${activeCapabilities2.workspace_setup}, terminal=${activeCapabilities2.terminal}`);
13314
+ activeCapabilities = await fetchCapabilities(serverUrl, token);
13315
+ console.log(`Capabilities: workspace_setup=${activeCapabilities.workspace_setup}, terminal=${activeCapabilities.terminal}`);
12695
13316
  ensureGitRepo(runId);
12696
13317
  await validateRepoUrl(serverUrl, token);
12697
13318
  const lock = checkLockfile();
@@ -12719,13 +13340,13 @@ if (!__isMcpMode) {
12719
13340
  const existingCreds = loadCredentials();
12720
13341
  if (existingCreds) {
12721
13342
  if (!token) {
12722
- activeCapabilities2 = await fetchCapabilities(existingCreds.serverUrl, existingCreds.token);
13343
+ activeCapabilities = await fetchCapabilities(existingCreds.serverUrl, existingCreds.token);
12723
13344
  }
12724
- if (activeCapabilities2.terminal) {
13345
+ if (activeCapabilities.terminal) {
12725
13346
  initTerminalBridge(server, PROJECT_DIR, import_path8.default.join(PROJECT_DIR, ".launchpod", "sessions"));
12726
13347
  }
12727
- startPipelineSystem(existingCreds);
12728
- } else if (activeCapabilities2.terminal) {
13348
+ startPipelineSystem(existingCreds, activeCapabilities);
13349
+ } else if (activeCapabilities.terminal) {
12729
13350
  initTerminalBridge(server, PROJECT_DIR, import_path8.default.join(PROJECT_DIR, ".launchpod", "sessions"));
12730
13351
  }
12731
13352
  }