@launchsecure/launch-kit 0.0.5 → 0.0.7
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-0Xm1mXjM.js +379 -0
- package/dist/chart-client/assets/{index-DFslt72L.css → index-C-OUsIfD.css} +1 -1
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/{index-DCC--GO-.js → index-Ci95xk2_.js} +1 -1
- package/dist/client/assets/index-DbqEe7we.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +611 -99
- package/dist/server/cli.js +621 -124
- package/dist/server/graph-mcp-entry.js +760 -129
- package/package.json +1 -1
- package/dist/chart-client/assets/index-BUih0oqR.js +0 -358
- package/dist/client/assets/index-BCxRNp8I.css +0 -32
|
@@ -29,17 +29,39 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
33
|
|
|
33
34
|
// src/server/lockfile.ts
|
|
34
|
-
function lockDir() {
|
|
35
|
+
function lockDir(projectRoot) {
|
|
36
|
+
if (projectRoot) {
|
|
37
|
+
return (0, import_node_path.join)(projectRoot, ".launchsecure");
|
|
38
|
+
}
|
|
35
39
|
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
36
40
|
}
|
|
37
|
-
function lockPath() {
|
|
38
|
-
return (0, import_node_path.join)(lockDir(), "launch-chart.lock");
|
|
41
|
+
function lockPath(projectRoot) {
|
|
42
|
+
return (0, import_node_path.join)(lockDir(projectRoot), "launch-chart.lock");
|
|
43
|
+
}
|
|
44
|
+
function setProjectRoot(root) {
|
|
45
|
+
_activeProjectRoot = root;
|
|
39
46
|
}
|
|
40
|
-
function readLock() {
|
|
41
|
-
const
|
|
42
|
-
|
|
47
|
+
function readLock(projectRoot) {
|
|
48
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
49
|
+
const p = lockPath(root);
|
|
50
|
+
if (!(0, import_node_fs.existsSync)(p)) {
|
|
51
|
+
if (root) {
|
|
52
|
+
const globalP = lockPath();
|
|
53
|
+
if ((0, import_node_fs.existsSync)(globalP)) {
|
|
54
|
+
try {
|
|
55
|
+
const data = JSON.parse((0, import_node_fs.readFileSync)(globalP, "utf-8"));
|
|
56
|
+
if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
43
65
|
try {
|
|
44
66
|
const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
|
|
45
67
|
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
@@ -70,31 +92,35 @@ function getListenerPid(port) {
|
|
|
70
92
|
return null;
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
|
-
function getLiveLock() {
|
|
74
|
-
const
|
|
95
|
+
function getLiveLock(projectRoot) {
|
|
96
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
97
|
+
const lock = readLock(root);
|
|
75
98
|
if (!lock) return null;
|
|
76
99
|
const listenerPid = getListenerPid(lock.port);
|
|
77
100
|
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
78
101
|
if (!live) {
|
|
79
102
|
try {
|
|
80
|
-
(0, import_node_fs.unlinkSync)(lockPath());
|
|
103
|
+
(0, import_node_fs.unlinkSync)(lockPath(root));
|
|
81
104
|
} catch {
|
|
82
105
|
}
|
|
83
106
|
return null;
|
|
84
107
|
}
|
|
85
108
|
return lock;
|
|
86
109
|
}
|
|
87
|
-
function writeLock(data) {
|
|
88
|
-
|
|
89
|
-
(0, import_node_fs.
|
|
110
|
+
function writeLock(data, projectRoot) {
|
|
111
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
112
|
+
(0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
|
|
113
|
+
(0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
114
|
+
if (root) _activeProjectRoot = root;
|
|
90
115
|
}
|
|
91
|
-
function clearLock() {
|
|
116
|
+
function clearLock(projectRoot) {
|
|
117
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
92
118
|
try {
|
|
93
|
-
(0, import_node_fs.unlinkSync)(lockPath());
|
|
119
|
+
(0, import_node_fs.unlinkSync)(lockPath(root));
|
|
94
120
|
} catch {
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
|
-
var import_node_child_process, import_node_fs, import_node_os, import_node_path;
|
|
123
|
+
var import_node_child_process, import_node_fs, import_node_os, import_node_path, _activeProjectRoot;
|
|
98
124
|
var init_lockfile = __esm({
|
|
99
125
|
"src/server/lockfile.ts"() {
|
|
100
126
|
"use strict";
|
|
@@ -106,6 +132,10 @@ var init_lockfile = __esm({
|
|
|
106
132
|
});
|
|
107
133
|
|
|
108
134
|
// src/server/graph/core/config.ts
|
|
135
|
+
var config_exports = {};
|
|
136
|
+
__export(config_exports, {
|
|
137
|
+
loadConfig: () => loadConfig
|
|
138
|
+
});
|
|
109
139
|
function loadConfig(rootDir) {
|
|
110
140
|
const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
|
|
111
141
|
if (!(0, import_node_fs2.existsSync)(configPath)) return {};
|
|
@@ -586,34 +616,6 @@ function classifyType(id) {
|
|
|
586
616
|
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
587
617
|
return "component";
|
|
588
618
|
}
|
|
589
|
-
function classifyModule(id) {
|
|
590
|
-
if (/app\/\(auth\)\//.test(id)) return "auth";
|
|
591
|
-
if (/app\/\(admin\)\//.test(id)) return "admin";
|
|
592
|
-
if (/app\/\(settings\)\//.test(id)) return "settings";
|
|
593
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
|
|
594
|
-
if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
|
|
595
|
-
if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
|
|
596
|
-
if (id.startsWith("app/integrations/")) return "integrations";
|
|
597
|
-
if (id.startsWith("app/docs/")) return "admin";
|
|
598
|
-
if (id.startsWith("client/components/ui/")) return "shared-ui";
|
|
599
|
-
if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
|
|
600
|
-
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";
|
|
601
|
-
if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
|
|
602
|
-
if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
|
|
603
|
-
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";
|
|
604
|
-
if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
|
|
605
|
-
if (id.startsWith("server/auth/")) return "auth";
|
|
606
|
-
if (id.startsWith("server/mcp/")) return "mcp";
|
|
607
|
-
if (id.startsWith("server/lib/")) return "server-lib";
|
|
608
|
-
if (id.startsWith("server/middleware")) return "middleware";
|
|
609
|
-
if (id.startsWith("server/services/")) return "services";
|
|
610
|
-
if (id.startsWith("server/db")) return "db";
|
|
611
|
-
if (id.startsWith("server/errors")) return "errors";
|
|
612
|
-
if (id.startsWith("server/")) return "server-lib";
|
|
613
|
-
if (id.startsWith("config/")) return "config";
|
|
614
|
-
if (id.startsWith("lib/")) return "lib";
|
|
615
|
-
return "root";
|
|
616
|
-
}
|
|
617
619
|
function extractRoute(id) {
|
|
618
620
|
if (!id.endsWith("/page.tsx")) return null;
|
|
619
621
|
let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
|
|
@@ -834,8 +836,7 @@ function generate(rootDir) {
|
|
|
834
836
|
const parsed = parsedByPath.get(absPath);
|
|
835
837
|
const name = parsed.name || nameFromFilename(absPath);
|
|
836
838
|
const route = extractRoute(id);
|
|
837
|
-
|
|
838
|
-
nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
|
|
839
|
+
nodes.push({ id, type, name, route, exports: parsed.exports });
|
|
839
840
|
nodeIdSet.add(id);
|
|
840
841
|
nodeTypeMap.set(id, type);
|
|
841
842
|
if (route) routeToNodeId.set(route, id);
|
|
@@ -939,7 +940,6 @@ function generate(rootDir) {
|
|
|
939
940
|
type: "external",
|
|
940
941
|
name: parsed.name || nameFromFilename(absPath),
|
|
941
942
|
route: null,
|
|
942
|
-
module: "external",
|
|
943
943
|
exports: parsed.exports
|
|
944
944
|
});
|
|
945
945
|
nodeIdSet.add(externalId);
|
|
@@ -2031,29 +2031,443 @@ var init_graph_builder = __esm({
|
|
|
2031
2031
|
}
|
|
2032
2032
|
});
|
|
2033
2033
|
|
|
2034
|
+
// src/server/graph/taggers/module-tagger.ts
|
|
2035
|
+
function matchGlob(pattern, id) {
|
|
2036
|
+
const patParts = pattern.split("/");
|
|
2037
|
+
const idParts = id.split("/");
|
|
2038
|
+
return matchParts(patParts, 0, idParts, 0);
|
|
2039
|
+
}
|
|
2040
|
+
function matchParts(pat, pi, id, ii) {
|
|
2041
|
+
while (pi < pat.length && ii < id.length) {
|
|
2042
|
+
const p = pat[pi];
|
|
2043
|
+
if (p === "**") {
|
|
2044
|
+
for (let skip = ii; skip <= id.length; skip++) {
|
|
2045
|
+
if (matchParts(pat, pi + 1, id, skip)) return true;
|
|
2046
|
+
}
|
|
2047
|
+
return false;
|
|
2048
|
+
}
|
|
2049
|
+
if (p === "*") {
|
|
2050
|
+
pi++;
|
|
2051
|
+
ii++;
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (p !== id[ii]) return false;
|
|
2055
|
+
pi++;
|
|
2056
|
+
ii++;
|
|
2057
|
+
}
|
|
2058
|
+
while (pi < pat.length && pat[pi] === "**") pi++;
|
|
2059
|
+
return pi === pat.length && ii === id.length;
|
|
2060
|
+
}
|
|
2061
|
+
function detectConventionDirs(rootDir) {
|
|
2062
|
+
const result = /* @__PURE__ */ new Map();
|
|
2063
|
+
const searchDirs = [
|
|
2064
|
+
rootDir,
|
|
2065
|
+
(0, import_node_path11.join)(rootDir, "src"),
|
|
2066
|
+
(0, import_node_path11.join)(rootDir, "app"),
|
|
2067
|
+
(0, import_node_path11.join)(rootDir, "lib")
|
|
2068
|
+
];
|
|
2069
|
+
for (const base of searchDirs) {
|
|
2070
|
+
for (const convention of CONVENTION_DIRS) {
|
|
2071
|
+
const dir = (0, import_node_path11.join)(base, convention);
|
|
2072
|
+
if (!(0, import_node_fs10.existsSync)(dir)) continue;
|
|
2073
|
+
try {
|
|
2074
|
+
const stat = (0, import_node_fs10.statSync)(dir);
|
|
2075
|
+
if (!stat.isDirectory()) continue;
|
|
2076
|
+
const entries = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2077
|
+
if (entries.length > 0) {
|
|
2078
|
+
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
2079
|
+
result.set(relPath, entries);
|
|
2080
|
+
}
|
|
2081
|
+
} catch {
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return result;
|
|
2086
|
+
}
|
|
2087
|
+
function extractRouteGroups(id) {
|
|
2088
|
+
const groups = [];
|
|
2089
|
+
const re = /\(([^)]+)\)/g;
|
|
2090
|
+
let m;
|
|
2091
|
+
while ((m = re.exec(id)) !== null) {
|
|
2092
|
+
groups.push(m[1]);
|
|
2093
|
+
}
|
|
2094
|
+
return groups;
|
|
2095
|
+
}
|
|
2096
|
+
function isRouteGroup(segment) {
|
|
2097
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
2098
|
+
}
|
|
2099
|
+
function isDynamicSegment(segment) {
|
|
2100
|
+
return segment.startsWith("[") || segment.startsWith(":");
|
|
2101
|
+
}
|
|
2102
|
+
function isDomainDir(segment) {
|
|
2103
|
+
return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
|
|
2104
|
+
}
|
|
2105
|
+
function isTrivialGroup(name, extraTrivial) {
|
|
2106
|
+
if (TRIVIAL_GROUPS.has(name)) return true;
|
|
2107
|
+
if (extraTrivial?.has(name)) return true;
|
|
2108
|
+
const lower = name.toLowerCase();
|
|
2109
|
+
const wrapperPatterns = [
|
|
2110
|
+
/^.*-?wrapper$/,
|
|
2111
|
+
// "page-wrapper", "use-page-wrapper"
|
|
2112
|
+
/^.*-?layout$/,
|
|
2113
|
+
// "admin-layout", "settings-layout"
|
|
2114
|
+
/^use-/,
|
|
2115
|
+
// "use-page-wrapper"
|
|
2116
|
+
/^default$/
|
|
2117
|
+
];
|
|
2118
|
+
return wrapperPatterns.some((p) => p.test(lower));
|
|
2119
|
+
}
|
|
2120
|
+
function normalizeGroupName(name) {
|
|
2121
|
+
return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
|
|
2122
|
+
}
|
|
2123
|
+
function extractModuleFromPath(id, extraTrivial) {
|
|
2124
|
+
const segments = id.split("/");
|
|
2125
|
+
const routeGroups = extractRouteGroups(id);
|
|
2126
|
+
const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
|
|
2127
|
+
if (moduleGroups.length > 0) {
|
|
2128
|
+
return moduleGroups[moduleGroups.length - 1];
|
|
2129
|
+
}
|
|
2130
|
+
const meaningful = [];
|
|
2131
|
+
for (const seg of segments) {
|
|
2132
|
+
if (seg.includes(".")) continue;
|
|
2133
|
+
if (isRouteGroup(seg)) continue;
|
|
2134
|
+
if (isDynamicSegment(seg)) continue;
|
|
2135
|
+
if (isDomainDir(seg)) continue;
|
|
2136
|
+
if (SKIP_SEGMENTS.has(seg)) continue;
|
|
2137
|
+
meaningful.push(seg);
|
|
2138
|
+
}
|
|
2139
|
+
if (meaningful.length > 0) {
|
|
2140
|
+
return meaningful[0];
|
|
2141
|
+
}
|
|
2142
|
+
return "root";
|
|
2143
|
+
}
|
|
2144
|
+
var import_node_fs10, import_node_path11, CONVENTION_DIRS, SKIP_SEGMENTS, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2145
|
+
var init_module_tagger = __esm({
|
|
2146
|
+
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2147
|
+
"use strict";
|
|
2148
|
+
import_node_fs10 = require("node:fs");
|
|
2149
|
+
import_node_path11 = require("node:path");
|
|
2150
|
+
CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
|
|
2151
|
+
SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
2152
|
+
"src",
|
|
2153
|
+
"app",
|
|
2154
|
+
"client",
|
|
2155
|
+
"server",
|
|
2156
|
+
"lib",
|
|
2157
|
+
"config"
|
|
2158
|
+
]);
|
|
2159
|
+
TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
|
|
2160
|
+
// Generic app wrappers
|
|
2161
|
+
"app",
|
|
2162
|
+
"all",
|
|
2163
|
+
"ee",
|
|
2164
|
+
"home",
|
|
2165
|
+
"root",
|
|
2166
|
+
"main",
|
|
2167
|
+
"site",
|
|
2168
|
+
// Auth/access boundary wrappers — protect routes, not feature modules
|
|
2169
|
+
"protected",
|
|
2170
|
+
"authenticated",
|
|
2171
|
+
"authed",
|
|
2172
|
+
"private",
|
|
2173
|
+
"public",
|
|
2174
|
+
"logged-in",
|
|
2175
|
+
"logged-out",
|
|
2176
|
+
"unprotected",
|
|
2177
|
+
"unauthenticated",
|
|
2178
|
+
"auth-required",
|
|
2179
|
+
"no-auth",
|
|
2180
|
+
"guest-only"
|
|
2181
|
+
]);
|
|
2182
|
+
cachedRootDir = null;
|
|
2183
|
+
cachedConventionDirs = /* @__PURE__ */ new Map();
|
|
2184
|
+
moduleTagger = {
|
|
2185
|
+
id: "module",
|
|
2186
|
+
tagKey: "module",
|
|
2187
|
+
trackUntagged: true,
|
|
2188
|
+
layers: null,
|
|
2189
|
+
// applies to all layers
|
|
2190
|
+
tag(nodes, layer, rootDir) {
|
|
2191
|
+
if (cachedRootDir !== rootDir) {
|
|
2192
|
+
cachedConventionDirs = detectConventionDirs(rootDir);
|
|
2193
|
+
cachedRootDir = rootDir;
|
|
2194
|
+
}
|
|
2195
|
+
let configRules = [];
|
|
2196
|
+
let extraTrivial;
|
|
2197
|
+
try {
|
|
2198
|
+
const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
|
|
2199
|
+
const config = loadConfig2(rootDir);
|
|
2200
|
+
configRules = config.taggers?.module?.rules ?? [];
|
|
2201
|
+
const trivialFromConfig = config.taggers?.module?.trivialGroups;
|
|
2202
|
+
if (trivialFromConfig?.length) {
|
|
2203
|
+
extraTrivial = new Set(trivialFromConfig);
|
|
2204
|
+
}
|
|
2205
|
+
} catch {
|
|
2206
|
+
}
|
|
2207
|
+
const result = /* @__PURE__ */ new Map();
|
|
2208
|
+
for (const node of nodes) {
|
|
2209
|
+
const id = node.id;
|
|
2210
|
+
let matched = false;
|
|
2211
|
+
for (const rule of configRules) {
|
|
2212
|
+
if (matchGlob(rule.match, id)) {
|
|
2213
|
+
result.set(id, rule.module);
|
|
2214
|
+
matched = true;
|
|
2215
|
+
break;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (matched) continue;
|
|
2219
|
+
matched = false;
|
|
2220
|
+
for (const [convDir, moduleNames] of cachedConventionDirs) {
|
|
2221
|
+
if (id.startsWith(convDir + "/")) {
|
|
2222
|
+
const rest = id.slice(convDir.length + 1);
|
|
2223
|
+
const firstSeg = rest.split("/")[0];
|
|
2224
|
+
if (moduleNames.includes(firstSeg)) {
|
|
2225
|
+
result.set(id, firstSeg);
|
|
2226
|
+
matched = true;
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
if (matched) continue;
|
|
2232
|
+
const module2 = extractModuleFromPath(id, extraTrivial);
|
|
2233
|
+
result.set(id, module2);
|
|
2234
|
+
}
|
|
2235
|
+
return result;
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
// src/server/graph/taggers/screen-tagger.ts
|
|
2242
|
+
var SCREEN_TYPES, screenTagger;
|
|
2243
|
+
var init_screen_tagger = __esm({
|
|
2244
|
+
"src/server/graph/taggers/screen-tagger.ts"() {
|
|
2245
|
+
"use strict";
|
|
2246
|
+
SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
|
|
2247
|
+
screenTagger = {
|
|
2248
|
+
id: "screen",
|
|
2249
|
+
tagKey: "screen",
|
|
2250
|
+
trackUntagged: true,
|
|
2251
|
+
layers: ["ui"],
|
|
2252
|
+
tag(nodes, layer) {
|
|
2253
|
+
if (layer !== "ui") return /* @__PURE__ */ new Map();
|
|
2254
|
+
const result = /* @__PURE__ */ new Map();
|
|
2255
|
+
for (const node of nodes) {
|
|
2256
|
+
if (SCREEN_TYPES.has(node.type)) {
|
|
2257
|
+
result.set(node.id, "true");
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return result;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
// src/server/graph/core/tagger-registry.ts
|
|
2267
|
+
function registerBuiltins2(registry, disabled, config) {
|
|
2268
|
+
for (const tagger of BUILTIN_TAGGERS) {
|
|
2269
|
+
if (disabled.has(tagger.id)) continue;
|
|
2270
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
2271
|
+
if (override !== void 0) {
|
|
2272
|
+
tagger.trackUntagged = override;
|
|
2273
|
+
}
|
|
2274
|
+
registry.register(tagger);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
2278
|
+
for (const entry of config.taggers?.custom ?? []) {
|
|
2279
|
+
if (disabled.has(entry.id)) continue;
|
|
2280
|
+
try {
|
|
2281
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
2282
|
+
const mod = require(absPath);
|
|
2283
|
+
const tagger = "default" in mod ? mod.default : mod;
|
|
2284
|
+
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
2285
|
+
if (override !== void 0) {
|
|
2286
|
+
tagger.trackUntagged = override;
|
|
2287
|
+
}
|
|
2288
|
+
registry.register(tagger);
|
|
2289
|
+
} catch (err2) {
|
|
2290
|
+
process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
|
|
2291
|
+
`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function createTaggerRegistry(config, rootDir) {
|
|
2296
|
+
const registry = new TaggerRegistry();
|
|
2297
|
+
const disabled = new Set(config.taggers?.disabled ?? []);
|
|
2298
|
+
registerBuiltins2(registry, disabled, config);
|
|
2299
|
+
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
2300
|
+
return registry;
|
|
2301
|
+
}
|
|
2302
|
+
var import_node_path12, TaggerRegistry, BUILTIN_TAGGERS;
|
|
2303
|
+
var init_tagger_registry = __esm({
|
|
2304
|
+
"src/server/graph/core/tagger-registry.ts"() {
|
|
2305
|
+
"use strict";
|
|
2306
|
+
import_node_path12 = require("node:path");
|
|
2307
|
+
init_module_tagger();
|
|
2308
|
+
init_screen_tagger();
|
|
2309
|
+
TaggerRegistry = class {
|
|
2310
|
+
constructor() {
|
|
2311
|
+
this.taggers = [];
|
|
2312
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
2313
|
+
}
|
|
2314
|
+
register(tagger) {
|
|
2315
|
+
if (this.ids.has(tagger.id)) {
|
|
2316
|
+
throw new Error(`Duplicate tagger id: ${tagger.id}`);
|
|
2317
|
+
}
|
|
2318
|
+
this.ids.add(tagger.id);
|
|
2319
|
+
this.taggers.push(tagger);
|
|
2320
|
+
}
|
|
2321
|
+
getAll() {
|
|
2322
|
+
return this.taggers;
|
|
2323
|
+
}
|
|
2324
|
+
getForLayer(layer) {
|
|
2325
|
+
return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
BUILTIN_TAGGERS = [moduleTagger, screenTagger];
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// src/server/graph/core/tag-store.ts
|
|
2333
|
+
function tagsFilePath(rootDir) {
|
|
2334
|
+
return (0, import_node_path13.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2335
|
+
}
|
|
2336
|
+
function readTagStore(rootDir) {
|
|
2337
|
+
const filePath = tagsFilePath(rootDir);
|
|
2338
|
+
if (!(0, import_node_fs11.existsSync)(filePath)) return {};
|
|
2339
|
+
const stat = (0, import_node_fs11.statSync)(filePath);
|
|
2340
|
+
const cached = tagCache.get(filePath);
|
|
2341
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2342
|
+
return cached.store;
|
|
2343
|
+
}
|
|
2344
|
+
try {
|
|
2345
|
+
const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
|
|
2346
|
+
const store = JSON.parse(content);
|
|
2347
|
+
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2348
|
+
return store;
|
|
2349
|
+
} catch {
|
|
2350
|
+
return {};
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
function writeTagStore(rootDir, store) {
|
|
2354
|
+
const filePath = tagsFilePath(rootDir);
|
|
2355
|
+
const dir = (0, import_node_path13.dirname)(filePath);
|
|
2356
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
2357
|
+
const cleaned = {};
|
|
2358
|
+
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2359
|
+
if (Object.keys(tags).length > 0) {
|
|
2360
|
+
cleaned[nodeId] = tags;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
(0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2364
|
+
tagCache.delete(filePath);
|
|
2365
|
+
}
|
|
2366
|
+
function setTag(rootDir, nodeId, key, value) {
|
|
2367
|
+
const store = readTagStore(rootDir);
|
|
2368
|
+
if (!store[nodeId]) store[nodeId] = {};
|
|
2369
|
+
store[nodeId][key] = value;
|
|
2370
|
+
writeTagStore(rootDir, store);
|
|
2371
|
+
}
|
|
2372
|
+
function removeTag(rootDir, nodeId, key) {
|
|
2373
|
+
const store = readTagStore(rootDir);
|
|
2374
|
+
if (!store[nodeId]) return;
|
|
2375
|
+
delete store[nodeId][key];
|
|
2376
|
+
if (Object.keys(store[nodeId]).length === 0) {
|
|
2377
|
+
delete store[nodeId];
|
|
2378
|
+
}
|
|
2379
|
+
writeTagStore(rootDir, store);
|
|
2380
|
+
}
|
|
2381
|
+
var import_node_fs11, import_node_path13, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
2382
|
+
var init_tag_store = __esm({
|
|
2383
|
+
"src/server/graph/core/tag-store.ts"() {
|
|
2384
|
+
"use strict";
|
|
2385
|
+
import_node_fs11 = require("node:fs");
|
|
2386
|
+
import_node_path13 = require("node:path");
|
|
2387
|
+
TAGS_FILENAME = "tags.json";
|
|
2388
|
+
GRAPHS_DIR = ".launchsecure/graphs";
|
|
2389
|
+
tagCache = /* @__PURE__ */ new Map();
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2034
2393
|
// src/server/graph/index.ts
|
|
2035
2394
|
function graphsDir(rootDir) {
|
|
2036
|
-
return (0,
|
|
2395
|
+
return (0, import_node_path14.join)(rootDir, GRAPHS_DIR2);
|
|
2037
2396
|
}
|
|
2038
2397
|
function graphFilePath(rootDir, layer) {
|
|
2039
|
-
return (0,
|
|
2398
|
+
return (0, import_node_path14.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2399
|
+
}
|
|
2400
|
+
function tagsFilePath2(rootDir) {
|
|
2401
|
+
return (0, import_node_path14.join)(graphsDir(rootDir), "tags.json");
|
|
2402
|
+
}
|
|
2403
|
+
function getMtimeMs(filePath) {
|
|
2404
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return 0;
|
|
2405
|
+
return (0, import_node_fs12.statSync)(filePath).mtimeMs;
|
|
2040
2406
|
}
|
|
2041
2407
|
function invalidateCache(filePath) {
|
|
2042
2408
|
graphCache.delete(filePath);
|
|
2043
2409
|
}
|
|
2044
|
-
function
|
|
2410
|
+
function invalidateTaggedCache(rootDir, layer) {
|
|
2411
|
+
taggedCache.delete(`${rootDir}:${layer}`);
|
|
2412
|
+
}
|
|
2413
|
+
function applyTags(graph, layer, rootDir) {
|
|
2414
|
+
const config = loadConfig(rootDir);
|
|
2415
|
+
const registry = createTaggerRegistry(config, rootDir);
|
|
2416
|
+
const manualTags = readTagStore(rootDir);
|
|
2417
|
+
const taggedNodes = graph.nodes.map((n) => ({ ...n }));
|
|
2418
|
+
const taggers = registry.getForLayer(layer);
|
|
2419
|
+
for (const tagger of taggers) {
|
|
2420
|
+
const assignments = tagger.tag(taggedNodes, layer, rootDir);
|
|
2421
|
+
for (const node of taggedNodes) {
|
|
2422
|
+
if (!node.tags) node.tags = {};
|
|
2423
|
+
const tags = node.tags;
|
|
2424
|
+
const value = assignments.get(node.id);
|
|
2425
|
+
if (value !== void 0) {
|
|
2426
|
+
tags[tagger.tagKey] = value;
|
|
2427
|
+
} else if (tagger.trackUntagged) {
|
|
2428
|
+
tags[tagger.tagKey] = "untagged";
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
for (const node of taggedNodes) {
|
|
2433
|
+
const manual = manualTags[node.id];
|
|
2434
|
+
if (manual) {
|
|
2435
|
+
if (!node.tags) node.tags = {};
|
|
2436
|
+
const tags = node.tags;
|
|
2437
|
+
Object.assign(tags, manual);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
return { ...graph, nodes: taggedNodes };
|
|
2441
|
+
}
|
|
2442
|
+
function readGraphRaw(rootDir, layer) {
|
|
2045
2443
|
const filePath = graphFilePath(rootDir, layer);
|
|
2046
|
-
if (!(0,
|
|
2047
|
-
const stat = (0,
|
|
2444
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
2445
|
+
const stat = (0, import_node_fs12.statSync)(filePath);
|
|
2048
2446
|
const cached = graphCache.get(filePath);
|
|
2049
2447
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2050
2448
|
return cached.graph;
|
|
2051
2449
|
}
|
|
2052
|
-
const content = (0,
|
|
2450
|
+
const content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
|
|
2053
2451
|
const graph = JSON.parse(content);
|
|
2054
2452
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2055
2453
|
return graph;
|
|
2056
2454
|
}
|
|
2455
|
+
function readGraph(rootDir, layer) {
|
|
2456
|
+
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2457
|
+
if (!(0, import_node_fs12.existsSync)(rawFilePath)) return null;
|
|
2458
|
+
const rawMtime = getMtimeMs(rawFilePath);
|
|
2459
|
+
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2460
|
+
const cacheKey = `${rootDir}:${layer}`;
|
|
2461
|
+
const cached = taggedCache.get(cacheKey);
|
|
2462
|
+
if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
|
|
2463
|
+
return cached.graph;
|
|
2464
|
+
}
|
|
2465
|
+
const raw = readGraphRaw(rootDir, layer);
|
|
2466
|
+
if (!raw) return null;
|
|
2467
|
+
const tagged = applyTags(raw, layer, rootDir);
|
|
2468
|
+
taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
|
|
2469
|
+
return tagged;
|
|
2470
|
+
}
|
|
2057
2471
|
function readAllGraphs(rootDir) {
|
|
2058
2472
|
const result = {};
|
|
2059
2473
|
for (const layer of LAYERS) {
|
|
@@ -2064,25 +2478,31 @@ function readAllGraphs(rootDir) {
|
|
|
2064
2478
|
}
|
|
2065
2479
|
function generateGraph(rootDir, layer) {
|
|
2066
2480
|
const dir = graphsDir(rootDir);
|
|
2067
|
-
(0,
|
|
2481
|
+
(0, import_node_fs12.mkdirSync)(dir, { recursive: true });
|
|
2068
2482
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2069
2483
|
for (const result of results) {
|
|
2070
2484
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
2071
|
-
(0,
|
|
2485
|
+
(0, import_node_fs12.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2072
2486
|
invalidateCache(filePath);
|
|
2487
|
+
invalidateTaggedCache(rootDir, result.layer);
|
|
2073
2488
|
}
|
|
2074
2489
|
return results;
|
|
2075
2490
|
}
|
|
2076
|
-
var
|
|
2491
|
+
var import_node_fs12, import_node_path14, GRAPHS_DIR2, LAYERS, graphCache, taggedCache;
|
|
2077
2492
|
var init_graph = __esm({
|
|
2078
2493
|
"src/server/graph/index.ts"() {
|
|
2079
2494
|
"use strict";
|
|
2080
|
-
|
|
2081
|
-
|
|
2495
|
+
import_node_fs12 = require("node:fs");
|
|
2496
|
+
import_node_path14 = require("node:path");
|
|
2082
2497
|
init_graph_builder();
|
|
2083
|
-
|
|
2498
|
+
init_config();
|
|
2499
|
+
init_tagger_registry();
|
|
2500
|
+
init_tag_store();
|
|
2501
|
+
init_tag_store();
|
|
2502
|
+
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2084
2503
|
LAYERS = ["ui", "api", "db"];
|
|
2085
2504
|
graphCache = /* @__PURE__ */ new Map();
|
|
2505
|
+
taggedCache = /* @__PURE__ */ new Map();
|
|
2086
2506
|
}
|
|
2087
2507
|
});
|
|
2088
2508
|
|
|
@@ -2092,19 +2512,22 @@ __export(chart_serve_exports, {
|
|
|
2092
2512
|
runServeCli: () => runServeCli,
|
|
2093
2513
|
startChartServer: () => startChartServer
|
|
2094
2514
|
});
|
|
2095
|
-
function
|
|
2515
|
+
function randomPort() {
|
|
2516
|
+
return 49152 + Math.floor(Math.random() * (65535 - 49152));
|
|
2517
|
+
}
|
|
2518
|
+
function findProjectRoot(startDir) {
|
|
2096
2519
|
let dir = startDir;
|
|
2097
2520
|
for (let i = 0; i < 8; i++) {
|
|
2098
|
-
const graphsDir2 =
|
|
2099
|
-
if (
|
|
2100
|
-
const parent =
|
|
2521
|
+
const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
|
|
2522
|
+
if (import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "ui.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "api.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "db.json"))) return dir;
|
|
2523
|
+
const parent = import_node_path15.default.dirname(dir);
|
|
2101
2524
|
if (parent === dir) break;
|
|
2102
2525
|
dir = parent;
|
|
2103
2526
|
}
|
|
2104
2527
|
dir = startDir;
|
|
2105
2528
|
for (let i = 0; i < 8; i++) {
|
|
2106
|
-
if (
|
|
2107
|
-
const parent =
|
|
2529
|
+
if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
|
|
2530
|
+
const parent = import_node_path15.default.dirname(dir);
|
|
2108
2531
|
if (parent === dir) break;
|
|
2109
2532
|
dir = parent;
|
|
2110
2533
|
}
|
|
@@ -2156,16 +2579,16 @@ function buildMergedGraph(projectRoot) {
|
|
|
2156
2579
|
};
|
|
2157
2580
|
}
|
|
2158
2581
|
function serveStatic(res, filePath) {
|
|
2159
|
-
if (!
|
|
2160
|
-
const ext =
|
|
2582
|
+
if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
|
|
2583
|
+
const ext = import_node_path15.default.extname(filePath).toLowerCase();
|
|
2161
2584
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2162
2585
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2163
|
-
|
|
2586
|
+
import_node_fs13.default.createReadStream(filePath).pipe(res);
|
|
2164
2587
|
return true;
|
|
2165
2588
|
}
|
|
2166
2589
|
function serveIndex(res, clientDir) {
|
|
2167
|
-
const indexPath =
|
|
2168
|
-
if (!
|
|
2590
|
+
const indexPath = import_node_path15.default.join(clientDir, "index.html");
|
|
2591
|
+
if (!import_node_fs13.default.existsSync(indexPath)) {
|
|
2169
2592
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2170
2593
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2171
2594
|
return;
|
|
@@ -2173,14 +2596,14 @@ function serveIndex(res, clientDir) {
|
|
|
2173
2596
|
serveStatic(res, indexPath);
|
|
2174
2597
|
}
|
|
2175
2598
|
function tryListen(server, port) {
|
|
2176
|
-
return new Promise((
|
|
2599
|
+
return new Promise((resolve3, reject) => {
|
|
2177
2600
|
const onError = (err2) => {
|
|
2178
2601
|
server.off("listening", onListening);
|
|
2179
2602
|
reject(err2);
|
|
2180
2603
|
};
|
|
2181
2604
|
const onListening = () => {
|
|
2182
2605
|
server.off("error", onError);
|
|
2183
|
-
|
|
2606
|
+
resolve3(port);
|
|
2184
2607
|
};
|
|
2185
2608
|
server.once("error", onError);
|
|
2186
2609
|
server.once("listening", onListening);
|
|
@@ -2206,8 +2629,8 @@ async function bindWithFallback(server, startPort) {
|
|
|
2206
2629
|
}
|
|
2207
2630
|
async function startChartServer(opts = {}) {
|
|
2208
2631
|
const cwd = opts.cwd ?? process.cwd();
|
|
2209
|
-
const projectRoot =
|
|
2210
|
-
const existing = getLiveLock();
|
|
2632
|
+
const projectRoot = findProjectRoot(cwd);
|
|
2633
|
+
const existing = getLiveLock(projectRoot);
|
|
2211
2634
|
if (existing) {
|
|
2212
2635
|
if (!opts.quiet) {
|
|
2213
2636
|
process.stderr.write(
|
|
@@ -2217,7 +2640,7 @@ async function startChartServer(opts = {}) {
|
|
|
2217
2640
|
}
|
|
2218
2641
|
return { port: existing.port, url: existing.url };
|
|
2219
2642
|
}
|
|
2220
|
-
const clientDir = opts.clientDir ??
|
|
2643
|
+
const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
|
|
2221
2644
|
const server = import_node_http.default.createServer((req, res) => {
|
|
2222
2645
|
try {
|
|
2223
2646
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -2255,6 +2678,26 @@ async function startChartServer(opts = {}) {
|
|
|
2255
2678
|
}
|
|
2256
2679
|
return;
|
|
2257
2680
|
}
|
|
2681
|
+
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2682
|
+
const relPath = url2.searchParams.get("path");
|
|
2683
|
+
if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
|
|
2684
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2685
|
+
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
const filePath = import_node_path15.default.join(projectRoot, relPath);
|
|
2689
|
+
if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
|
|
2690
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2691
|
+
res.end(JSON.stringify({ error: "File not found" }));
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const ext = import_node_path15.default.extname(filePath).toLowerCase();
|
|
2695
|
+
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2696
|
+
const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
|
|
2697
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2698
|
+
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2258
2701
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2259
2702
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2260
2703
|
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
@@ -2284,8 +2727,94 @@ async function startChartServer(opts = {}) {
|
|
|
2284
2727
|
req.on("end", () => {
|
|
2285
2728
|
try {
|
|
2286
2729
|
const newConfig = JSON.parse(body);
|
|
2287
|
-
const configPath =
|
|
2288
|
-
|
|
2730
|
+
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2731
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2732
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2733
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2734
|
+
} catch (err2) {
|
|
2735
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2736
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
2742
|
+
const config = loadConfig(projectRoot);
|
|
2743
|
+
const builtinTaggers = [
|
|
2744
|
+
{ id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
|
|
2745
|
+
{ id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
|
|
2746
|
+
];
|
|
2747
|
+
const disabled = config.taggers?.disabled ?? [];
|
|
2748
|
+
const customTaggers = config.taggers?.custom ?? [];
|
|
2749
|
+
const moduleRules = config.taggers?.module?.rules ?? [];
|
|
2750
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2751
|
+
res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
|
|
2755
|
+
let body = "";
|
|
2756
|
+
req.on("data", (chunk) => {
|
|
2757
|
+
body += chunk.toString();
|
|
2758
|
+
});
|
|
2759
|
+
req.on("end", () => {
|
|
2760
|
+
try {
|
|
2761
|
+
const taggerConfig = JSON.parse(body);
|
|
2762
|
+
const config = loadConfig(projectRoot);
|
|
2763
|
+
config.taggers = taggerConfig;
|
|
2764
|
+
const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
|
|
2765
|
+
import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2766
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2767
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2768
|
+
} catch (err2) {
|
|
2769
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2770
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
2776
|
+
const store = readTagStore(projectRoot);
|
|
2777
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2778
|
+
res.end(JSON.stringify(store));
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
if (req.method === "POST" && url2.pathname === "/api/tags") {
|
|
2782
|
+
let body = "";
|
|
2783
|
+
req.on("data", (chunk) => {
|
|
2784
|
+
body += chunk.toString();
|
|
2785
|
+
});
|
|
2786
|
+
req.on("end", () => {
|
|
2787
|
+
try {
|
|
2788
|
+
const { nodeId, key, value } = JSON.parse(body);
|
|
2789
|
+
if (!nodeId || !key || !value) {
|
|
2790
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2791
|
+
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
setTag(projectRoot, nodeId, key, value);
|
|
2795
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2796
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2797
|
+
} catch (err2) {
|
|
2798
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2799
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
if (req.method === "DELETE" && url2.pathname === "/api/tags") {
|
|
2805
|
+
let body = "";
|
|
2806
|
+
req.on("data", (chunk) => {
|
|
2807
|
+
body += chunk.toString();
|
|
2808
|
+
});
|
|
2809
|
+
req.on("end", () => {
|
|
2810
|
+
try {
|
|
2811
|
+
const { nodeId, key } = JSON.parse(body);
|
|
2812
|
+
if (!nodeId || !key) {
|
|
2813
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2814
|
+
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
removeTag(projectRoot, nodeId, key);
|
|
2289
2818
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2290
2819
|
res.end(JSON.stringify({ ok: true }));
|
|
2291
2820
|
} catch (err2) {
|
|
@@ -2296,7 +2825,7 @@ async function startChartServer(opts = {}) {
|
|
|
2296
2825
|
return;
|
|
2297
2826
|
}
|
|
2298
2827
|
if (url2.pathname !== "/") {
|
|
2299
|
-
const staticPath =
|
|
2828
|
+
const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
|
|
2300
2829
|
if (serveStatic(res, staticPath)) return;
|
|
2301
2830
|
}
|
|
2302
2831
|
serveIndex(res, clientDir);
|
|
@@ -2305,7 +2834,8 @@ async function startChartServer(opts = {}) {
|
|
|
2305
2834
|
res.end(JSON.stringify({ error: String(err2) }));
|
|
2306
2835
|
}
|
|
2307
2836
|
});
|
|
2308
|
-
const
|
|
2837
|
+
const startPort = opts.port ?? randomPort();
|
|
2838
|
+
const port = await bindWithFallback(server, startPort);
|
|
2309
2839
|
const url = `http://localhost:${port}`;
|
|
2310
2840
|
writeLock({
|
|
2311
2841
|
pid: process.pid,
|
|
@@ -2313,9 +2843,9 @@ async function startChartServer(opts = {}) {
|
|
|
2313
2843
|
cwd,
|
|
2314
2844
|
url,
|
|
2315
2845
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2316
|
-
});
|
|
2846
|
+
}, projectRoot);
|
|
2317
2847
|
const cleanup = () => {
|
|
2318
|
-
clearLock();
|
|
2848
|
+
clearLock(projectRoot);
|
|
2319
2849
|
server.close();
|
|
2320
2850
|
};
|
|
2321
2851
|
process.once("SIGINT", () => {
|
|
@@ -2350,21 +2880,20 @@ function runServeCli(argv) {
|
|
|
2350
2880
|
process.exit(1);
|
|
2351
2881
|
});
|
|
2352
2882
|
}
|
|
2353
|
-
var import_node_http,
|
|
2883
|
+
var import_node_http, import_node_fs13, import_node_path15, MAX_PORT_SCAN, MIME_TYPES;
|
|
2354
2884
|
var init_chart_serve = __esm({
|
|
2355
2885
|
"src/server/chart-serve.ts"() {
|
|
2356
2886
|
"use strict";
|
|
2357
2887
|
import_node_http = __toESM(require("node:http"));
|
|
2358
|
-
|
|
2359
|
-
|
|
2888
|
+
import_node_fs13 = __toESM(require("node:fs"));
|
|
2889
|
+
import_node_path15 = __toESM(require("node:path"));
|
|
2360
2890
|
init_graph();
|
|
2361
2891
|
init_lockfile();
|
|
2362
2892
|
init_config();
|
|
2363
2893
|
init_react_nextjs();
|
|
2364
2894
|
init_nextjs_routes();
|
|
2365
2895
|
init_prisma_schema();
|
|
2366
|
-
|
|
2367
|
-
MAX_PORT_SCAN = 20;
|
|
2896
|
+
MAX_PORT_SCAN = 3;
|
|
2368
2897
|
MIME_TYPES = {
|
|
2369
2898
|
".html": "text/html; charset=utf-8",
|
|
2370
2899
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -2395,7 +2924,7 @@ function matchesSearch(node, query) {
|
|
|
2395
2924
|
function toMinimal(nodes) {
|
|
2396
2925
|
return nodes.map((n) => {
|
|
2397
2926
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
2398
|
-
if (n.
|
|
2927
|
+
if (n.tags != null) out.tags = n.tags;
|
|
2399
2928
|
if (n.route != null) out.route = n.route;
|
|
2400
2929
|
if (n.methods != null) out.methods = n.methods;
|
|
2401
2930
|
return out;
|
|
@@ -2403,11 +2932,13 @@ function toMinimal(nodes) {
|
|
|
2403
2932
|
}
|
|
2404
2933
|
function toCompactNode(n) {
|
|
2405
2934
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
2406
|
-
|
|
2935
|
+
const tags = n.tags;
|
|
2936
|
+
if (tags?.module) out.m = tags.module;
|
|
2407
2937
|
if (n.route != null) out.r = n.route;
|
|
2408
2938
|
if (n.methods != null) out.mt = n.methods;
|
|
2409
2939
|
if (n.exports != null) out.x = n.exports;
|
|
2410
2940
|
if (n.columns != null) out.c = n.columns;
|
|
2941
|
+
if (tags != null) out.tg = tags;
|
|
2411
2942
|
for (const k of Object.keys(n)) {
|
|
2412
2943
|
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
2413
2944
|
}
|
|
@@ -2483,7 +3014,8 @@ function layerSummary(graph) {
|
|
|
2483
3014
|
const moduleCounts = {};
|
|
2484
3015
|
for (const n of graph.nodes) {
|
|
2485
3016
|
typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
|
|
2486
|
-
const
|
|
3017
|
+
const tags = n.tags;
|
|
3018
|
+
const mod = tags?.module;
|
|
2487
3019
|
if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
|
|
2488
3020
|
}
|
|
2489
3021
|
const edgeTypeCounts = {};
|
|
@@ -2540,12 +3072,14 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
2540
3072
|
const search = args.search;
|
|
2541
3073
|
const type = args.type;
|
|
2542
3074
|
const module_ = args.module;
|
|
3075
|
+
const tagKey = args.tag_key;
|
|
3076
|
+
const tagValue = args.tag_value;
|
|
2543
3077
|
const nodeId = args.node_id;
|
|
2544
3078
|
const hops = args.hops ?? 1;
|
|
2545
3079
|
const layerIsDb = args.layer === "db";
|
|
2546
3080
|
const minimal = args.minimal ?? layerIsDb;
|
|
2547
3081
|
const includeEdges = args.include_edges;
|
|
2548
|
-
const hasFilter = !!(search || type || module_ || nodeId);
|
|
3082
|
+
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
2549
3083
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
2550
3084
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
2551
3085
|
}
|
|
@@ -2601,7 +3135,9 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
2601
3135
|
const matched = graph.nodes.filter((n) => {
|
|
2602
3136
|
if (search && !matchesSearch(n, search)) return false;
|
|
2603
3137
|
if (type && n.type !== type) return false;
|
|
2604
|
-
|
|
3138
|
+
const nodeTags = n.tags;
|
|
3139
|
+
if (module_ && nodeTags?.module !== module_) return false;
|
|
3140
|
+
if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
|
|
2605
3141
|
return true;
|
|
2606
3142
|
});
|
|
2607
3143
|
const matchedIds = new Set(matched.map((n) => n.id));
|
|
@@ -2688,9 +3224,9 @@ function handleReadGraph(args) {
|
|
|
2688
3224
|
return okJson(result);
|
|
2689
3225
|
}
|
|
2690
3226
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
2691
|
-
if (layer === "ui") return (0,
|
|
2692
|
-
if (layer === "api") return (0,
|
|
2693
|
-
if (layer === "db") return (0,
|
|
3227
|
+
if (layer === "ui") return (0, import_node_path16.join)(rootDir, "src", nodeId);
|
|
3228
|
+
if (layer === "api") return (0, import_node_path16.join)(rootDir, nodeId);
|
|
3229
|
+
if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
|
|
2694
3230
|
return null;
|
|
2695
3231
|
}
|
|
2696
3232
|
function handleGrepNodes(args) {
|
|
@@ -2750,11 +3286,11 @@ function handleGrepNodes(args) {
|
|
|
2750
3286
|
let filesSearched = 0;
|
|
2751
3287
|
let truncated = false;
|
|
2752
3288
|
for (const [filePath, nodeId] of filePaths) {
|
|
2753
|
-
if (!(0,
|
|
3289
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) continue;
|
|
2754
3290
|
filesSearched++;
|
|
2755
3291
|
let content;
|
|
2756
3292
|
try {
|
|
2757
|
-
content = (0,
|
|
3293
|
+
content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
2758
3294
|
} catch {
|
|
2759
3295
|
continue;
|
|
2760
3296
|
}
|
|
@@ -2792,7 +3328,8 @@ function handleGrepNodes(args) {
|
|
|
2792
3328
|
});
|
|
2793
3329
|
}
|
|
2794
3330
|
function handleChartServerStatus() {
|
|
2795
|
-
const
|
|
3331
|
+
const rootDir = process.cwd();
|
|
3332
|
+
const lock = getLiveLock(rootDir);
|
|
2796
3333
|
if (!lock) {
|
|
2797
3334
|
return okJson({ running: false });
|
|
2798
3335
|
}
|
|
@@ -2806,7 +3343,8 @@ function handleChartServerStatus() {
|
|
|
2806
3343
|
});
|
|
2807
3344
|
}
|
|
2808
3345
|
function handleStartChartServer(args) {
|
|
2809
|
-
const
|
|
3346
|
+
const rootDir = process.cwd();
|
|
3347
|
+
const lock = getLiveLock(rootDir);
|
|
2810
3348
|
if (lock) {
|
|
2811
3349
|
return okJson({
|
|
2812
3350
|
started: false,
|
|
@@ -2817,11 +3355,11 @@ function handleStartChartServer(args) {
|
|
|
2817
3355
|
});
|
|
2818
3356
|
}
|
|
2819
3357
|
const entryPath = process.argv[1];
|
|
2820
|
-
const logDir = (0,
|
|
2821
|
-
(0,
|
|
2822
|
-
const logPath = (0,
|
|
2823
|
-
const out = (0,
|
|
2824
|
-
const err2 = (0,
|
|
3358
|
+
const logDir = (0, import_node_path16.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
3359
|
+
(0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
|
|
3360
|
+
const logPath = (0, import_node_path16.join)(logDir, "launch-chart.log");
|
|
3361
|
+
const out = (0, import_node_fs14.openSync)(logPath, "a");
|
|
3362
|
+
const err2 = (0, import_node_fs14.openSync)(logPath, "a");
|
|
2825
3363
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
2826
3364
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
2827
3365
|
detached: true,
|
|
@@ -2836,7 +3374,8 @@ function handleStartChartServer(args) {
|
|
|
2836
3374
|
});
|
|
2837
3375
|
}
|
|
2838
3376
|
function handleStopChartServer() {
|
|
2839
|
-
const
|
|
3377
|
+
const rootDir = process.cwd();
|
|
3378
|
+
const lock = getLiveLock(rootDir);
|
|
2840
3379
|
if (!lock) {
|
|
2841
3380
|
return okJson({ stopped: false, reason: "not_running" });
|
|
2842
3381
|
}
|
|
@@ -2846,14 +3385,45 @@ function handleStopChartServer() {
|
|
|
2846
3385
|
} catch (e) {
|
|
2847
3386
|
const code = e.code;
|
|
2848
3387
|
if (code === "ESRCH") {
|
|
2849
|
-
clearLock();
|
|
3388
|
+
clearLock(rootDir);
|
|
2850
3389
|
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
2851
3390
|
}
|
|
2852
3391
|
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
2853
3392
|
}
|
|
2854
3393
|
}
|
|
3394
|
+
function handleAddTag(args) {
|
|
3395
|
+
const rootDir = process.cwd();
|
|
3396
|
+
const nodeId = args.node_id;
|
|
3397
|
+
const key = args.key;
|
|
3398
|
+
const value = args.value;
|
|
3399
|
+
if (!nodeId) return err("node_id is required");
|
|
3400
|
+
if (!key) return err("key is required");
|
|
3401
|
+
if (!value) return err("value is required");
|
|
3402
|
+
const graphs = readAllGraphs(rootDir);
|
|
3403
|
+
let found = false;
|
|
3404
|
+
for (const graph of Object.values(graphs)) {
|
|
3405
|
+
if (graph && graph.nodes.some((n) => n.id === nodeId)) {
|
|
3406
|
+
found = true;
|
|
3407
|
+
break;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
if (!found) {
|
|
3411
|
+
return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
|
|
3412
|
+
}
|
|
3413
|
+
setTag(rootDir, nodeId, key, value);
|
|
3414
|
+
return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
|
|
3415
|
+
}
|
|
3416
|
+
function handleRemoveTag(args) {
|
|
3417
|
+
const rootDir = process.cwd();
|
|
3418
|
+
const nodeId = args.node_id;
|
|
3419
|
+
const key = args.key;
|
|
3420
|
+
if (!nodeId) return err("node_id is required");
|
|
3421
|
+
if (!key) return err("key is required");
|
|
3422
|
+
removeTag(rootDir, nodeId, key);
|
|
3423
|
+
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
3424
|
+
}
|
|
2855
3425
|
function handleDetectProjectStack() {
|
|
2856
|
-
const rootDir =
|
|
3426
|
+
const rootDir = process.cwd();
|
|
2857
3427
|
const parsers = [
|
|
2858
3428
|
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
2859
3429
|
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
@@ -2871,20 +3441,20 @@ function handleDetectProjectStack() {
|
|
|
2871
3441
|
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
2872
3442
|
}
|
|
2873
3443
|
}
|
|
2874
|
-
const srcDir = (0,
|
|
2875
|
-
if ((0,
|
|
3444
|
+
const srcDir = (0, import_node_path16.join)(rootDir, "src");
|
|
3445
|
+
if ((0, import_node_fs14.existsSync)(srcDir)) {
|
|
2876
3446
|
const scanDir = (dir) => {
|
|
2877
|
-
if (!(0,
|
|
2878
|
-
for (const entry of (0,
|
|
3447
|
+
if (!(0, import_node_fs14.existsSync)(dir)) return;
|
|
3448
|
+
for (const entry of (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true })) {
|
|
2879
3449
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2880
|
-
const full = (0,
|
|
3450
|
+
const full = (0, import_node_path16.join)(dir, entry.name);
|
|
2881
3451
|
if (entry.isDirectory()) {
|
|
2882
3452
|
scanDir(full);
|
|
2883
3453
|
continue;
|
|
2884
3454
|
}
|
|
2885
|
-
if (![".ts", ".tsx"].includes((0,
|
|
3455
|
+
if (![".ts", ".tsx"].includes((0, import_node_path16.extname)(entry.name))) continue;
|
|
2886
3456
|
try {
|
|
2887
|
-
const content = (0,
|
|
3457
|
+
const content = (0, import_node_fs14.readFileSync)(full, "utf-8");
|
|
2888
3458
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
2889
3459
|
if (matches) stats.annotations += matches.length;
|
|
2890
3460
|
} catch {
|
|
@@ -2971,6 +3541,14 @@ function handleMessage(msg) {
|
|
|
2971
3541
|
respond(id ?? null, handleDetectProjectStack());
|
|
2972
3542
|
return;
|
|
2973
3543
|
}
|
|
3544
|
+
if (toolName === "add_tag") {
|
|
3545
|
+
respond(id ?? null, handleAddTag(args));
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
if (toolName === "remove_tag") {
|
|
3549
|
+
respond(id ?? null, handleRemoveTag(args));
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
2974
3552
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
2975
3553
|
return;
|
|
2976
3554
|
}
|
|
@@ -3006,12 +3584,12 @@ function startGraphMcpServer() {
|
|
|
3006
3584
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
3007
3585
|
`);
|
|
3008
3586
|
}
|
|
3009
|
-
var
|
|
3587
|
+
var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3010
3588
|
var init_graph_mcp = __esm({
|
|
3011
3589
|
"src/server/graph-mcp.ts"() {
|
|
3012
3590
|
"use strict";
|
|
3013
|
-
|
|
3014
|
-
|
|
3591
|
+
import_node_fs14 = require("node:fs");
|
|
3592
|
+
import_node_path16 = require("node:path");
|
|
3015
3593
|
import_node_child_process2 = require("node:child_process");
|
|
3016
3594
|
import_node_os2 = require("node:os");
|
|
3017
3595
|
init_graph();
|
|
@@ -3041,7 +3619,7 @@ var init_graph_mcp = __esm({
|
|
|
3041
3619
|
},
|
|
3042
3620
|
{
|
|
3043
3621
|
name: "read_graph",
|
|
3044
|
-
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 (
|
|
3622
|
+
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.',
|
|
3045
3623
|
inputSchema: {
|
|
3046
3624
|
type: "object",
|
|
3047
3625
|
properties: {
|
|
@@ -3060,7 +3638,15 @@ var init_graph_mcp = __esm({
|
|
|
3060
3638
|
},
|
|
3061
3639
|
module: {
|
|
3062
3640
|
type: "string",
|
|
3063
|
-
description: '
|
|
3641
|
+
description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
|
|
3642
|
+
},
|
|
3643
|
+
tag_key: {
|
|
3644
|
+
type: "string",
|
|
3645
|
+
description: "Filter by arbitrary tag key. Must be used with tag_value."
|
|
3646
|
+
},
|
|
3647
|
+
tag_value: {
|
|
3648
|
+
type: "string",
|
|
3649
|
+
description: "Filter by tag value for the given tag_key."
|
|
3064
3650
|
},
|
|
3065
3651
|
node_id: {
|
|
3066
3652
|
type: "string",
|
|
@@ -3184,6 +3770,46 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
3184
3770
|
type: "object",
|
|
3185
3771
|
properties: {}
|
|
3186
3772
|
}
|
|
3773
|
+
},
|
|
3774
|
+
{
|
|
3775
|
+
name: "add_tag",
|
|
3776
|
+
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.',
|
|
3777
|
+
inputSchema: {
|
|
3778
|
+
type: "object",
|
|
3779
|
+
properties: {
|
|
3780
|
+
node_id: {
|
|
3781
|
+
type: "string",
|
|
3782
|
+
description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
|
|
3783
|
+
},
|
|
3784
|
+
key: {
|
|
3785
|
+
type: "string",
|
|
3786
|
+
description: 'Tag key (e.g. "module", "owner", "refactor_later").'
|
|
3787
|
+
},
|
|
3788
|
+
value: {
|
|
3789
|
+
type: "string",
|
|
3790
|
+
description: 'Tag value (e.g. "auth", "alice", "true").'
|
|
3791
|
+
}
|
|
3792
|
+
},
|
|
3793
|
+
required: ["node_id", "key", "value"]
|
|
3794
|
+
}
|
|
3795
|
+
},
|
|
3796
|
+
{
|
|
3797
|
+
name: "remove_tag",
|
|
3798
|
+
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).",
|
|
3799
|
+
inputSchema: {
|
|
3800
|
+
type: "object",
|
|
3801
|
+
properties: {
|
|
3802
|
+
node_id: {
|
|
3803
|
+
type: "string",
|
|
3804
|
+
description: "The node id to remove the tag from."
|
|
3805
|
+
},
|
|
3806
|
+
key: {
|
|
3807
|
+
type: "string",
|
|
3808
|
+
description: "Tag key to remove."
|
|
3809
|
+
}
|
|
3810
|
+
},
|
|
3811
|
+
required: ["node_id", "key"]
|
|
3812
|
+
}
|
|
3187
3813
|
}
|
|
3188
3814
|
];
|
|
3189
3815
|
COMPACT_SCHEMA = {
|
|
@@ -3191,11 +3817,12 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
3191
3817
|
i: "id",
|
|
3192
3818
|
t: "type",
|
|
3193
3819
|
n: "name",
|
|
3194
|
-
m: "module",
|
|
3820
|
+
m: "module (from tags)",
|
|
3195
3821
|
r: "route",
|
|
3196
3822
|
mt: "methods",
|
|
3197
3823
|
x: "exports",
|
|
3198
|
-
c: "columns"
|
|
3824
|
+
c: "columns",
|
|
3825
|
+
tg: "tags"
|
|
3199
3826
|
},
|
|
3200
3827
|
edges: {
|
|
3201
3828
|
s: "source_node_index",
|
|
@@ -3213,7 +3840,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
3213
3840
|
"route",
|
|
3214
3841
|
"methods",
|
|
3215
3842
|
"exports",
|
|
3216
|
-
"columns"
|
|
3843
|
+
"columns",
|
|
3844
|
+
"tags"
|
|
3217
3845
|
]);
|
|
3218
3846
|
EST_CHARS_PER_NODE_FULL = {
|
|
3219
3847
|
ui: 300,
|
|
@@ -3237,10 +3865,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
3237
3865
|
|
|
3238
3866
|
// src/server/graph-mcp-entry.ts
|
|
3239
3867
|
var import_node_child_process3 = require("node:child_process");
|
|
3240
|
-
var
|
|
3241
|
-
var
|
|
3868
|
+
var import_node_fs15 = require("node:fs");
|
|
3869
|
+
var import_node_path17 = __toESM(require("node:path"));
|
|
3242
3870
|
var import_node_os3 = require("node:os");
|
|
3243
|
-
var
|
|
3871
|
+
var import_node_fs16 = require("node:fs");
|
|
3244
3872
|
init_lockfile();
|
|
3245
3873
|
function logStderr(msg) {
|
|
3246
3874
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -3248,17 +3876,19 @@ function logStderr(msg) {
|
|
|
3248
3876
|
}
|
|
3249
3877
|
function maybeAutoServe() {
|
|
3250
3878
|
if (process.env.LAUNCH_CHART_AUTOSERVE !== "1") return;
|
|
3251
|
-
const
|
|
3879
|
+
const rootDir = process.cwd();
|
|
3880
|
+
setProjectRoot(rootDir);
|
|
3881
|
+
const existing = getLiveLock(rootDir);
|
|
3252
3882
|
if (existing) {
|
|
3253
3883
|
logStderr(`autoserve: reusing existing server at ${existing.url}`);
|
|
3254
3884
|
return;
|
|
3255
3885
|
}
|
|
3256
3886
|
try {
|
|
3257
|
-
const logDir =
|
|
3258
|
-
(0,
|
|
3259
|
-
const logPath =
|
|
3260
|
-
const out = (0,
|
|
3261
|
-
const err2 = (0,
|
|
3887
|
+
const logDir = import_node_path17.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
3888
|
+
(0, import_node_fs16.mkdirSync)(logDir, { recursive: true });
|
|
3889
|
+
const logPath = import_node_path17.default.join(logDir, "launch-chart.log");
|
|
3890
|
+
const out = (0, import_node_fs15.openSync)(logPath, "a");
|
|
3891
|
+
const err2 = (0, import_node_fs15.openSync)(logPath, "a");
|
|
3262
3892
|
const entryPath = process.argv[1];
|
|
3263
3893
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
3264
3894
|
detached: true,
|
|
@@ -3272,6 +3902,7 @@ function maybeAutoServe() {
|
|
|
3272
3902
|
}
|
|
3273
3903
|
}
|
|
3274
3904
|
async function main() {
|
|
3905
|
+
setProjectRoot(process.cwd());
|
|
3275
3906
|
const argv = process.argv.slice(2);
|
|
3276
3907
|
const subcommand = argv[0];
|
|
3277
3908
|
if (subcommand === "serve") {
|