@launchsecure/launch-kit 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart-client/assets/index-BN_N_I08.js +379 -0
- package/dist/chart-client/assets/index-DJRXEWQm.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-CCpAvTkG.css +32 -0
- package/dist/client/assets/{index-DCC--GO-.js → index-DldfczJ1.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +589 -99
- package/dist/server/cli.js +599 -124
- package/dist/server/graph-mcp-entry.js +731 -128
- package/package.json +1 -1
- package/dist/chart-client/assets/index-BUih0oqR.js +0 -358
- package/dist/chart-client/assets/index-DFslt72L.css +0 -1
- package/dist/client/assets/index-BCxRNp8I.css +0 -32
package/dist/server/cli.js
CHANGED
|
@@ -1727,7 +1727,7 @@ var require_usage_reader = __commonJS({
|
|
|
1727
1727
|
async readJsonlFile(filePath, cutoffTime) {
|
|
1728
1728
|
const entries = [];
|
|
1729
1729
|
const fileProcessedEntries = /* @__PURE__ */ new Set();
|
|
1730
|
-
return new Promise((
|
|
1730
|
+
return new Promise((resolve3) => {
|
|
1731
1731
|
const rl = readline.createInterface({
|
|
1732
1732
|
input: createReadStream(filePath),
|
|
1733
1733
|
crlfDelay: Infinity
|
|
@@ -1785,11 +1785,11 @@ var require_usage_reader = __commonJS({
|
|
|
1785
1785
|
}
|
|
1786
1786
|
});
|
|
1787
1787
|
rl.on("close", () => {
|
|
1788
|
-
|
|
1788
|
+
resolve3(entries);
|
|
1789
1789
|
});
|
|
1790
1790
|
rl.on("error", (error) => {
|
|
1791
1791
|
console.error("Error reading file:", filePath, error);
|
|
1792
|
-
|
|
1792
|
+
resolve3(entries);
|
|
1793
1793
|
});
|
|
1794
1794
|
});
|
|
1795
1795
|
}
|
|
@@ -3587,7 +3587,7 @@ var require_src = __commonJS({
|
|
|
3587
3587
|
if (session.active) throw new Error(`Agent already running in session ${sessionId}`);
|
|
3588
3588
|
const { command, args = [], env = {} } = options;
|
|
3589
3589
|
if (!command) throw new Error("startScriptInSession requires a command");
|
|
3590
|
-
return new Promise((
|
|
3590
|
+
return new Promise((resolve3, reject) => {
|
|
3591
3591
|
this.scriptBridge.startSession(sessionId, {
|
|
3592
3592
|
command,
|
|
3593
3593
|
args,
|
|
@@ -3609,7 +3609,7 @@ var require_src = __commonJS({
|
|
|
3609
3609
|
session.lastActivity = /* @__PURE__ */ new Date();
|
|
3610
3610
|
this.broadcastToSession(sessionId, { type: "script_stopped", sessionId });
|
|
3611
3611
|
if (exitCode === 0) {
|
|
3612
|
-
|
|
3612
|
+
resolve3({ code: exitCode, signal });
|
|
3613
3613
|
} else {
|
|
3614
3614
|
reject(new Error(`Script exited with code ${exitCode}`));
|
|
3615
3615
|
}
|
|
@@ -3701,6 +3701,30 @@ var require_src = __commonJS({
|
|
|
3701
3701
|
}
|
|
3702
3702
|
});
|
|
3703
3703
|
|
|
3704
|
+
// src/server/graph/core/config.ts
|
|
3705
|
+
var config_exports = {};
|
|
3706
|
+
__export(config_exports, {
|
|
3707
|
+
loadConfig: () => loadConfig
|
|
3708
|
+
});
|
|
3709
|
+
function loadConfig(rootDir) {
|
|
3710
|
+
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
3711
|
+
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
3712
|
+
try {
|
|
3713
|
+
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
3714
|
+
} catch {
|
|
3715
|
+
return {};
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
var import_node_fs, import_node_path, CONFIG_FILENAME;
|
|
3719
|
+
var init_config = __esm({
|
|
3720
|
+
"src/server/graph/core/config.ts"() {
|
|
3721
|
+
"use strict";
|
|
3722
|
+
import_node_fs = require("node:fs");
|
|
3723
|
+
import_node_path = require("node:path");
|
|
3724
|
+
CONFIG_FILENAME = ".launchchart.json";
|
|
3725
|
+
}
|
|
3726
|
+
});
|
|
3727
|
+
|
|
3704
3728
|
// src/server/cli.ts
|
|
3705
3729
|
var import_http = __toESM(require("http"));
|
|
3706
3730
|
var import_https = __toESM(require("https"));
|
|
@@ -5270,7 +5294,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5270
5294
|
return 3001;
|
|
5271
5295
|
}
|
|
5272
5296
|
startDevServer(port, databaseUrl) {
|
|
5273
|
-
return new Promise((
|
|
5297
|
+
return new Promise((resolve3) => {
|
|
5274
5298
|
const env = { ...process.env, PORT: String(port), ...databaseUrl ? { DATABASE_URL: databaseUrl } : {} };
|
|
5275
5299
|
this.devProcess = (0, import_child_process3.spawn)("npm", ["run", "dev"], {
|
|
5276
5300
|
cwd: this.workingDir,
|
|
@@ -5282,7 +5306,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5282
5306
|
const timeout = setTimeout(() => {
|
|
5283
5307
|
if (!resolved) {
|
|
5284
5308
|
resolved = true;
|
|
5285
|
-
this.healthCheck(port).then(
|
|
5309
|
+
this.healthCheck(port).then(resolve3);
|
|
5286
5310
|
}
|
|
5287
5311
|
}, 15e3);
|
|
5288
5312
|
const onData = (data) => {
|
|
@@ -5291,7 +5315,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5291
5315
|
if (!resolved) {
|
|
5292
5316
|
resolved = true;
|
|
5293
5317
|
clearTimeout(timeout);
|
|
5294
|
-
|
|
5318
|
+
resolve3(true);
|
|
5295
5319
|
}
|
|
5296
5320
|
}
|
|
5297
5321
|
};
|
|
@@ -5302,7 +5326,7 @@ var PostImplLaunchExecutor = class {
|
|
|
5302
5326
|
if (!resolved) {
|
|
5303
5327
|
resolved = true;
|
|
5304
5328
|
clearTimeout(timeout);
|
|
5305
|
-
|
|
5329
|
+
resolve3(false);
|
|
5306
5330
|
}
|
|
5307
5331
|
});
|
|
5308
5332
|
this.devProcess.unref();
|
|
@@ -6324,26 +6348,13 @@ ${links}
|
|
|
6324
6348
|
}
|
|
6325
6349
|
|
|
6326
6350
|
// src/server/graph/index.ts
|
|
6327
|
-
var
|
|
6328
|
-
var
|
|
6351
|
+
var import_node_fs11 = require("node:fs");
|
|
6352
|
+
var import_node_path13 = require("node:path");
|
|
6329
6353
|
|
|
6330
6354
|
// src/server/graph/core/graph-builder.ts
|
|
6331
6355
|
var import_node_fs8 = require("node:fs");
|
|
6332
6356
|
var import_node_path9 = require("node:path");
|
|
6333
|
-
|
|
6334
|
-
// src/server/graph/core/config.ts
|
|
6335
|
-
var import_node_fs = require("node:fs");
|
|
6336
|
-
var import_node_path = require("node:path");
|
|
6337
|
-
var CONFIG_FILENAME = ".launchchart.json";
|
|
6338
|
-
function loadConfig(rootDir) {
|
|
6339
|
-
const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
|
|
6340
|
-
if (!(0, import_node_fs.existsSync)(configPath)) return {};
|
|
6341
|
-
try {
|
|
6342
|
-
return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
|
|
6343
|
-
} catch {
|
|
6344
|
-
return {};
|
|
6345
|
-
}
|
|
6346
|
-
}
|
|
6357
|
+
init_config();
|
|
6347
6358
|
|
|
6348
6359
|
// src/server/graph/core/parser-registry.ts
|
|
6349
6360
|
var import_node_path8 = require("node:path");
|
|
@@ -6809,34 +6820,6 @@ function classifyType(id) {
|
|
|
6809
6820
|
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
6810
6821
|
return "component";
|
|
6811
6822
|
}
|
|
6812
|
-
function classifyModule(id) {
|
|
6813
|
-
if (/app\/\(auth\)\//.test(id)) return "auth";
|
|
6814
|
-
if (/app\/\(admin\)\//.test(id)) return "admin";
|
|
6815
|
-
if (/app\/\(settings\)\//.test(id)) return "settings";
|
|
6816
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
|
|
6817
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
|
|
6818
|
-
if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
|
|
6819
|
-
if (id.startsWith("app/integrations/")) return "integrations";
|
|
6820
|
-
if (id.startsWith("app/docs/")) return "admin";
|
|
6821
|
-
if (id.startsWith("client/components/ui/")) return "shared-ui";
|
|
6822
|
-
if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
|
|
6823
|
-
if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
|
|
6824
|
-
if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
|
|
6825
|
-
if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
|
|
6826
|
-
if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
|
|
6827
|
-
if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
|
|
6828
|
-
if (id.startsWith("server/auth/")) return "auth";
|
|
6829
|
-
if (id.startsWith("server/mcp/")) return "mcp";
|
|
6830
|
-
if (id.startsWith("server/lib/")) return "server-lib";
|
|
6831
|
-
if (id.startsWith("server/middleware")) return "middleware";
|
|
6832
|
-
if (id.startsWith("server/services/")) return "services";
|
|
6833
|
-
if (id.startsWith("server/db")) return "db";
|
|
6834
|
-
if (id.startsWith("server/errors")) return "errors";
|
|
6835
|
-
if (id.startsWith("server/")) return "server-lib";
|
|
6836
|
-
if (id.startsWith("config/")) return "config";
|
|
6837
|
-
if (id.startsWith("lib/")) return "lib";
|
|
6838
|
-
return "root";
|
|
6839
|
-
}
|
|
6840
6823
|
function extractRoute(id) {
|
|
6841
6824
|
if (!id.endsWith("/page.tsx")) return null;
|
|
6842
6825
|
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
@@ -7057,8 +7040,7 @@ function generate(rootDir) {
|
|
|
7057
7040
|
const parsed = parsedByPath.get(absPath);
|
|
7058
7041
|
const name = parsed.name || nameFromFilename(absPath);
|
|
7059
7042
|
const route = extractRoute(id);
|
|
7060
|
-
|
|
7061
|
-
nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
|
|
7043
|
+
nodes.push({ id, type, name, route, exports: parsed.exports });
|
|
7062
7044
|
nodeIdSet.add(id);
|
|
7063
7045
|
nodeTypeMap.set(id, type);
|
|
7064
7046
|
if (route) routeToNodeId.set(route, id);
|
|
@@ -7162,7 +7144,6 @@ function generate(rootDir) {
|
|
|
7162
7144
|
type: "external",
|
|
7163
7145
|
name: parsed.name || nameFromFilename(absPath),
|
|
7164
7146
|
route: null,
|
|
7165
|
-
module: "external",
|
|
7166
7147
|
exports: parsed.exports
|
|
7167
7148
|
});
|
|
7168
7149
|
nodeIdSet.add(externalId);
|
|
@@ -8177,31 +8158,403 @@ function generateAll(rootDir) {
|
|
|
8177
8158
|
}
|
|
8178
8159
|
|
|
8179
8160
|
// src/server/graph/index.ts
|
|
8161
|
+
init_config();
|
|
8162
|
+
|
|
8163
|
+
// src/server/graph/core/tagger-registry.ts
|
|
8164
|
+
var import_node_path11 = require("node:path");
|
|
8165
|
+
|
|
8166
|
+
// src/server/graph/taggers/module-tagger.ts
|
|
8167
|
+
var import_node_fs9 = require("node:fs");
|
|
8168
|
+
var import_node_path10 = require("node:path");
|
|
8169
|
+
function matchGlob(pattern, id) {
|
|
8170
|
+
const patParts = pattern.split("/");
|
|
8171
|
+
const idParts = id.split("/");
|
|
8172
|
+
return matchParts(patParts, 0, idParts, 0);
|
|
8173
|
+
}
|
|
8174
|
+
function matchParts(pat, pi, id, ii) {
|
|
8175
|
+
while (pi < pat.length && ii < id.length) {
|
|
8176
|
+
const p = pat[pi];
|
|
8177
|
+
if (p === "**") {
|
|
8178
|
+
for (let skip = ii; skip <= id.length; skip++) {
|
|
8179
|
+
if (matchParts(pat, pi + 1, id, skip)) return true;
|
|
8180
|
+
}
|
|
8181
|
+
return false;
|
|
8182
|
+
}
|
|
8183
|
+
if (p === "*") {
|
|
8184
|
+
pi++;
|
|
8185
|
+
ii++;
|
|
8186
|
+
continue;
|
|
8187
|
+
}
|
|
8188
|
+
if (p !== id[ii]) return false;
|
|
8189
|
+
pi++;
|
|
8190
|
+
ii++;
|
|
8191
|
+
}
|
|
8192
|
+
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
8193
|
+
return pi === pat.length && ii === id.length;
|
|
8194
|
+
}
|
|
8195
|
+
var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
|
|
8196
|
+
function detectConventionDirs(rootDir) {
|
|
8197
|
+
const result = /* @__PURE__ */ new Map();
|
|
8198
|
+
const searchDirs = [
|
|
8199
|
+
rootDir,
|
|
8200
|
+
(0, import_node_path10.join)(rootDir, "src"),
|
|
8201
|
+
(0, import_node_path10.join)(rootDir, "app"),
|
|
8202
|
+
(0, import_node_path10.join)(rootDir, "lib")
|
|
8203
|
+
];
|
|
8204
|
+
for (const base of searchDirs) {
|
|
8205
|
+
for (const convention of CONVENTION_DIRS) {
|
|
8206
|
+
const dir = (0, import_node_path10.join)(base, convention);
|
|
8207
|
+
if (!(0, import_node_fs9.existsSync)(dir)) continue;
|
|
8208
|
+
try {
|
|
8209
|
+
const stat = (0, import_node_fs9.statSync)(dir);
|
|
8210
|
+
if (!stat.isDirectory()) continue;
|
|
8211
|
+
const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
8212
|
+
if (entries.length > 0) {
|
|
8213
|
+
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
8214
|
+
result.set(relPath, entries);
|
|
8215
|
+
}
|
|
8216
|
+
} catch {
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
return result;
|
|
8221
|
+
}
|
|
8222
|
+
function extractRouteGroups(id) {
|
|
8223
|
+
const groups = [];
|
|
8224
|
+
const re = /\(([^)]+)\)/g;
|
|
8225
|
+
let m;
|
|
8226
|
+
while ((m = re.exec(id)) !== null) {
|
|
8227
|
+
groups.push(m[1]);
|
|
8228
|
+
}
|
|
8229
|
+
return groups;
|
|
8230
|
+
}
|
|
8231
|
+
var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
8232
|
+
"src",
|
|
8233
|
+
"app",
|
|
8234
|
+
"client",
|
|
8235
|
+
"server",
|
|
8236
|
+
"lib",
|
|
8237
|
+
"config"
|
|
8238
|
+
]);
|
|
8239
|
+
function isRouteGroup(segment) {
|
|
8240
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
8241
|
+
}
|
|
8242
|
+
function isDynamicSegment(segment) {
|
|
8243
|
+
return segment.startsWith("[") || segment.startsWith(":");
|
|
8244
|
+
}
|
|
8245
|
+
function isDomainDir(segment) {
|
|
8246
|
+
return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
|
|
8247
|
+
}
|
|
8248
|
+
var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
|
|
8249
|
+
"app",
|
|
8250
|
+
"all",
|
|
8251
|
+
"ee",
|
|
8252
|
+
"home",
|
|
8253
|
+
"root"
|
|
8254
|
+
]);
|
|
8255
|
+
function isTrivialGroup(name) {
|
|
8256
|
+
if (TRIVIAL_GROUPS.has(name)) return true;
|
|
8257
|
+
const lower = name.toLowerCase();
|
|
8258
|
+
const wrapperPatterns = [
|
|
8259
|
+
/^.*-?wrapper$/,
|
|
8260
|
+
// "page-wrapper", "use-page-wrapper"
|
|
8261
|
+
/^.*-?layout$/,
|
|
8262
|
+
// "admin-layout", "settings-layout"
|
|
8263
|
+
/^use-/,
|
|
8264
|
+
// "use-page-wrapper"
|
|
8265
|
+
/^default$/
|
|
8266
|
+
];
|
|
8267
|
+
return wrapperPatterns.some((p) => p.test(lower));
|
|
8268
|
+
}
|
|
8269
|
+
function normalizeGroupName(name) {
|
|
8270
|
+
return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
8271
|
+
}
|
|
8272
|
+
function extractModuleFromPath(id) {
|
|
8273
|
+
const segments = id.split("/");
|
|
8274
|
+
const routeGroups = extractRouteGroups(id);
|
|
8275
|
+
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
|
|
8276
|
+
if (moduleGroups.length > 0) {
|
|
8277
|
+
return moduleGroups[moduleGroups.length - 1];
|
|
8278
|
+
}
|
|
8279
|
+
const meaningful = [];
|
|
8280
|
+
for (const seg of segments) {
|
|
8281
|
+
if (seg.includes(".")) continue;
|
|
8282
|
+
if (isRouteGroup(seg)) continue;
|
|
8283
|
+
if (isDynamicSegment(seg)) continue;
|
|
8284
|
+
if (isDomainDir(seg)) continue;
|
|
8285
|
+
if (SKIP_SEGMENTS.has(seg)) continue;
|
|
8286
|
+
meaningful.push(seg);
|
|
8287
|
+
}
|
|
8288
|
+
if (meaningful.length > 0) {
|
|
8289
|
+
return meaningful[0];
|
|
8290
|
+
}
|
|
8291
|
+
return "root";
|
|
8292
|
+
}
|
|
8293
|
+
var cachedRootDir = null;
|
|
8294
|
+
var cachedConventionDirs = /* @__PURE__ */ new Map();
|
|
8295
|
+
var moduleTagger = {
|
|
8296
|
+
id: "module",
|
|
8297
|
+
tagKey: "module",
|
|
8298
|
+
trackUntagged: true,
|
|
8299
|
+
layers: null,
|
|
8300
|
+
// applies to all layers
|
|
8301
|
+
tag(nodes, layer, rootDir) {
|
|
8302
|
+
if (cachedRootDir !== rootDir) {
|
|
8303
|
+
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
8304
|
+
cachedRootDir = rootDir;
|
|
8305
|
+
}
|
|
8306
|
+
let configRules = [];
|
|
8307
|
+
try {
|
|
8308
|
+
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
8309
|
+
const config = loadConfig2(rootDir);
|
|
8310
|
+
configRules = config.taggers?.module?.rules ?? [];
|
|
8311
|
+
} catch {
|
|
8312
|
+
}
|
|
8313
|
+
const result = /* @__PURE__ */ new Map();
|
|
8314
|
+
for (const node of nodes) {
|
|
8315
|
+
const id = node.id;
|
|
8316
|
+
let matched = false;
|
|
8317
|
+
for (const rule of configRules) {
|
|
8318
|
+
if (matchGlob(rule.match, id)) {
|
|
8319
|
+
result.set(id, rule.module);
|
|
8320
|
+
matched = true;
|
|
8321
|
+
break;
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
if (matched) continue;
|
|
8325
|
+
matched = false;
|
|
8326
|
+
for (const [convDir, moduleNames] of cachedConventionDirs) {
|
|
8327
|
+
if (id.startsWith(convDir + "/")) {
|
|
8328
|
+
const rest = id.slice(convDir.length + 1);
|
|
8329
|
+
const firstSeg = rest.split("/")[0];
|
|
8330
|
+
if (moduleNames.includes(firstSeg)) {
|
|
8331
|
+
result.set(id, firstSeg);
|
|
8332
|
+
matched = true;
|
|
8333
|
+
break;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
if (matched) continue;
|
|
8338
|
+
const module2 = extractModuleFromPath(id);
|
|
8339
|
+
result.set(id, module2);
|
|
8340
|
+
}
|
|
8341
|
+
return result;
|
|
8342
|
+
}
|
|
8343
|
+
};
|
|
8344
|
+
|
|
8345
|
+
// src/server/graph/taggers/screen-tagger.ts
|
|
8346
|
+
var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
|
|
8347
|
+
var screenTagger = {
|
|
8348
|
+
id: "screen",
|
|
8349
|
+
tagKey: "screen",
|
|
8350
|
+
trackUntagged: true,
|
|
8351
|
+
layers: ["ui"],
|
|
8352
|
+
tag(nodes, layer) {
|
|
8353
|
+
if (layer !== "ui") return /* @__PURE__ */ new Map();
|
|
8354
|
+
const result = /* @__PURE__ */ new Map();
|
|
8355
|
+
for (const node of nodes) {
|
|
8356
|
+
if (SCREEN_TYPES.has(node.type)) {
|
|
8357
|
+
result.set(node.id, "true");
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
return result;
|
|
8361
|
+
}
|
|
8362
|
+
};
|
|
8363
|
+
|
|
8364
|
+
// src/server/graph/core/tagger-registry.ts
|
|
8365
|
+
var TaggerRegistry = class {
|
|
8366
|
+
constructor() {
|
|
8367
|
+
this.taggers = [];
|
|
8368
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
8369
|
+
}
|
|
8370
|
+
register(tagger) {
|
|
8371
|
+
if (this.ids.has(tagger.id)) {
|
|
8372
|
+
throw new Error(`Duplicate tagger id: ${tagger.id}`);
|
|
8373
|
+
}
|
|
8374
|
+
this.ids.add(tagger.id);
|
|
8375
|
+
this.taggers.push(tagger);
|
|
8376
|
+
}
|
|
8377
|
+
getAll() {
|
|
8378
|
+
return this.taggers;
|
|
8379
|
+
}
|
|
8380
|
+
getForLayer(layer) {
|
|
8381
|
+
return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
|
|
8382
|
+
}
|
|
8383
|
+
};
|
|
8384
|
+
var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
|
|
8385
|
+
function registerBuiltins2(registry, disabled, config) {
|
|
8386
|
+
for (const tagger of BUILTIN_TAGGERS) {
|
|
8387
|
+
if (disabled.has(tagger.id)) continue;
|
|
8388
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
8389
|
+
if (override !== void 0) {
|
|
8390
|
+
tagger.trackUntagged = override;
|
|
8391
|
+
}
|
|
8392
|
+
registry.register(tagger);
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
8396
|
+
for (const entry of config.taggers?.custom ?? []) {
|
|
8397
|
+
if (disabled.has(entry.id)) continue;
|
|
8398
|
+
try {
|
|
8399
|
+
const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
|
|
8400
|
+
const mod = require(absPath);
|
|
8401
|
+
const tagger = "default" in mod ? mod.default : mod;
|
|
8402
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
8403
|
+
if (override !== void 0) {
|
|
8404
|
+
tagger.trackUntagged = override;
|
|
8405
|
+
}
|
|
8406
|
+
registry.register(tagger);
|
|
8407
|
+
} catch (err2) {
|
|
8408
|
+
process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
|
|
8409
|
+
`);
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
function createTaggerRegistry(config, rootDir) {
|
|
8414
|
+
const registry = new TaggerRegistry();
|
|
8415
|
+
const disabled = new Set(config.taggers?.disabled ?? []);
|
|
8416
|
+
registerBuiltins2(registry, disabled, config);
|
|
8417
|
+
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
8418
|
+
return registry;
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
// src/server/graph/core/tag-store.ts
|
|
8422
|
+
var import_node_fs10 = require("node:fs");
|
|
8423
|
+
var import_node_path12 = require("node:path");
|
|
8424
|
+
var TAGS_FILENAME = "tags.json";
|
|
8180
8425
|
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
8426
|
+
var tagCache = /* @__PURE__ */ new Map();
|
|
8427
|
+
function tagsFilePath(rootDir) {
|
|
8428
|
+
return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
8429
|
+
}
|
|
8430
|
+
function readTagStore(rootDir) {
|
|
8431
|
+
const filePath = tagsFilePath(rootDir);
|
|
8432
|
+
if (!(0, import_node_fs10.existsSync)(filePath)) return {};
|
|
8433
|
+
const stat = (0, import_node_fs10.statSync)(filePath);
|
|
8434
|
+
const cached = tagCache.get(filePath);
|
|
8435
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
8436
|
+
return cached.store;
|
|
8437
|
+
}
|
|
8438
|
+
try {
|
|
8439
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
8440
|
+
const store = JSON.parse(content);
|
|
8441
|
+
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
8442
|
+
return store;
|
|
8443
|
+
} catch {
|
|
8444
|
+
return {};
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
8447
|
+
function writeTagStore(rootDir, store) {
|
|
8448
|
+
const filePath = tagsFilePath(rootDir);
|
|
8449
|
+
const dir = (0, import_node_path12.dirname)(filePath);
|
|
8450
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
8451
|
+
const cleaned = {};
|
|
8452
|
+
for (const [nodeId, tags] of Object.entries(store)) {
|
|
8453
|
+
if (Object.keys(tags).length > 0) {
|
|
8454
|
+
cleaned[nodeId] = tags;
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
(0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
8458
|
+
tagCache.delete(filePath);
|
|
8459
|
+
}
|
|
8460
|
+
function setTag(rootDir, nodeId, key, value) {
|
|
8461
|
+
const store = readTagStore(rootDir);
|
|
8462
|
+
if (!store[nodeId]) store[nodeId] = {};
|
|
8463
|
+
store[nodeId][key] = value;
|
|
8464
|
+
writeTagStore(rootDir, store);
|
|
8465
|
+
}
|
|
8466
|
+
function removeTag(rootDir, nodeId, key) {
|
|
8467
|
+
const store = readTagStore(rootDir);
|
|
8468
|
+
if (!store[nodeId]) return;
|
|
8469
|
+
delete store[nodeId][key];
|
|
8470
|
+
if (Object.keys(store[nodeId]).length === 0) {
|
|
8471
|
+
delete store[nodeId];
|
|
8472
|
+
}
|
|
8473
|
+
writeTagStore(rootDir, store);
|
|
8474
|
+
}
|
|
8475
|
+
|
|
8476
|
+
// src/server/graph/index.ts
|
|
8477
|
+
var GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
8181
8478
|
var LAYERS = ["ui", "api", "db"];
|
|
8182
8479
|
var graphCache = /* @__PURE__ */ new Map();
|
|
8480
|
+
var taggedCache = /* @__PURE__ */ new Map();
|
|
8183
8481
|
function graphsDir(rootDir) {
|
|
8184
|
-
return (0,
|
|
8482
|
+
return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
|
|
8185
8483
|
}
|
|
8186
8484
|
function graphFilePath(rootDir, layer) {
|
|
8187
|
-
return (0,
|
|
8485
|
+
return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
|
|
8486
|
+
}
|
|
8487
|
+
function tagsFilePath2(rootDir) {
|
|
8488
|
+
return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
|
|
8489
|
+
}
|
|
8490
|
+
function getMtimeMs(filePath) {
|
|
8491
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
|
|
8492
|
+
return (0, import_node_fs11.statSync)(filePath).mtimeMs;
|
|
8188
8493
|
}
|
|
8189
8494
|
function invalidateCache(filePath) {
|
|
8190
8495
|
graphCache.delete(filePath);
|
|
8191
8496
|
}
|
|
8192
|
-
function
|
|
8497
|
+
function invalidateTaggedCache(rootDir, layer) {
|
|
8498
|
+
taggedCache.delete(`${rootDir}:${layer}`);
|
|
8499
|
+
}
|
|
8500
|
+
function applyTags(graph, layer, rootDir) {
|
|
8501
|
+
const config = loadConfig(rootDir);
|
|
8502
|
+
const registry = createTaggerRegistry(config, rootDir);
|
|
8503
|
+
const manualTags = readTagStore(rootDir);
|
|
8504
|
+
const taggedNodes = graph.nodes.map((n) => ({ ...n }));
|
|
8505
|
+
const taggers = registry.getForLayer(layer);
|
|
8506
|
+
for (const tagger of taggers) {
|
|
8507
|
+
const assignments = tagger.tag(taggedNodes, layer, rootDir);
|
|
8508
|
+
for (const node of taggedNodes) {
|
|
8509
|
+
if (!node.tags) node.tags = {};
|
|
8510
|
+
const tags = node.tags;
|
|
8511
|
+
const value = assignments.get(node.id);
|
|
8512
|
+
if (value !== void 0) {
|
|
8513
|
+
tags[tagger.tagKey] = value;
|
|
8514
|
+
} else if (tagger.trackUntagged) {
|
|
8515
|
+
tags[tagger.tagKey] = "untagged";
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
for (const node of taggedNodes) {
|
|
8520
|
+
const manual = manualTags[node.id];
|
|
8521
|
+
if (manual) {
|
|
8522
|
+
if (!node.tags) node.tags = {};
|
|
8523
|
+
const tags = node.tags;
|
|
8524
|
+
Object.assign(tags, manual);
|
|
8525
|
+
}
|
|
8526
|
+
}
|
|
8527
|
+
return { ...graph, nodes: taggedNodes };
|
|
8528
|
+
}
|
|
8529
|
+
function readGraphRaw(rootDir, layer) {
|
|
8193
8530
|
const filePath = graphFilePath(rootDir, layer);
|
|
8194
|
-
if (!(0,
|
|
8195
|
-
const stat = (0,
|
|
8531
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return null;
|
|
8532
|
+
const stat = (0, import_node_fs11.statSync)(filePath);
|
|
8196
8533
|
const cached = graphCache.get(filePath);
|
|
8197
8534
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
8198
8535
|
return cached.graph;
|
|
8199
8536
|
}
|
|
8200
|
-
const content = (0,
|
|
8537
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
8201
8538
|
const graph = JSON.parse(content);
|
|
8202
8539
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
8203
8540
|
return graph;
|
|
8204
8541
|
}
|
|
8542
|
+
function readGraph(rootDir, layer) {
|
|
8543
|
+
const rawFilePath = graphFilePath(rootDir, layer);
|
|
8544
|
+
if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
|
|
8545
|
+
const rawMtime = getMtimeMs(rawFilePath);
|
|
8546
|
+
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
8547
|
+
const cacheKey = `${rootDir}:${layer}`;
|
|
8548
|
+
const cached = taggedCache.get(cacheKey);
|
|
8549
|
+
if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
|
|
8550
|
+
return cached.graph;
|
|
8551
|
+
}
|
|
8552
|
+
const raw = readGraphRaw(rootDir, layer);
|
|
8553
|
+
if (!raw) return null;
|
|
8554
|
+
const tagged = applyTags(raw, layer, rootDir);
|
|
8555
|
+
taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
|
|
8556
|
+
return tagged;
|
|
8557
|
+
}
|
|
8205
8558
|
function readAllGraphs(rootDir) {
|
|
8206
8559
|
const result = {};
|
|
8207
8560
|
for (const layer of LAYERS) {
|
|
@@ -8212,12 +8565,13 @@ function readAllGraphs(rootDir) {
|
|
|
8212
8565
|
}
|
|
8213
8566
|
function generateGraph(rootDir, layer) {
|
|
8214
8567
|
const dir = graphsDir(rootDir);
|
|
8215
|
-
(0,
|
|
8568
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
8216
8569
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
8217
8570
|
for (const result of results) {
|
|
8218
8571
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
8219
|
-
(0,
|
|
8572
|
+
(0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
8220
8573
|
invalidateCache(filePath);
|
|
8574
|
+
invalidateTaggedCache(rootDir, result.layer);
|
|
8221
8575
|
}
|
|
8222
8576
|
return results;
|
|
8223
8577
|
}
|
|
@@ -8278,27 +8632,46 @@ function handleGraphCommand(subcommand, args) {
|
|
|
8278
8632
|
}
|
|
8279
8633
|
|
|
8280
8634
|
// src/server/graph-mcp.ts
|
|
8281
|
-
var
|
|
8282
|
-
var
|
|
8635
|
+
var import_node_fs13 = require("node:fs");
|
|
8636
|
+
var import_node_path15 = require("node:path");
|
|
8283
8637
|
var import_node_child_process2 = require("node:child_process");
|
|
8284
8638
|
var import_node_os2 = require("node:os");
|
|
8285
8639
|
|
|
8286
8640
|
// src/server/lockfile.ts
|
|
8287
8641
|
var import_node_child_process = require("node:child_process");
|
|
8288
|
-
var
|
|
8642
|
+
var import_node_fs12 = require("node:fs");
|
|
8289
8643
|
var import_node_os = require("node:os");
|
|
8290
|
-
var
|
|
8291
|
-
function lockDir() {
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
return (0,
|
|
8296
|
-
}
|
|
8297
|
-
function
|
|
8298
|
-
|
|
8299
|
-
|
|
8644
|
+
var import_node_path14 = require("node:path");
|
|
8645
|
+
function lockDir(projectRoot) {
|
|
8646
|
+
if (projectRoot) {
|
|
8647
|
+
return (0, import_node_path14.join)(projectRoot, ".launchsecure");
|
|
8648
|
+
}
|
|
8649
|
+
return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
8650
|
+
}
|
|
8651
|
+
function lockPath(projectRoot) {
|
|
8652
|
+
return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
8653
|
+
}
|
|
8654
|
+
var _activeProjectRoot;
|
|
8655
|
+
function readLock(projectRoot) {
|
|
8656
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
8657
|
+
const p = lockPath(root);
|
|
8658
|
+
if (!(0, import_node_fs12.existsSync)(p)) {
|
|
8659
|
+
if (root) {
|
|
8660
|
+
const globalP = lockPath();
|
|
8661
|
+
if ((0, import_node_fs12.existsSync)(globalP)) {
|
|
8662
|
+
try {
|
|
8663
|
+
const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
|
|
8664
|
+
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
8665
|
+
return data;
|
|
8666
|
+
}
|
|
8667
|
+
} catch {
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
8671
|
+
return null;
|
|
8672
|
+
}
|
|
8300
8673
|
try {
|
|
8301
|
-
const data = JSON.parse((0,
|
|
8674
|
+
const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
|
|
8302
8675
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
8303
8676
|
return data;
|
|
8304
8677
|
} catch {
|
|
@@ -8327,28 +8700,31 @@ function getListenerPid(port) {
|
|
|
8327
8700
|
return null;
|
|
8328
8701
|
}
|
|
8329
8702
|
}
|
|
8330
|
-
function getLiveLock() {
|
|
8331
|
-
const
|
|
8703
|
+
function getLiveLock(projectRoot) {
|
|
8704
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
8705
|
+
const lock = readLock(root);
|
|
8332
8706
|
if (!lock) return null;
|
|
8333
8707
|
const listenerPid = getListenerPid(lock.port);
|
|
8334
8708
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
8335
8709
|
if (!live) {
|
|
8336
8710
|
try {
|
|
8337
|
-
(0,
|
|
8711
|
+
(0, import_node_fs12.unlinkSync)(lockPath(root));
|
|
8338
8712
|
} catch {
|
|
8339
8713
|
}
|
|
8340
8714
|
return null;
|
|
8341
8715
|
}
|
|
8342
8716
|
return lock;
|
|
8343
8717
|
}
|
|
8344
|
-
function clearLock() {
|
|
8718
|
+
function clearLock(projectRoot) {
|
|
8719
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
8345
8720
|
try {
|
|
8346
|
-
(0,
|
|
8721
|
+
(0, import_node_fs12.unlinkSync)(lockPath(root));
|
|
8347
8722
|
} catch {
|
|
8348
8723
|
}
|
|
8349
8724
|
}
|
|
8350
8725
|
|
|
8351
8726
|
// src/server/graph-mcp.ts
|
|
8727
|
+
init_config();
|
|
8352
8728
|
var SERVER_INFO = {
|
|
8353
8729
|
name: "launchsecure-graph",
|
|
8354
8730
|
version: "0.0.1"
|
|
@@ -8370,7 +8746,7 @@ var TOOLS = [
|
|
|
8370
8746
|
},
|
|
8371
8747
|
{
|
|
8372
8748
|
name: "read_graph",
|
|
8373
|
-
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (
|
|
8749
|
+
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
8374
8750
|
inputSchema: {
|
|
8375
8751
|
type: "object",
|
|
8376
8752
|
properties: {
|
|
@@ -8389,7 +8765,15 @@ var TOOLS = [
|
|
|
8389
8765
|
},
|
|
8390
8766
|
module: {
|
|
8391
8767
|
type: "string",
|
|
8392
|
-
description: '
|
|
8768
|
+
description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
|
|
8769
|
+
},
|
|
8770
|
+
tag_key: {
|
|
8771
|
+
type: "string",
|
|
8772
|
+
description: "Filter by arbitrary tag key. Must be used with tag_value."
|
|
8773
|
+
},
|
|
8774
|
+
tag_value: {
|
|
8775
|
+
type: "string",
|
|
8776
|
+
description: "Filter by tag value for the given tag_key."
|
|
8393
8777
|
},
|
|
8394
8778
|
node_id: {
|
|
8395
8779
|
type: "string",
|
|
@@ -8513,6 +8897,46 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
8513
8897
|
type: "object",
|
|
8514
8898
|
properties: {}
|
|
8515
8899
|
}
|
|
8900
|
+
},
|
|
8901
|
+
{
|
|
8902
|
+
name: "add_tag",
|
|
8903
|
+
description: 'Tag a graph node with a key-value pair. Tags persist in .launchsecure/graphs/tags.json and survive graph regeneration. Use for annotating nodes with arbitrary metadata (e.g. "refactor_later", "owner", "priority"). Manual tags override computed tags (like module and screen) for the same key.',
|
|
8904
|
+
inputSchema: {
|
|
8905
|
+
type: "object",
|
|
8906
|
+
properties: {
|
|
8907
|
+
node_id: {
|
|
8908
|
+
type: "string",
|
|
8909
|
+
description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
|
|
8910
|
+
},
|
|
8911
|
+
key: {
|
|
8912
|
+
type: "string",
|
|
8913
|
+
description: 'Tag key (e.g. "module", "owner", "refactor_later").'
|
|
8914
|
+
},
|
|
8915
|
+
value: {
|
|
8916
|
+
type: "string",
|
|
8917
|
+
description: 'Tag value (e.g. "auth", "alice", "true").'
|
|
8918
|
+
}
|
|
8919
|
+
},
|
|
8920
|
+
required: ["node_id", "key", "value"]
|
|
8921
|
+
}
|
|
8922
|
+
},
|
|
8923
|
+
{
|
|
8924
|
+
name: "remove_tag",
|
|
8925
|
+
description: "Remove a manual tag from a graph node. Only removes tags from tags.json \u2014 computed tags (module, screen) cannot be removed (they are re-derived at read time).",
|
|
8926
|
+
inputSchema: {
|
|
8927
|
+
type: "object",
|
|
8928
|
+
properties: {
|
|
8929
|
+
node_id: {
|
|
8930
|
+
type: "string",
|
|
8931
|
+
description: "The node id to remove the tag from."
|
|
8932
|
+
},
|
|
8933
|
+
key: {
|
|
8934
|
+
type: "string",
|
|
8935
|
+
description: "Tag key to remove."
|
|
8936
|
+
}
|
|
8937
|
+
},
|
|
8938
|
+
required: ["node_id", "key"]
|
|
8939
|
+
}
|
|
8516
8940
|
}
|
|
8517
8941
|
];
|
|
8518
8942
|
function matchesSearch(node, query) {
|
|
@@ -8526,7 +8950,7 @@ function matchesSearch(node, query) {
|
|
|
8526
8950
|
function toMinimal(nodes) {
|
|
8527
8951
|
return nodes.map((n) => {
|
|
8528
8952
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
8529
|
-
if (n.
|
|
8953
|
+
if (n.tags != null) out.tags = n.tags;
|
|
8530
8954
|
if (n.route != null) out.route = n.route;
|
|
8531
8955
|
if (n.methods != null) out.methods = n.methods;
|
|
8532
8956
|
return out;
|
|
@@ -8537,11 +8961,12 @@ var COMPACT_SCHEMA = {
|
|
|
8537
8961
|
i: "id",
|
|
8538
8962
|
t: "type",
|
|
8539
8963
|
n: "name",
|
|
8540
|
-
m: "module",
|
|
8964
|
+
m: "module (from tags)",
|
|
8541
8965
|
r: "route",
|
|
8542
8966
|
mt: "methods",
|
|
8543
8967
|
x: "exports",
|
|
8544
|
-
c: "columns"
|
|
8968
|
+
c: "columns",
|
|
8969
|
+
tg: "tags"
|
|
8545
8970
|
},
|
|
8546
8971
|
edges: {
|
|
8547
8972
|
s: "source_node_index",
|
|
@@ -8559,7 +8984,8 @@ var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
|
8559
8984
|
"route",
|
|
8560
8985
|
"methods",
|
|
8561
8986
|
"exports",
|
|
8562
|
-
"columns"
|
|
8987
|
+
"columns",
|
|
8988
|
+
"tags"
|
|
8563
8989
|
]);
|
|
8564
8990
|
var EST_CHARS_PER_NODE_FULL = {
|
|
8565
8991
|
ui: 300,
|
|
@@ -8580,11 +9006,13 @@ var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
|
8580
9006
|
var BATCH_BUDGET_CHARS = 6e4;
|
|
8581
9007
|
function toCompactNode(n) {
|
|
8582
9008
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
8583
|
-
|
|
9009
|
+
const tags = n.tags;
|
|
9010
|
+
if (tags?.module) out.m = tags.module;
|
|
8584
9011
|
if (n.route != null) out.r = n.route;
|
|
8585
9012
|
if (n.methods != null) out.mt = n.methods;
|
|
8586
9013
|
if (n.exports != null) out.x = n.exports;
|
|
8587
9014
|
if (n.columns != null) out.c = n.columns;
|
|
9015
|
+
if (tags != null) out.tg = tags;
|
|
8588
9016
|
for (const k of Object.keys(n)) {
|
|
8589
9017
|
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
8590
9018
|
}
|
|
@@ -8660,7 +9088,8 @@ function layerSummary(graph) {
|
|
|
8660
9088
|
const moduleCounts = {};
|
|
8661
9089
|
for (const n of graph.nodes) {
|
|
8662
9090
|
typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
|
|
8663
|
-
const
|
|
9091
|
+
const tags = n.tags;
|
|
9092
|
+
const mod = tags?.module;
|
|
8664
9093
|
if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
|
|
8665
9094
|
}
|
|
8666
9095
|
const edgeTypeCounts = {};
|
|
@@ -8717,12 +9146,14 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
8717
9146
|
const search = args.search;
|
|
8718
9147
|
const type = args.type;
|
|
8719
9148
|
const module_ = args.module;
|
|
9149
|
+
const tagKey = args.tag_key;
|
|
9150
|
+
const tagValue = args.tag_value;
|
|
8720
9151
|
const nodeId = args.node_id;
|
|
8721
9152
|
const hops = args.hops ?? 1;
|
|
8722
9153
|
const layerIsDb = args.layer === "db";
|
|
8723
9154
|
const minimal = args.minimal ?? layerIsDb;
|
|
8724
9155
|
const includeEdges = args.include_edges;
|
|
8725
|
-
const hasFilter = !!(search || type || module_ || nodeId);
|
|
9156
|
+
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
8726
9157
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
8727
9158
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
8728
9159
|
}
|
|
@@ -8778,7 +9209,9 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
8778
9209
|
const matched = graph.nodes.filter((n) => {
|
|
8779
9210
|
if (search && !matchesSearch(n, search)) return false;
|
|
8780
9211
|
if (type && n.type !== type) return false;
|
|
8781
|
-
|
|
9212
|
+
const nodeTags = n.tags;
|
|
9213
|
+
if (module_ && nodeTags?.module !== module_) return false;
|
|
9214
|
+
if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
|
|
8782
9215
|
return true;
|
|
8783
9216
|
});
|
|
8784
9217
|
const matchedIds = new Set(matched.map((n) => n.id));
|
|
@@ -8865,9 +9298,9 @@ function handleReadGraph(args) {
|
|
|
8865
9298
|
return okJson(result);
|
|
8866
9299
|
}
|
|
8867
9300
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
8868
|
-
if (layer === "ui") return (0,
|
|
8869
|
-
if (layer === "api") return (0,
|
|
8870
|
-
if (layer === "db") return (0,
|
|
9301
|
+
if (layer === "ui") return (0, import_node_path15.join)(rootDir, "src", nodeId);
|
|
9302
|
+
if (layer === "api") return (0, import_node_path15.join)(rootDir, nodeId);
|
|
9303
|
+
if (layer === "db") return (0, import_node_path15.join)(rootDir, "prisma", "schema.prisma");
|
|
8871
9304
|
return null;
|
|
8872
9305
|
}
|
|
8873
9306
|
function handleGrepNodes(args) {
|
|
@@ -8927,11 +9360,11 @@ function handleGrepNodes(args) {
|
|
|
8927
9360
|
let filesSearched = 0;
|
|
8928
9361
|
let truncated = false;
|
|
8929
9362
|
for (const [filePath, nodeId] of filePaths) {
|
|
8930
|
-
if (!(0,
|
|
9363
|
+
if (!(0, import_node_fs13.existsSync)(filePath)) continue;
|
|
8931
9364
|
filesSearched++;
|
|
8932
9365
|
let content;
|
|
8933
9366
|
try {
|
|
8934
|
-
content = (0,
|
|
9367
|
+
content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
|
|
8935
9368
|
} catch {
|
|
8936
9369
|
continue;
|
|
8937
9370
|
}
|
|
@@ -8969,7 +9402,8 @@ function handleGrepNodes(args) {
|
|
|
8969
9402
|
});
|
|
8970
9403
|
}
|
|
8971
9404
|
function handleChartServerStatus() {
|
|
8972
|
-
const
|
|
9405
|
+
const rootDir = process.cwd();
|
|
9406
|
+
const lock = getLiveLock(rootDir);
|
|
8973
9407
|
if (!lock) {
|
|
8974
9408
|
return okJson({ running: false });
|
|
8975
9409
|
}
|
|
@@ -8983,7 +9417,8 @@ function handleChartServerStatus() {
|
|
|
8983
9417
|
});
|
|
8984
9418
|
}
|
|
8985
9419
|
function handleStartChartServer(args) {
|
|
8986
|
-
const
|
|
9420
|
+
const rootDir = process.cwd();
|
|
9421
|
+
const lock = getLiveLock(rootDir);
|
|
8987
9422
|
if (lock) {
|
|
8988
9423
|
return okJson({
|
|
8989
9424
|
started: false,
|
|
@@ -8994,11 +9429,11 @@ function handleStartChartServer(args) {
|
|
|
8994
9429
|
});
|
|
8995
9430
|
}
|
|
8996
9431
|
const entryPath = process.argv[1];
|
|
8997
|
-
const logDir = (0,
|
|
8998
|
-
(0,
|
|
8999
|
-
const logPath = (0,
|
|
9000
|
-
const out = (0,
|
|
9001
|
-
const err2 = (0,
|
|
9432
|
+
const logDir = (0, import_node_path15.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
9433
|
+
(0, import_node_fs13.mkdirSync)(logDir, { recursive: true });
|
|
9434
|
+
const logPath = (0, import_node_path15.join)(logDir, "launch-chart.log");
|
|
9435
|
+
const out = (0, import_node_fs13.openSync)(logPath, "a");
|
|
9436
|
+
const err2 = (0, import_node_fs13.openSync)(logPath, "a");
|
|
9002
9437
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
9003
9438
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
9004
9439
|
detached: true,
|
|
@@ -9013,7 +9448,8 @@ function handleStartChartServer(args) {
|
|
|
9013
9448
|
});
|
|
9014
9449
|
}
|
|
9015
9450
|
function handleStopChartServer() {
|
|
9016
|
-
const
|
|
9451
|
+
const rootDir = process.cwd();
|
|
9452
|
+
const lock = getLiveLock(rootDir);
|
|
9017
9453
|
if (!lock) {
|
|
9018
9454
|
return okJson({ stopped: false, reason: "not_running" });
|
|
9019
9455
|
}
|
|
@@ -9023,14 +9459,45 @@ function handleStopChartServer() {
|
|
|
9023
9459
|
} catch (e) {
|
|
9024
9460
|
const code = e.code;
|
|
9025
9461
|
if (code === "ESRCH") {
|
|
9026
|
-
clearLock();
|
|
9462
|
+
clearLock(rootDir);
|
|
9027
9463
|
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
9028
9464
|
}
|
|
9029
9465
|
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
9030
9466
|
}
|
|
9031
9467
|
}
|
|
9468
|
+
function handleAddTag(args) {
|
|
9469
|
+
const rootDir = process.cwd();
|
|
9470
|
+
const nodeId = args.node_id;
|
|
9471
|
+
const key = args.key;
|
|
9472
|
+
const value = args.value;
|
|
9473
|
+
if (!nodeId) return err("node_id is required");
|
|
9474
|
+
if (!key) return err("key is required");
|
|
9475
|
+
if (!value) return err("value is required");
|
|
9476
|
+
const graphs = readAllGraphs(rootDir);
|
|
9477
|
+
let found = false;
|
|
9478
|
+
for (const graph of Object.values(graphs)) {
|
|
9479
|
+
if (graph && graph.nodes.some((n) => n.id === nodeId)) {
|
|
9480
|
+
found = true;
|
|
9481
|
+
break;
|
|
9482
|
+
}
|
|
9483
|
+
}
|
|
9484
|
+
if (!found) {
|
|
9485
|
+
return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
|
|
9486
|
+
}
|
|
9487
|
+
setTag(rootDir, nodeId, key, value);
|
|
9488
|
+
return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
|
|
9489
|
+
}
|
|
9490
|
+
function handleRemoveTag(args) {
|
|
9491
|
+
const rootDir = process.cwd();
|
|
9492
|
+
const nodeId = args.node_id;
|
|
9493
|
+
const key = args.key;
|
|
9494
|
+
if (!nodeId) return err("node_id is required");
|
|
9495
|
+
if (!key) return err("key is required");
|
|
9496
|
+
removeTag(rootDir, nodeId, key);
|
|
9497
|
+
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
9498
|
+
}
|
|
9032
9499
|
function handleDetectProjectStack() {
|
|
9033
|
-
const rootDir =
|
|
9500
|
+
const rootDir = process.cwd();
|
|
9034
9501
|
const parsers = [
|
|
9035
9502
|
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
9036
9503
|
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
@@ -9048,20 +9515,20 @@ function handleDetectProjectStack() {
|
|
|
9048
9515
|
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
9049
9516
|
}
|
|
9050
9517
|
}
|
|
9051
|
-
const srcDir = (0,
|
|
9052
|
-
if ((0,
|
|
9518
|
+
const srcDir = (0, import_node_path15.join)(rootDir, "src");
|
|
9519
|
+
if ((0, import_node_fs13.existsSync)(srcDir)) {
|
|
9053
9520
|
const scanDir = (dir) => {
|
|
9054
|
-
if (!(0,
|
|
9055
|
-
for (const entry of (0,
|
|
9521
|
+
if (!(0, import_node_fs13.existsSync)(dir)) return;
|
|
9522
|
+
for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
|
|
9056
9523
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
9057
|
-
const full = (0,
|
|
9524
|
+
const full = (0, import_node_path15.join)(dir, entry.name);
|
|
9058
9525
|
if (entry.isDirectory()) {
|
|
9059
9526
|
scanDir(full);
|
|
9060
9527
|
continue;
|
|
9061
9528
|
}
|
|
9062
|
-
if (![".ts", ".tsx"].includes((0,
|
|
9529
|
+
if (![".ts", ".tsx"].includes((0, import_node_path15.extname)(entry.name))) continue;
|
|
9063
9530
|
try {
|
|
9064
|
-
const content = (0,
|
|
9531
|
+
const content = (0, import_node_fs13.readFileSync)(full, "utf-8");
|
|
9065
9532
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
9066
9533
|
if (matches) stats.annotations += matches.length;
|
|
9067
9534
|
} catch {
|
|
@@ -9148,6 +9615,14 @@ function handleMessage(msg) {
|
|
|
9148
9615
|
respond(id ?? null, handleDetectProjectStack());
|
|
9149
9616
|
return;
|
|
9150
9617
|
}
|
|
9618
|
+
if (toolName === "add_tag") {
|
|
9619
|
+
respond(id ?? null, handleAddTag(args));
|
|
9620
|
+
return;
|
|
9621
|
+
}
|
|
9622
|
+
if (toolName === "remove_tag") {
|
|
9623
|
+
respond(id ?? null, handleRemoveTag(args));
|
|
9624
|
+
return;
|
|
9625
|
+
}
|
|
9151
9626
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
9152
9627
|
return;
|
|
9153
9628
|
}
|
|
@@ -9245,7 +9720,7 @@ function parseArgs() {
|
|
|
9245
9720
|
return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
|
|
9246
9721
|
}
|
|
9247
9722
|
function tryListen(server, port, maxRetries = 10) {
|
|
9248
|
-
return new Promise((
|
|
9723
|
+
return new Promise((resolve3, reject) => {
|
|
9249
9724
|
let attempts = 0;
|
|
9250
9725
|
function attempt(p) {
|
|
9251
9726
|
server.once("error", (err2) => {
|
|
@@ -9256,7 +9731,7 @@ function tryListen(server, port, maxRetries = 10) {
|
|
|
9256
9731
|
reject(err2);
|
|
9257
9732
|
}
|
|
9258
9733
|
});
|
|
9259
|
-
server.listen(p, () =>
|
|
9734
|
+
server.listen(p, () => resolve3(p));
|
|
9260
9735
|
}
|
|
9261
9736
|
attempt(port);
|
|
9262
9737
|
});
|
|
@@ -9277,7 +9752,7 @@ function saveCredentials(creds) {
|
|
|
9277
9752
|
});
|
|
9278
9753
|
}
|
|
9279
9754
|
function verifyToken(serverUrl, token) {
|
|
9280
|
-
return new Promise((
|
|
9755
|
+
return new Promise((resolve3) => {
|
|
9281
9756
|
const url = new URL("/api/mcp/verify", serverUrl);
|
|
9282
9757
|
const body = JSON.stringify({ token });
|
|
9283
9758
|
const mod = url.protocol === "https:" ? import_https.default : import_http.default;
|
|
@@ -9292,30 +9767,30 @@ function verifyToken(serverUrl, token) {
|
|
|
9292
9767
|
res.on("data", (chunk) => data += chunk);
|
|
9293
9768
|
res.on("end", () => {
|
|
9294
9769
|
try {
|
|
9295
|
-
|
|
9770
|
+
resolve3(JSON.parse(data));
|
|
9296
9771
|
} catch {
|
|
9297
|
-
|
|
9772
|
+
resolve3({ valid: false, error: "Invalid response from server" });
|
|
9298
9773
|
}
|
|
9299
9774
|
});
|
|
9300
9775
|
});
|
|
9301
9776
|
req.on("error", (err2) => {
|
|
9302
|
-
|
|
9777
|
+
resolve3({ valid: false, error: `Cannot reach server: ${err2.message}` });
|
|
9303
9778
|
});
|
|
9304
9779
|
req.setTimeout(1e4, () => {
|
|
9305
9780
|
req.destroy();
|
|
9306
|
-
|
|
9781
|
+
resolve3({ valid: false, error: "Connection timed out" });
|
|
9307
9782
|
});
|
|
9308
9783
|
req.write(body);
|
|
9309
9784
|
req.end();
|
|
9310
9785
|
});
|
|
9311
9786
|
}
|
|
9312
9787
|
function httpRequest(reqUrl, options, body, timeout = 3e4) {
|
|
9313
|
-
return new Promise((
|
|
9788
|
+
return new Promise((resolve3, reject) => {
|
|
9314
9789
|
const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
|
|
9315
9790
|
const r = mod.request(reqUrl, options, (resp) => {
|
|
9316
9791
|
let data = "";
|
|
9317
9792
|
resp.on("data", (chunk) => data += chunk);
|
|
9318
|
-
resp.on("end", () =>
|
|
9793
|
+
resp.on("end", () => resolve3({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
|
|
9319
9794
|
});
|
|
9320
9795
|
r.on("error", reject);
|
|
9321
9796
|
r.setTimeout(timeout, () => {
|