@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.
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/server/graph/core/config.ts
34
+ var config_exports = {};
35
+ __export(config_exports, {
36
+ loadConfig: () => loadConfig
37
+ });
38
+ function loadConfig(rootDir) {
39
+ const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
40
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
41
+ try {
42
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+ var import_node_fs, import_node_path, CONFIG_FILENAME;
48
+ var init_config = __esm({
49
+ "src/server/graph/core/config.ts"() {
50
+ "use strict";
51
+ import_node_fs = require("node:fs");
52
+ import_node_path = require("node:path");
53
+ CONFIG_FILENAME = ".launchchart.json";
54
+ }
55
+ });
56
+
30
57
  // src/server/chart-serve.ts
31
58
  var chart_serve_exports = {};
32
59
  __export(chart_serve_exports, {
@@ -35,30 +62,17 @@ __export(chart_serve_exports, {
35
62
  });
36
63
  module.exports = __toCommonJS(chart_serve_exports);
37
64
  var import_node_http = __toESM(require("node:http"));
38
- var import_node_fs11 = __toESM(require("node:fs"));
39
- var import_node_path12 = __toESM(require("node:path"));
65
+ var import_node_fs13 = __toESM(require("node:fs"));
66
+ var import_node_path15 = __toESM(require("node:path"));
40
67
 
41
68
  // src/server/graph/index.ts
42
- var import_node_fs9 = require("node:fs");
43
- var import_node_path10 = require("node:path");
69
+ var import_node_fs11 = require("node:fs");
70
+ var import_node_path13 = require("node:path");
44
71
 
45
72
  // src/server/graph/core/graph-builder.ts
46
73
  var import_node_fs8 = require("node:fs");
47
74
  var import_node_path9 = require("node:path");
48
-
49
- // src/server/graph/core/config.ts
50
- var import_node_fs = require("node:fs");
51
- var import_node_path = require("node:path");
52
- var CONFIG_FILENAME = ".launchchart.json";
53
- function loadConfig(rootDir) {
54
- const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
55
- if (!(0, import_node_fs.existsSync)(configPath)) return {};
56
- try {
57
- return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
58
- } catch {
59
- return {};
60
- }
61
- }
75
+ init_config();
62
76
 
63
77
  // src/server/graph/core/parser-registry.ts
64
78
  var import_node_path8 = require("node:path");
@@ -524,34 +538,6 @@ function classifyType(id) {
524
538
  if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
525
539
  return "component";
526
540
  }
527
- function classifyModule(id) {
528
- if (/app\/\(auth\)\//.test(id)) return "auth";
529
- if (/app\/\(admin\)\//.test(id)) return "admin";
530
- if (/app\/\(settings\)\//.test(id)) return "settings";
531
- if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
532
- if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
533
- if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
534
- if (id.startsWith("app/integrations/")) return "integrations";
535
- if (id.startsWith("app/docs/")) return "admin";
536
- if (id.startsWith("client/components/ui/")) return "shared-ui";
537
- if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
538
- 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";
539
- if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
540
- if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
541
- 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";
542
- if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
543
- if (id.startsWith("server/auth/")) return "auth";
544
- if (id.startsWith("server/mcp/")) return "mcp";
545
- if (id.startsWith("server/lib/")) return "server-lib";
546
- if (id.startsWith("server/middleware")) return "middleware";
547
- if (id.startsWith("server/services/")) return "services";
548
- if (id.startsWith("server/db")) return "db";
549
- if (id.startsWith("server/errors")) return "errors";
550
- if (id.startsWith("server/")) return "server-lib";
551
- if (id.startsWith("config/")) return "config";
552
- if (id.startsWith("lib/")) return "lib";
553
- return "root";
554
- }
555
541
  function extractRoute(id) {
556
542
  if (!id.endsWith("/page.tsx")) return null;
557
543
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
@@ -772,8 +758,7 @@ function generate(rootDir) {
772
758
  const parsed = parsedByPath.get(absPath);
773
759
  const name = parsed.name || nameFromFilename(absPath);
774
760
  const route = extractRoute(id);
775
- const module_ = classifyModule(id);
776
- nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
761
+ nodes.push({ id, type, name, route, exports: parsed.exports });
777
762
  nodeIdSet.add(id);
778
763
  nodeTypeMap.set(id, type);
779
764
  if (route) routeToNodeId.set(route, id);
@@ -877,7 +862,6 @@ function generate(rootDir) {
877
862
  type: "external",
878
863
  name: parsed.name || nameFromFilename(absPath),
879
864
  route: null,
880
- module: "external",
881
865
  exports: parsed.exports
882
866
  });
883
867
  nodeIdSet.add(externalId);
@@ -1892,31 +1876,425 @@ function generateAll(rootDir) {
1892
1876
  }
1893
1877
 
1894
1878
  // src/server/graph/index.ts
1879
+ init_config();
1880
+
1881
+ // src/server/graph/core/tagger-registry.ts
1882
+ var import_node_path11 = require("node:path");
1883
+
1884
+ // src/server/graph/taggers/module-tagger.ts
1885
+ var import_node_fs9 = require("node:fs");
1886
+ var import_node_path10 = require("node:path");
1887
+ function matchGlob(pattern, id) {
1888
+ const patParts = pattern.split("/");
1889
+ const idParts = id.split("/");
1890
+ return matchParts(patParts, 0, idParts, 0);
1891
+ }
1892
+ function matchParts(pat, pi, id, ii) {
1893
+ while (pi < pat.length && ii < id.length) {
1894
+ const p = pat[pi];
1895
+ if (p === "**") {
1896
+ for (let skip = ii; skip <= id.length; skip++) {
1897
+ if (matchParts(pat, pi + 1, id, skip)) return true;
1898
+ }
1899
+ return false;
1900
+ }
1901
+ if (p === "*") {
1902
+ pi++;
1903
+ ii++;
1904
+ continue;
1905
+ }
1906
+ if (p !== id[ii]) return false;
1907
+ pi++;
1908
+ ii++;
1909
+ }
1910
+ while (pi < pat.length && pat[pi] === "**") pi++;
1911
+ return pi === pat.length && ii === id.length;
1912
+ }
1913
+ var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
1914
+ function detectConventionDirs(rootDir) {
1915
+ const result = /* @__PURE__ */ new Map();
1916
+ const searchDirs = [
1917
+ rootDir,
1918
+ (0, import_node_path10.join)(rootDir, "src"),
1919
+ (0, import_node_path10.join)(rootDir, "app"),
1920
+ (0, import_node_path10.join)(rootDir, "lib")
1921
+ ];
1922
+ for (const base of searchDirs) {
1923
+ for (const convention of CONVENTION_DIRS) {
1924
+ const dir = (0, import_node_path10.join)(base, convention);
1925
+ if (!(0, import_node_fs9.existsSync)(dir)) continue;
1926
+ try {
1927
+ const stat = (0, import_node_fs9.statSync)(dir);
1928
+ if (!stat.isDirectory()) continue;
1929
+ const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
1930
+ if (entries.length > 0) {
1931
+ const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
1932
+ result.set(relPath, entries);
1933
+ }
1934
+ } catch {
1935
+ }
1936
+ }
1937
+ }
1938
+ return result;
1939
+ }
1940
+ function extractRouteGroups(id) {
1941
+ const groups = [];
1942
+ const re = /\(([^)]+)\)/g;
1943
+ let m;
1944
+ while ((m = re.exec(id)) !== null) {
1945
+ groups.push(m[1]);
1946
+ }
1947
+ return groups;
1948
+ }
1949
+ var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
1950
+ "src",
1951
+ "app",
1952
+ "client",
1953
+ "server",
1954
+ "lib",
1955
+ "config"
1956
+ ]);
1957
+ function isRouteGroup(segment) {
1958
+ return segment.startsWith("(") && segment.endsWith(")");
1959
+ }
1960
+ function isDynamicSegment(segment) {
1961
+ return segment.startsWith("[") || segment.startsWith(":");
1962
+ }
1963
+ function isDomainDir(segment) {
1964
+ return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
1965
+ }
1966
+ var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
1967
+ // Generic app wrappers
1968
+ "app",
1969
+ "all",
1970
+ "ee",
1971
+ "home",
1972
+ "root",
1973
+ "main",
1974
+ "site",
1975
+ // Auth/access boundary wrappers — protect routes, not feature modules
1976
+ "protected",
1977
+ "authenticated",
1978
+ "authed",
1979
+ "private",
1980
+ "public",
1981
+ "logged-in",
1982
+ "logged-out",
1983
+ "unprotected",
1984
+ "unauthenticated",
1985
+ "auth-required",
1986
+ "no-auth",
1987
+ "guest-only"
1988
+ ]);
1989
+ function isTrivialGroup(name, extraTrivial) {
1990
+ if (TRIVIAL_GROUPS.has(name)) return true;
1991
+ if (extraTrivial?.has(name)) return true;
1992
+ const lower = name.toLowerCase();
1993
+ const wrapperPatterns = [
1994
+ /^.*-?wrapper$/,
1995
+ // "page-wrapper", "use-page-wrapper"
1996
+ /^.*-?layout$/,
1997
+ // "admin-layout", "settings-layout"
1998
+ /^use-/,
1999
+ // "use-page-wrapper"
2000
+ /^default$/
2001
+ ];
2002
+ return wrapperPatterns.some((p) => p.test(lower));
2003
+ }
2004
+ function normalizeGroupName(name) {
2005
+ return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
2006
+ }
2007
+ function extractModuleFromPath(id, extraTrivial) {
2008
+ const segments = id.split("/");
2009
+ const routeGroups = extractRouteGroups(id);
2010
+ const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
2011
+ if (moduleGroups.length > 0) {
2012
+ return moduleGroups[moduleGroups.length - 1];
2013
+ }
2014
+ const meaningful = [];
2015
+ for (const seg of segments) {
2016
+ if (seg.includes(".")) continue;
2017
+ if (isRouteGroup(seg)) continue;
2018
+ if (isDynamicSegment(seg)) continue;
2019
+ if (isDomainDir(seg)) continue;
2020
+ if (SKIP_SEGMENTS.has(seg)) continue;
2021
+ meaningful.push(seg);
2022
+ }
2023
+ if (meaningful.length > 0) {
2024
+ return meaningful[0];
2025
+ }
2026
+ return "root";
2027
+ }
2028
+ var cachedRootDir = null;
2029
+ var cachedConventionDirs = /* @__PURE__ */ new Map();
2030
+ var moduleTagger = {
2031
+ id: "module",
2032
+ tagKey: "module",
2033
+ trackUntagged: true,
2034
+ layers: null,
2035
+ // applies to all layers
2036
+ tag(nodes, layer, rootDir) {
2037
+ if (cachedRootDir !== rootDir) {
2038
+ cachedConventionDirs = detectConventionDirs(rootDir);
2039
+ cachedRootDir = rootDir;
2040
+ }
2041
+ let configRules = [];
2042
+ let extraTrivial;
2043
+ try {
2044
+ const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
2045
+ const config = loadConfig2(rootDir);
2046
+ configRules = config.taggers?.module?.rules ?? [];
2047
+ const trivialFromConfig = config.taggers?.module?.trivialGroups;
2048
+ if (trivialFromConfig?.length) {
2049
+ extraTrivial = new Set(trivialFromConfig);
2050
+ }
2051
+ } catch {
2052
+ }
2053
+ const result = /* @__PURE__ */ new Map();
2054
+ for (const node of nodes) {
2055
+ const id = node.id;
2056
+ let matched = false;
2057
+ for (const rule of configRules) {
2058
+ if (matchGlob(rule.match, id)) {
2059
+ result.set(id, rule.module);
2060
+ matched = true;
2061
+ break;
2062
+ }
2063
+ }
2064
+ if (matched) continue;
2065
+ matched = false;
2066
+ for (const [convDir, moduleNames] of cachedConventionDirs) {
2067
+ if (id.startsWith(convDir + "/")) {
2068
+ const rest = id.slice(convDir.length + 1);
2069
+ const firstSeg = rest.split("/")[0];
2070
+ if (moduleNames.includes(firstSeg)) {
2071
+ result.set(id, firstSeg);
2072
+ matched = true;
2073
+ break;
2074
+ }
2075
+ }
2076
+ }
2077
+ if (matched) continue;
2078
+ const module2 = extractModuleFromPath(id, extraTrivial);
2079
+ result.set(id, module2);
2080
+ }
2081
+ return result;
2082
+ }
2083
+ };
2084
+
2085
+ // src/server/graph/taggers/screen-tagger.ts
2086
+ var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
2087
+ var screenTagger = {
2088
+ id: "screen",
2089
+ tagKey: "screen",
2090
+ trackUntagged: true,
2091
+ layers: ["ui"],
2092
+ tag(nodes, layer) {
2093
+ if (layer !== "ui") return /* @__PURE__ */ new Map();
2094
+ const result = /* @__PURE__ */ new Map();
2095
+ for (const node of nodes) {
2096
+ if (SCREEN_TYPES.has(node.type)) {
2097
+ result.set(node.id, "true");
2098
+ }
2099
+ }
2100
+ return result;
2101
+ }
2102
+ };
2103
+
2104
+ // src/server/graph/core/tagger-registry.ts
2105
+ var TaggerRegistry = class {
2106
+ constructor() {
2107
+ this.taggers = [];
2108
+ this.ids = /* @__PURE__ */ new Set();
2109
+ }
2110
+ register(tagger) {
2111
+ if (this.ids.has(tagger.id)) {
2112
+ throw new Error(`Duplicate tagger id: ${tagger.id}`);
2113
+ }
2114
+ this.ids.add(tagger.id);
2115
+ this.taggers.push(tagger);
2116
+ }
2117
+ getAll() {
2118
+ return this.taggers;
2119
+ }
2120
+ getForLayer(layer) {
2121
+ return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
2122
+ }
2123
+ };
2124
+ var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
2125
+ function registerBuiltins2(registry, disabled, config) {
2126
+ for (const tagger of BUILTIN_TAGGERS) {
2127
+ if (disabled.has(tagger.id)) continue;
2128
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2129
+ if (override !== void 0) {
2130
+ tagger.trackUntagged = override;
2131
+ }
2132
+ registry.register(tagger);
2133
+ }
2134
+ }
2135
+ function loadCustomTaggers(registry, config, rootDir, disabled) {
2136
+ for (const entry of config.taggers?.custom ?? []) {
2137
+ if (disabled.has(entry.id)) continue;
2138
+ try {
2139
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
2140
+ const mod = require(absPath);
2141
+ const tagger = "default" in mod ? mod.default : mod;
2142
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2143
+ if (override !== void 0) {
2144
+ tagger.trackUntagged = override;
2145
+ }
2146
+ registry.register(tagger);
2147
+ } catch (err) {
2148
+ process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err}
2149
+ `);
2150
+ }
2151
+ }
2152
+ }
2153
+ function createTaggerRegistry(config, rootDir) {
2154
+ const registry = new TaggerRegistry();
2155
+ const disabled = new Set(config.taggers?.disabled ?? []);
2156
+ registerBuiltins2(registry, disabled, config);
2157
+ loadCustomTaggers(registry, config, rootDir, disabled);
2158
+ return registry;
2159
+ }
2160
+
2161
+ // src/server/graph/core/tag-store.ts
2162
+ var import_node_fs10 = require("node:fs");
2163
+ var import_node_path12 = require("node:path");
2164
+ var TAGS_FILENAME = "tags.json";
1895
2165
  var GRAPHS_DIR = ".launchsecure/graphs";
2166
+ var tagCache = /* @__PURE__ */ new Map();
2167
+ function tagsFilePath(rootDir) {
2168
+ return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2169
+ }
2170
+ function readTagStore(rootDir) {
2171
+ const filePath = tagsFilePath(rootDir);
2172
+ if (!(0, import_node_fs10.existsSync)(filePath)) return {};
2173
+ const stat = (0, import_node_fs10.statSync)(filePath);
2174
+ const cached = tagCache.get(filePath);
2175
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
2176
+ return cached.store;
2177
+ }
2178
+ try {
2179
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2180
+ const store = JSON.parse(content);
2181
+ tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2182
+ return store;
2183
+ } catch {
2184
+ return {};
2185
+ }
2186
+ }
2187
+ function writeTagStore(rootDir, store) {
2188
+ const filePath = tagsFilePath(rootDir);
2189
+ const dir = (0, import_node_path12.dirname)(filePath);
2190
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
2191
+ const cleaned = {};
2192
+ for (const [nodeId, tags] of Object.entries(store)) {
2193
+ if (Object.keys(tags).length > 0) {
2194
+ cleaned[nodeId] = tags;
2195
+ }
2196
+ }
2197
+ (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2198
+ tagCache.delete(filePath);
2199
+ }
2200
+ function setTag(rootDir, nodeId, key, value) {
2201
+ const store = readTagStore(rootDir);
2202
+ if (!store[nodeId]) store[nodeId] = {};
2203
+ store[nodeId][key] = value;
2204
+ writeTagStore(rootDir, store);
2205
+ }
2206
+ function removeTag(rootDir, nodeId, key) {
2207
+ const store = readTagStore(rootDir);
2208
+ if (!store[nodeId]) return;
2209
+ delete store[nodeId][key];
2210
+ if (Object.keys(store[nodeId]).length === 0) {
2211
+ delete store[nodeId];
2212
+ }
2213
+ writeTagStore(rootDir, store);
2214
+ }
2215
+
2216
+ // src/server/graph/index.ts
2217
+ var GRAPHS_DIR2 = ".launchsecure/graphs";
1896
2218
  var LAYERS = ["ui", "api", "db"];
1897
2219
  var graphCache = /* @__PURE__ */ new Map();
2220
+ var taggedCache = /* @__PURE__ */ new Map();
1898
2221
  function graphsDir(rootDir) {
1899
- return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
2222
+ return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
1900
2223
  }
1901
2224
  function graphFilePath(rootDir, layer) {
1902
- return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
2225
+ return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
2226
+ }
2227
+ function tagsFilePath2(rootDir) {
2228
+ return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
2229
+ }
2230
+ function getMtimeMs(filePath) {
2231
+ if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
2232
+ return (0, import_node_fs11.statSync)(filePath).mtimeMs;
1903
2233
  }
1904
2234
  function invalidateCache(filePath) {
1905
2235
  graphCache.delete(filePath);
1906
2236
  }
1907
- function readGraph(rootDir, layer) {
2237
+ function invalidateTaggedCache(rootDir, layer) {
2238
+ taggedCache.delete(`${rootDir}:${layer}`);
2239
+ }
2240
+ function applyTags(graph, layer, rootDir) {
2241
+ const config = loadConfig(rootDir);
2242
+ const registry = createTaggerRegistry(config, rootDir);
2243
+ const manualTags = readTagStore(rootDir);
2244
+ const taggedNodes = graph.nodes.map((n) => ({ ...n }));
2245
+ const taggers = registry.getForLayer(layer);
2246
+ for (const tagger of taggers) {
2247
+ const assignments = tagger.tag(taggedNodes, layer, rootDir);
2248
+ for (const node of taggedNodes) {
2249
+ if (!node.tags) node.tags = {};
2250
+ const tags = node.tags;
2251
+ const value = assignments.get(node.id);
2252
+ if (value !== void 0) {
2253
+ tags[tagger.tagKey] = value;
2254
+ } else if (tagger.trackUntagged) {
2255
+ tags[tagger.tagKey] = "untagged";
2256
+ }
2257
+ }
2258
+ }
2259
+ for (const node of taggedNodes) {
2260
+ const manual = manualTags[node.id];
2261
+ if (manual) {
2262
+ if (!node.tags) node.tags = {};
2263
+ const tags = node.tags;
2264
+ Object.assign(tags, manual);
2265
+ }
2266
+ }
2267
+ return { ...graph, nodes: taggedNodes };
2268
+ }
2269
+ function readGraphRaw(rootDir, layer) {
1908
2270
  const filePath = graphFilePath(rootDir, layer);
1909
- if (!(0, import_node_fs9.existsSync)(filePath)) return null;
1910
- const stat = (0, import_node_fs9.statSync)(filePath);
2271
+ if (!(0, import_node_fs11.existsSync)(filePath)) return null;
2272
+ const stat = (0, import_node_fs11.statSync)(filePath);
1911
2273
  const cached = graphCache.get(filePath);
1912
2274
  if (cached && cached.mtimeMs === stat.mtimeMs) {
1913
2275
  return cached.graph;
1914
2276
  }
1915
- const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
2277
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
1916
2278
  const graph = JSON.parse(content);
1917
2279
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1918
2280
  return graph;
1919
2281
  }
2282
+ function readGraph(rootDir, layer) {
2283
+ const rawFilePath = graphFilePath(rootDir, layer);
2284
+ if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
2285
+ const rawMtime = getMtimeMs(rawFilePath);
2286
+ const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2287
+ const cacheKey = `${rootDir}:${layer}`;
2288
+ const cached = taggedCache.get(cacheKey);
2289
+ if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
2290
+ return cached.graph;
2291
+ }
2292
+ const raw = readGraphRaw(rootDir, layer);
2293
+ if (!raw) return null;
2294
+ const tagged = applyTags(raw, layer, rootDir);
2295
+ taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
2296
+ return tagged;
2297
+ }
1920
2298
  function readAllGraphs(rootDir) {
1921
2299
  const result = {};
1922
2300
  for (const layer of LAYERS) {
@@ -1927,32 +2305,52 @@ function readAllGraphs(rootDir) {
1927
2305
  }
1928
2306
  function generateGraph(rootDir, layer) {
1929
2307
  const dir = graphsDir(rootDir);
1930
- (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
2308
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
1931
2309
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1932
2310
  for (const result of results) {
1933
2311
  const filePath = graphFilePath(rootDir, result.layer);
1934
- (0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2312
+ (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1935
2313
  invalidateCache(filePath);
2314
+ invalidateTaggedCache(rootDir, result.layer);
1936
2315
  }
1937
2316
  return results;
1938
2317
  }
1939
2318
 
1940
2319
  // src/server/lockfile.ts
1941
2320
  var import_node_child_process = require("node:child_process");
1942
- var import_node_fs10 = require("node:fs");
2321
+ var import_node_fs12 = require("node:fs");
1943
2322
  var import_node_os = require("node:os");
1944
- var import_node_path11 = require("node:path");
1945
- function lockDir() {
1946
- return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
1947
- }
1948
- function lockPath() {
1949
- return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
1950
- }
1951
- function readLock() {
1952
- const p = lockPath();
1953
- if (!(0, import_node_fs10.existsSync)(p)) return null;
2323
+ var import_node_path14 = require("node:path");
2324
+ function lockDir(projectRoot) {
2325
+ if (projectRoot) {
2326
+ return (0, import_node_path14.join)(projectRoot, ".launchsecure");
2327
+ }
2328
+ return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
2329
+ }
2330
+ function lockPath(projectRoot) {
2331
+ return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
2332
+ }
2333
+ var _activeProjectRoot;
2334
+ function readLock(projectRoot) {
2335
+ const root = projectRoot ?? _activeProjectRoot;
2336
+ const p = lockPath(root);
2337
+ if (!(0, import_node_fs12.existsSync)(p)) {
2338
+ if (root) {
2339
+ const globalP = lockPath();
2340
+ if ((0, import_node_fs12.existsSync)(globalP)) {
2341
+ try {
2342
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
2343
+ if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
2344
+ return data;
2345
+ }
2346
+ } catch {
2347
+ }
2348
+ }
2349
+ }
2350
+ return null;
2351
+ }
1954
2352
  try {
1955
- const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
2353
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
1956
2354
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
1957
2355
  return data;
1958
2356
  } catch {
@@ -1981,34 +2379,41 @@ function getListenerPid(port) {
1981
2379
  return null;
1982
2380
  }
1983
2381
  }
1984
- function getLiveLock() {
1985
- const lock = readLock();
2382
+ function getLiveLock(projectRoot) {
2383
+ const root = projectRoot ?? _activeProjectRoot;
2384
+ const lock = readLock(root);
1986
2385
  if (!lock) return null;
1987
2386
  const listenerPid = getListenerPid(lock.port);
1988
2387
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
1989
2388
  if (!live) {
1990
2389
  try {
1991
- (0, import_node_fs10.unlinkSync)(lockPath());
2390
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
1992
2391
  } catch {
1993
2392
  }
1994
2393
  return null;
1995
2394
  }
1996
2395
  return lock;
1997
2396
  }
1998
- function writeLock(data) {
1999
- (0, import_node_fs10.mkdirSync)(lockDir(), { recursive: true });
2000
- (0, import_node_fs10.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
2397
+ function writeLock(data, projectRoot) {
2398
+ const root = projectRoot ?? _activeProjectRoot;
2399
+ (0, import_node_fs12.mkdirSync)(lockDir(root), { recursive: true });
2400
+ (0, import_node_fs12.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
2401
+ if (root) _activeProjectRoot = root;
2001
2402
  }
2002
- function clearLock() {
2403
+ function clearLock(projectRoot) {
2404
+ const root = projectRoot ?? _activeProjectRoot;
2003
2405
  try {
2004
- (0, import_node_fs10.unlinkSync)(lockPath());
2406
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
2005
2407
  } catch {
2006
2408
  }
2007
2409
  }
2008
2410
 
2009
2411
  // src/server/chart-serve.ts
2010
- var DEFAULT_PORT = 52819;
2011
- var MAX_PORT_SCAN = 20;
2412
+ init_config();
2413
+ var MAX_PORT_SCAN = 3;
2414
+ function randomPort() {
2415
+ return 49152 + Math.floor(Math.random() * (65535 - 49152));
2416
+ }
2012
2417
  var MIME_TYPES = {
2013
2418
  ".html": "text/html; charset=utf-8",
2014
2419
  ".js": "application/javascript; charset=utf-8",
@@ -2023,16 +2428,16 @@ var MIME_TYPES = {
2023
2428
  function findProjectRoot(startDir) {
2024
2429
  let dir = startDir;
2025
2430
  for (let i = 0; i < 8; i++) {
2026
- const graphsDir2 = import_node_path12.default.join(dir, ".launchsecure", "graphs");
2027
- if (import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "ui.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "api.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "db.json"))) return dir;
2028
- const parent = import_node_path12.default.dirname(dir);
2431
+ const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2432
+ 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;
2433
+ const parent = import_node_path15.default.dirname(dir);
2029
2434
  if (parent === dir) break;
2030
2435
  dir = parent;
2031
2436
  }
2032
2437
  dir = startDir;
2033
2438
  for (let i = 0; i < 8; i++) {
2034
- if (import_node_fs11.default.existsSync(import_node_path12.default.join(dir, ".git"))) return dir;
2035
- const parent = import_node_path12.default.dirname(dir);
2439
+ if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2440
+ const parent = import_node_path15.default.dirname(dir);
2036
2441
  if (parent === dir) break;
2037
2442
  dir = parent;
2038
2443
  }
@@ -2084,16 +2489,16 @@ function buildMergedGraph(projectRoot) {
2084
2489
  };
2085
2490
  }
2086
2491
  function serveStatic(res, filePath) {
2087
- if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.statSync(filePath).isFile()) return false;
2088
- const ext = import_node_path12.default.extname(filePath).toLowerCase();
2492
+ if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2493
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
2089
2494
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2090
2495
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2091
- import_node_fs11.default.createReadStream(filePath).pipe(res);
2496
+ import_node_fs13.default.createReadStream(filePath).pipe(res);
2092
2497
  return true;
2093
2498
  }
2094
2499
  function serveIndex(res, clientDir) {
2095
- const indexPath = import_node_path12.default.join(clientDir, "index.html");
2096
- if (!import_node_fs11.default.existsSync(indexPath)) {
2500
+ const indexPath = import_node_path15.default.join(clientDir, "index.html");
2501
+ if (!import_node_fs13.default.existsSync(indexPath)) {
2097
2502
  res.writeHead(500, { "Content-Type": "text/plain" });
2098
2503
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2099
2504
  return;
@@ -2101,14 +2506,14 @@ function serveIndex(res, clientDir) {
2101
2506
  serveStatic(res, indexPath);
2102
2507
  }
2103
2508
  function tryListen(server, port) {
2104
- return new Promise((resolve2, reject) => {
2509
+ return new Promise((resolve3, reject) => {
2105
2510
  const onError = (err) => {
2106
2511
  server.off("listening", onListening);
2107
2512
  reject(err);
2108
2513
  };
2109
2514
  const onListening = () => {
2110
2515
  server.off("error", onError);
2111
- resolve2(port);
2516
+ resolve3(port);
2112
2517
  };
2113
2518
  server.once("error", onError);
2114
2519
  server.once("listening", onListening);
@@ -2135,7 +2540,7 @@ async function bindWithFallback(server, startPort) {
2135
2540
  async function startChartServer(opts = {}) {
2136
2541
  const cwd = opts.cwd ?? process.cwd();
2137
2542
  const projectRoot = findProjectRoot(cwd);
2138
- const existing = getLiveLock();
2543
+ const existing = getLiveLock(projectRoot);
2139
2544
  if (existing) {
2140
2545
  if (!opts.quiet) {
2141
2546
  process.stderr.write(
@@ -2145,7 +2550,7 @@ async function startChartServer(opts = {}) {
2145
2550
  }
2146
2551
  return { port: existing.port, url: existing.url };
2147
2552
  }
2148
- const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
2553
+ const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
2149
2554
  const server = import_node_http.default.createServer((req, res) => {
2150
2555
  try {
2151
2556
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -2183,6 +2588,26 @@ async function startChartServer(opts = {}) {
2183
2588
  }
2184
2589
  return;
2185
2590
  }
2591
+ if (req.method === "GET" && url2.pathname === "/api/file-content") {
2592
+ const relPath = url2.searchParams.get("path");
2593
+ if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
2594
+ res.writeHead(400, { "Content-Type": "application/json" });
2595
+ res.end(JSON.stringify({ error: "Invalid path" }));
2596
+ return;
2597
+ }
2598
+ const filePath = import_node_path15.default.join(projectRoot, relPath);
2599
+ if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
2600
+ res.writeHead(404, { "Content-Type": "application/json" });
2601
+ res.end(JSON.stringify({ error: "File not found" }));
2602
+ return;
2603
+ }
2604
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
2605
+ const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2606
+ const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
2607
+ res.writeHead(200, { "Content-Type": "application/json" });
2608
+ res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2609
+ return;
2610
+ }
2186
2611
  if (req.method === "GET" && url2.pathname === "/api/health") {
2187
2612
  res.writeHead(200, { "Content-Type": "application/json" });
2188
2613
  res.end(JSON.stringify({ ok: true, projectRoot }));
@@ -2212,8 +2637,94 @@ async function startChartServer(opts = {}) {
2212
2637
  req.on("end", () => {
2213
2638
  try {
2214
2639
  const newConfig = JSON.parse(body);
2215
- const configPath = import_node_path12.default.join(projectRoot, ".launchchart.json");
2216
- import_node_fs11.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2640
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2641
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2642
+ res.writeHead(200, { "Content-Type": "application/json" });
2643
+ res.end(JSON.stringify({ ok: true }));
2644
+ } catch (err) {
2645
+ res.writeHead(400, { "Content-Type": "application/json" });
2646
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2647
+ }
2648
+ });
2649
+ return;
2650
+ }
2651
+ if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2652
+ const config = loadConfig(projectRoot);
2653
+ const builtinTaggers = [
2654
+ { id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
2655
+ { id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
2656
+ ];
2657
+ const disabled = config.taggers?.disabled ?? [];
2658
+ const customTaggers = config.taggers?.custom ?? [];
2659
+ const moduleRules = config.taggers?.module?.rules ?? [];
2660
+ res.writeHead(200, { "Content-Type": "application/json" });
2661
+ res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
2662
+ return;
2663
+ }
2664
+ if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
2665
+ let body = "";
2666
+ req.on("data", (chunk) => {
2667
+ body += chunk.toString();
2668
+ });
2669
+ req.on("end", () => {
2670
+ try {
2671
+ const taggerConfig = JSON.parse(body);
2672
+ const config = loadConfig(projectRoot);
2673
+ config.taggers = taggerConfig;
2674
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2675
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2676
+ res.writeHead(200, { "Content-Type": "application/json" });
2677
+ res.end(JSON.stringify({ ok: true }));
2678
+ } catch (err) {
2679
+ res.writeHead(400, { "Content-Type": "application/json" });
2680
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2681
+ }
2682
+ });
2683
+ return;
2684
+ }
2685
+ if (req.method === "GET" && url2.pathname === "/api/tags") {
2686
+ const store = readTagStore(projectRoot);
2687
+ res.writeHead(200, { "Content-Type": "application/json" });
2688
+ res.end(JSON.stringify(store));
2689
+ return;
2690
+ }
2691
+ if (req.method === "POST" && url2.pathname === "/api/tags") {
2692
+ let body = "";
2693
+ req.on("data", (chunk) => {
2694
+ body += chunk.toString();
2695
+ });
2696
+ req.on("end", () => {
2697
+ try {
2698
+ const { nodeId, key, value } = JSON.parse(body);
2699
+ if (!nodeId || !key || !value) {
2700
+ res.writeHead(400, { "Content-Type": "application/json" });
2701
+ res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
2702
+ return;
2703
+ }
2704
+ setTag(projectRoot, nodeId, key, value);
2705
+ res.writeHead(200, { "Content-Type": "application/json" });
2706
+ res.end(JSON.stringify({ ok: true }));
2707
+ } catch (err) {
2708
+ res.writeHead(400, { "Content-Type": "application/json" });
2709
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2710
+ }
2711
+ });
2712
+ return;
2713
+ }
2714
+ if (req.method === "DELETE" && url2.pathname === "/api/tags") {
2715
+ let body = "";
2716
+ req.on("data", (chunk) => {
2717
+ body += chunk.toString();
2718
+ });
2719
+ req.on("end", () => {
2720
+ try {
2721
+ const { nodeId, key } = JSON.parse(body);
2722
+ if (!nodeId || !key) {
2723
+ res.writeHead(400, { "Content-Type": "application/json" });
2724
+ res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
2725
+ return;
2726
+ }
2727
+ removeTag(projectRoot, nodeId, key);
2217
2728
  res.writeHead(200, { "Content-Type": "application/json" });
2218
2729
  res.end(JSON.stringify({ ok: true }));
2219
2730
  } catch (err) {
@@ -2224,7 +2735,7 @@ async function startChartServer(opts = {}) {
2224
2735
  return;
2225
2736
  }
2226
2737
  if (url2.pathname !== "/") {
2227
- const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
2738
+ const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
2228
2739
  if (serveStatic(res, staticPath)) return;
2229
2740
  }
2230
2741
  serveIndex(res, clientDir);
@@ -2233,7 +2744,8 @@ async function startChartServer(opts = {}) {
2233
2744
  res.end(JSON.stringify({ error: String(err) }));
2234
2745
  }
2235
2746
  });
2236
- const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
2747
+ const startPort = opts.port ?? randomPort();
2748
+ const port = await bindWithFallback(server, startPort);
2237
2749
  const url = `http://localhost:${port}`;
2238
2750
  writeLock({
2239
2751
  pid: process.pid,
@@ -2241,9 +2753,9 @@ async function startChartServer(opts = {}) {
2241
2753
  cwd,
2242
2754
  url,
2243
2755
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2244
- });
2756
+ }, projectRoot);
2245
2757
  const cleanup = () => {
2246
- clearLock();
2758
+ clearLock(projectRoot);
2247
2759
  server.close();
2248
2760
  };
2249
2761
  process.once("SIGINT", () => {