@launchsecure/launch-kit 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,403 @@ 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
+ "app",
1968
+ "all",
1969
+ "ee",
1970
+ "home",
1971
+ "root"
1972
+ ]);
1973
+ function isTrivialGroup(name) {
1974
+ if (TRIVIAL_GROUPS.has(name)) return true;
1975
+ const lower = name.toLowerCase();
1976
+ const wrapperPatterns = [
1977
+ /^.*-?wrapper$/,
1978
+ // "page-wrapper", "use-page-wrapper"
1979
+ /^.*-?layout$/,
1980
+ // "admin-layout", "settings-layout"
1981
+ /^use-/,
1982
+ // "use-page-wrapper"
1983
+ /^default$/
1984
+ ];
1985
+ return wrapperPatterns.some((p) => p.test(lower));
1986
+ }
1987
+ function normalizeGroupName(name) {
1988
+ return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
1989
+ }
1990
+ function extractModuleFromPath(id) {
1991
+ const segments = id.split("/");
1992
+ const routeGroups = extractRouteGroups(id);
1993
+ const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
1994
+ if (moduleGroups.length > 0) {
1995
+ return moduleGroups[moduleGroups.length - 1];
1996
+ }
1997
+ const meaningful = [];
1998
+ for (const seg of segments) {
1999
+ if (seg.includes(".")) continue;
2000
+ if (isRouteGroup(seg)) continue;
2001
+ if (isDynamicSegment(seg)) continue;
2002
+ if (isDomainDir(seg)) continue;
2003
+ if (SKIP_SEGMENTS.has(seg)) continue;
2004
+ meaningful.push(seg);
2005
+ }
2006
+ if (meaningful.length > 0) {
2007
+ return meaningful[0];
2008
+ }
2009
+ return "root";
2010
+ }
2011
+ var cachedRootDir = null;
2012
+ var cachedConventionDirs = /* @__PURE__ */ new Map();
2013
+ var moduleTagger = {
2014
+ id: "module",
2015
+ tagKey: "module",
2016
+ trackUntagged: true,
2017
+ layers: null,
2018
+ // applies to all layers
2019
+ tag(nodes, layer, rootDir) {
2020
+ if (cachedRootDir !== rootDir) {
2021
+ cachedConventionDirs = detectConventionDirs(rootDir);
2022
+ cachedRootDir = rootDir;
2023
+ }
2024
+ let configRules = [];
2025
+ try {
2026
+ const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
2027
+ const config = loadConfig2(rootDir);
2028
+ configRules = config.taggers?.module?.rules ?? [];
2029
+ } catch {
2030
+ }
2031
+ const result = /* @__PURE__ */ new Map();
2032
+ for (const node of nodes) {
2033
+ const id = node.id;
2034
+ let matched = false;
2035
+ for (const rule of configRules) {
2036
+ if (matchGlob(rule.match, id)) {
2037
+ result.set(id, rule.module);
2038
+ matched = true;
2039
+ break;
2040
+ }
2041
+ }
2042
+ if (matched) continue;
2043
+ matched = false;
2044
+ for (const [convDir, moduleNames] of cachedConventionDirs) {
2045
+ if (id.startsWith(convDir + "/")) {
2046
+ const rest = id.slice(convDir.length + 1);
2047
+ const firstSeg = rest.split("/")[0];
2048
+ if (moduleNames.includes(firstSeg)) {
2049
+ result.set(id, firstSeg);
2050
+ matched = true;
2051
+ break;
2052
+ }
2053
+ }
2054
+ }
2055
+ if (matched) continue;
2056
+ const module2 = extractModuleFromPath(id);
2057
+ result.set(id, module2);
2058
+ }
2059
+ return result;
2060
+ }
2061
+ };
2062
+
2063
+ // src/server/graph/taggers/screen-tagger.ts
2064
+ var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
2065
+ var screenTagger = {
2066
+ id: "screen",
2067
+ tagKey: "screen",
2068
+ trackUntagged: true,
2069
+ layers: ["ui"],
2070
+ tag(nodes, layer) {
2071
+ if (layer !== "ui") return /* @__PURE__ */ new Map();
2072
+ const result = /* @__PURE__ */ new Map();
2073
+ for (const node of nodes) {
2074
+ if (SCREEN_TYPES.has(node.type)) {
2075
+ result.set(node.id, "true");
2076
+ }
2077
+ }
2078
+ return result;
2079
+ }
2080
+ };
2081
+
2082
+ // src/server/graph/core/tagger-registry.ts
2083
+ var TaggerRegistry = class {
2084
+ constructor() {
2085
+ this.taggers = [];
2086
+ this.ids = /* @__PURE__ */ new Set();
2087
+ }
2088
+ register(tagger) {
2089
+ if (this.ids.has(tagger.id)) {
2090
+ throw new Error(`Duplicate tagger id: ${tagger.id}`);
2091
+ }
2092
+ this.ids.add(tagger.id);
2093
+ this.taggers.push(tagger);
2094
+ }
2095
+ getAll() {
2096
+ return this.taggers;
2097
+ }
2098
+ getForLayer(layer) {
2099
+ return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
2100
+ }
2101
+ };
2102
+ var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
2103
+ function registerBuiltins2(registry, disabled, config) {
2104
+ for (const tagger of BUILTIN_TAGGERS) {
2105
+ if (disabled.has(tagger.id)) continue;
2106
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2107
+ if (override !== void 0) {
2108
+ tagger.trackUntagged = override;
2109
+ }
2110
+ registry.register(tagger);
2111
+ }
2112
+ }
2113
+ function loadCustomTaggers(registry, config, rootDir, disabled) {
2114
+ for (const entry of config.taggers?.custom ?? []) {
2115
+ if (disabled.has(entry.id)) continue;
2116
+ try {
2117
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
2118
+ const mod = require(absPath);
2119
+ const tagger = "default" in mod ? mod.default : mod;
2120
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2121
+ if (override !== void 0) {
2122
+ tagger.trackUntagged = override;
2123
+ }
2124
+ registry.register(tagger);
2125
+ } catch (err) {
2126
+ process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err}
2127
+ `);
2128
+ }
2129
+ }
2130
+ }
2131
+ function createTaggerRegistry(config, rootDir) {
2132
+ const registry = new TaggerRegistry();
2133
+ const disabled = new Set(config.taggers?.disabled ?? []);
2134
+ registerBuiltins2(registry, disabled, config);
2135
+ loadCustomTaggers(registry, config, rootDir, disabled);
2136
+ return registry;
2137
+ }
2138
+
2139
+ // src/server/graph/core/tag-store.ts
2140
+ var import_node_fs10 = require("node:fs");
2141
+ var import_node_path12 = require("node:path");
2142
+ var TAGS_FILENAME = "tags.json";
1895
2143
  var GRAPHS_DIR = ".launchsecure/graphs";
2144
+ var tagCache = /* @__PURE__ */ new Map();
2145
+ function tagsFilePath(rootDir) {
2146
+ return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2147
+ }
2148
+ function readTagStore(rootDir) {
2149
+ const filePath = tagsFilePath(rootDir);
2150
+ if (!(0, import_node_fs10.existsSync)(filePath)) return {};
2151
+ const stat = (0, import_node_fs10.statSync)(filePath);
2152
+ const cached = tagCache.get(filePath);
2153
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
2154
+ return cached.store;
2155
+ }
2156
+ try {
2157
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2158
+ const store = JSON.parse(content);
2159
+ tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2160
+ return store;
2161
+ } catch {
2162
+ return {};
2163
+ }
2164
+ }
2165
+ function writeTagStore(rootDir, store) {
2166
+ const filePath = tagsFilePath(rootDir);
2167
+ const dir = (0, import_node_path12.dirname)(filePath);
2168
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
2169
+ const cleaned = {};
2170
+ for (const [nodeId, tags] of Object.entries(store)) {
2171
+ if (Object.keys(tags).length > 0) {
2172
+ cleaned[nodeId] = tags;
2173
+ }
2174
+ }
2175
+ (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2176
+ tagCache.delete(filePath);
2177
+ }
2178
+ function setTag(rootDir, nodeId, key, value) {
2179
+ const store = readTagStore(rootDir);
2180
+ if (!store[nodeId]) store[nodeId] = {};
2181
+ store[nodeId][key] = value;
2182
+ writeTagStore(rootDir, store);
2183
+ }
2184
+ function removeTag(rootDir, nodeId, key) {
2185
+ const store = readTagStore(rootDir);
2186
+ if (!store[nodeId]) return;
2187
+ delete store[nodeId][key];
2188
+ if (Object.keys(store[nodeId]).length === 0) {
2189
+ delete store[nodeId];
2190
+ }
2191
+ writeTagStore(rootDir, store);
2192
+ }
2193
+
2194
+ // src/server/graph/index.ts
2195
+ var GRAPHS_DIR2 = ".launchsecure/graphs";
1896
2196
  var LAYERS = ["ui", "api", "db"];
1897
2197
  var graphCache = /* @__PURE__ */ new Map();
2198
+ var taggedCache = /* @__PURE__ */ new Map();
1898
2199
  function graphsDir(rootDir) {
1899
- return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
2200
+ return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
1900
2201
  }
1901
2202
  function graphFilePath(rootDir, layer) {
1902
- return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
2203
+ return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
2204
+ }
2205
+ function tagsFilePath2(rootDir) {
2206
+ return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
2207
+ }
2208
+ function getMtimeMs(filePath) {
2209
+ if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
2210
+ return (0, import_node_fs11.statSync)(filePath).mtimeMs;
1903
2211
  }
1904
2212
  function invalidateCache(filePath) {
1905
2213
  graphCache.delete(filePath);
1906
2214
  }
1907
- function readGraph(rootDir, layer) {
2215
+ function invalidateTaggedCache(rootDir, layer) {
2216
+ taggedCache.delete(`${rootDir}:${layer}`);
2217
+ }
2218
+ function applyTags(graph, layer, rootDir) {
2219
+ const config = loadConfig(rootDir);
2220
+ const registry = createTaggerRegistry(config, rootDir);
2221
+ const manualTags = readTagStore(rootDir);
2222
+ const taggedNodes = graph.nodes.map((n) => ({ ...n }));
2223
+ const taggers = registry.getForLayer(layer);
2224
+ for (const tagger of taggers) {
2225
+ const assignments = tagger.tag(taggedNodes, layer, rootDir);
2226
+ for (const node of taggedNodes) {
2227
+ if (!node.tags) node.tags = {};
2228
+ const tags = node.tags;
2229
+ const value = assignments.get(node.id);
2230
+ if (value !== void 0) {
2231
+ tags[tagger.tagKey] = value;
2232
+ } else if (tagger.trackUntagged) {
2233
+ tags[tagger.tagKey] = "untagged";
2234
+ }
2235
+ }
2236
+ }
2237
+ for (const node of taggedNodes) {
2238
+ const manual = manualTags[node.id];
2239
+ if (manual) {
2240
+ if (!node.tags) node.tags = {};
2241
+ const tags = node.tags;
2242
+ Object.assign(tags, manual);
2243
+ }
2244
+ }
2245
+ return { ...graph, nodes: taggedNodes };
2246
+ }
2247
+ function readGraphRaw(rootDir, layer) {
1908
2248
  const filePath = graphFilePath(rootDir, layer);
1909
- if (!(0, import_node_fs9.existsSync)(filePath)) return null;
1910
- const stat = (0, import_node_fs9.statSync)(filePath);
2249
+ if (!(0, import_node_fs11.existsSync)(filePath)) return null;
2250
+ const stat = (0, import_node_fs11.statSync)(filePath);
1911
2251
  const cached = graphCache.get(filePath);
1912
2252
  if (cached && cached.mtimeMs === stat.mtimeMs) {
1913
2253
  return cached.graph;
1914
2254
  }
1915
- const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
2255
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
1916
2256
  const graph = JSON.parse(content);
1917
2257
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1918
2258
  return graph;
1919
2259
  }
2260
+ function readGraph(rootDir, layer) {
2261
+ const rawFilePath = graphFilePath(rootDir, layer);
2262
+ if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
2263
+ const rawMtime = getMtimeMs(rawFilePath);
2264
+ const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2265
+ const cacheKey = `${rootDir}:${layer}`;
2266
+ const cached = taggedCache.get(cacheKey);
2267
+ if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
2268
+ return cached.graph;
2269
+ }
2270
+ const raw = readGraphRaw(rootDir, layer);
2271
+ if (!raw) return null;
2272
+ const tagged = applyTags(raw, layer, rootDir);
2273
+ taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
2274
+ return tagged;
2275
+ }
1920
2276
  function readAllGraphs(rootDir) {
1921
2277
  const result = {};
1922
2278
  for (const layer of LAYERS) {
@@ -1927,32 +2283,52 @@ function readAllGraphs(rootDir) {
1927
2283
  }
1928
2284
  function generateGraph(rootDir, layer) {
1929
2285
  const dir = graphsDir(rootDir);
1930
- (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
2286
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
1931
2287
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1932
2288
  for (const result of results) {
1933
2289
  const filePath = graphFilePath(rootDir, result.layer);
1934
- (0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2290
+ (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1935
2291
  invalidateCache(filePath);
2292
+ invalidateTaggedCache(rootDir, result.layer);
1936
2293
  }
1937
2294
  return results;
1938
2295
  }
1939
2296
 
1940
2297
  // src/server/lockfile.ts
1941
2298
  var import_node_child_process = require("node:child_process");
1942
- var import_node_fs10 = require("node:fs");
2299
+ var import_node_fs12 = require("node:fs");
1943
2300
  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;
2301
+ var import_node_path14 = require("node:path");
2302
+ function lockDir(projectRoot) {
2303
+ if (projectRoot) {
2304
+ return (0, import_node_path14.join)(projectRoot, ".launchsecure");
2305
+ }
2306
+ return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
2307
+ }
2308
+ function lockPath(projectRoot) {
2309
+ return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
2310
+ }
2311
+ var _activeProjectRoot;
2312
+ function readLock(projectRoot) {
2313
+ const root = projectRoot ?? _activeProjectRoot;
2314
+ const p = lockPath(root);
2315
+ if (!(0, import_node_fs12.existsSync)(p)) {
2316
+ if (root) {
2317
+ const globalP = lockPath();
2318
+ if ((0, import_node_fs12.existsSync)(globalP)) {
2319
+ try {
2320
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
2321
+ if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
2322
+ return data;
2323
+ }
2324
+ } catch {
2325
+ }
2326
+ }
2327
+ }
2328
+ return null;
2329
+ }
1954
2330
  try {
1955
- const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
2331
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
1956
2332
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
1957
2333
  return data;
1958
2334
  } catch {
@@ -1981,34 +2357,41 @@ function getListenerPid(port) {
1981
2357
  return null;
1982
2358
  }
1983
2359
  }
1984
- function getLiveLock() {
1985
- const lock = readLock();
2360
+ function getLiveLock(projectRoot) {
2361
+ const root = projectRoot ?? _activeProjectRoot;
2362
+ const lock = readLock(root);
1986
2363
  if (!lock) return null;
1987
2364
  const listenerPid = getListenerPid(lock.port);
1988
2365
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
1989
2366
  if (!live) {
1990
2367
  try {
1991
- (0, import_node_fs10.unlinkSync)(lockPath());
2368
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
1992
2369
  } catch {
1993
2370
  }
1994
2371
  return null;
1995
2372
  }
1996
2373
  return lock;
1997
2374
  }
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");
2375
+ function writeLock(data, projectRoot) {
2376
+ const root = projectRoot ?? _activeProjectRoot;
2377
+ (0, import_node_fs12.mkdirSync)(lockDir(root), { recursive: true });
2378
+ (0, import_node_fs12.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
2379
+ if (root) _activeProjectRoot = root;
2001
2380
  }
2002
- function clearLock() {
2381
+ function clearLock(projectRoot) {
2382
+ const root = projectRoot ?? _activeProjectRoot;
2003
2383
  try {
2004
- (0, import_node_fs10.unlinkSync)(lockPath());
2384
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
2005
2385
  } catch {
2006
2386
  }
2007
2387
  }
2008
2388
 
2009
2389
  // src/server/chart-serve.ts
2010
- var DEFAULT_PORT = 52819;
2011
- var MAX_PORT_SCAN = 20;
2390
+ init_config();
2391
+ var MAX_PORT_SCAN = 3;
2392
+ function randomPort() {
2393
+ return 49152 + Math.floor(Math.random() * (65535 - 49152));
2394
+ }
2012
2395
  var MIME_TYPES = {
2013
2396
  ".html": "text/html; charset=utf-8",
2014
2397
  ".js": "application/javascript; charset=utf-8",
@@ -2023,16 +2406,16 @@ var MIME_TYPES = {
2023
2406
  function findProjectRoot(startDir) {
2024
2407
  let dir = startDir;
2025
2408
  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);
2409
+ const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2410
+ 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;
2411
+ const parent = import_node_path15.default.dirname(dir);
2029
2412
  if (parent === dir) break;
2030
2413
  dir = parent;
2031
2414
  }
2032
2415
  dir = startDir;
2033
2416
  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);
2417
+ if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2418
+ const parent = import_node_path15.default.dirname(dir);
2036
2419
  if (parent === dir) break;
2037
2420
  dir = parent;
2038
2421
  }
@@ -2084,16 +2467,16 @@ function buildMergedGraph(projectRoot) {
2084
2467
  };
2085
2468
  }
2086
2469
  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();
2470
+ if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2471
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
2089
2472
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2090
2473
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2091
- import_node_fs11.default.createReadStream(filePath).pipe(res);
2474
+ import_node_fs13.default.createReadStream(filePath).pipe(res);
2092
2475
  return true;
2093
2476
  }
2094
2477
  function serveIndex(res, clientDir) {
2095
- const indexPath = import_node_path12.default.join(clientDir, "index.html");
2096
- if (!import_node_fs11.default.existsSync(indexPath)) {
2478
+ const indexPath = import_node_path15.default.join(clientDir, "index.html");
2479
+ if (!import_node_fs13.default.existsSync(indexPath)) {
2097
2480
  res.writeHead(500, { "Content-Type": "text/plain" });
2098
2481
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2099
2482
  return;
@@ -2101,14 +2484,14 @@ function serveIndex(res, clientDir) {
2101
2484
  serveStatic(res, indexPath);
2102
2485
  }
2103
2486
  function tryListen(server, port) {
2104
- return new Promise((resolve2, reject) => {
2487
+ return new Promise((resolve3, reject) => {
2105
2488
  const onError = (err) => {
2106
2489
  server.off("listening", onListening);
2107
2490
  reject(err);
2108
2491
  };
2109
2492
  const onListening = () => {
2110
2493
  server.off("error", onError);
2111
- resolve2(port);
2494
+ resolve3(port);
2112
2495
  };
2113
2496
  server.once("error", onError);
2114
2497
  server.once("listening", onListening);
@@ -2135,7 +2518,7 @@ async function bindWithFallback(server, startPort) {
2135
2518
  async function startChartServer(opts = {}) {
2136
2519
  const cwd = opts.cwd ?? process.cwd();
2137
2520
  const projectRoot = findProjectRoot(cwd);
2138
- const existing = getLiveLock();
2521
+ const existing = getLiveLock(projectRoot);
2139
2522
  if (existing) {
2140
2523
  if (!opts.quiet) {
2141
2524
  process.stderr.write(
@@ -2145,7 +2528,7 @@ async function startChartServer(opts = {}) {
2145
2528
  }
2146
2529
  return { port: existing.port, url: existing.url };
2147
2530
  }
2148
- const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
2531
+ const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
2149
2532
  const server = import_node_http.default.createServer((req, res) => {
2150
2533
  try {
2151
2534
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -2183,6 +2566,26 @@ async function startChartServer(opts = {}) {
2183
2566
  }
2184
2567
  return;
2185
2568
  }
2569
+ if (req.method === "GET" && url2.pathname === "/api/file-content") {
2570
+ const relPath = url2.searchParams.get("path");
2571
+ if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
2572
+ res.writeHead(400, { "Content-Type": "application/json" });
2573
+ res.end(JSON.stringify({ error: "Invalid path" }));
2574
+ return;
2575
+ }
2576
+ const filePath = import_node_path15.default.join(projectRoot, relPath);
2577
+ if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
2578
+ res.writeHead(404, { "Content-Type": "application/json" });
2579
+ res.end(JSON.stringify({ error: "File not found" }));
2580
+ return;
2581
+ }
2582
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
2583
+ const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2584
+ const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
2585
+ res.writeHead(200, { "Content-Type": "application/json" });
2586
+ res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2587
+ return;
2588
+ }
2186
2589
  if (req.method === "GET" && url2.pathname === "/api/health") {
2187
2590
  res.writeHead(200, { "Content-Type": "application/json" });
2188
2591
  res.end(JSON.stringify({ ok: true, projectRoot }));
@@ -2212,8 +2615,94 @@ async function startChartServer(opts = {}) {
2212
2615
  req.on("end", () => {
2213
2616
  try {
2214
2617
  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");
2618
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2619
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2620
+ res.writeHead(200, { "Content-Type": "application/json" });
2621
+ res.end(JSON.stringify({ ok: true }));
2622
+ } catch (err) {
2623
+ res.writeHead(400, { "Content-Type": "application/json" });
2624
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2625
+ }
2626
+ });
2627
+ return;
2628
+ }
2629
+ if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2630
+ const config = loadConfig(projectRoot);
2631
+ const builtinTaggers = [
2632
+ { id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
2633
+ { id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
2634
+ ];
2635
+ const disabled = config.taggers?.disabled ?? [];
2636
+ const customTaggers = config.taggers?.custom ?? [];
2637
+ const moduleRules = config.taggers?.module?.rules ?? [];
2638
+ res.writeHead(200, { "Content-Type": "application/json" });
2639
+ res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
2640
+ return;
2641
+ }
2642
+ if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
2643
+ let body = "";
2644
+ req.on("data", (chunk) => {
2645
+ body += chunk.toString();
2646
+ });
2647
+ req.on("end", () => {
2648
+ try {
2649
+ const taggerConfig = JSON.parse(body);
2650
+ const config = loadConfig(projectRoot);
2651
+ config.taggers = taggerConfig;
2652
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2653
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2654
+ res.writeHead(200, { "Content-Type": "application/json" });
2655
+ res.end(JSON.stringify({ ok: true }));
2656
+ } catch (err) {
2657
+ res.writeHead(400, { "Content-Type": "application/json" });
2658
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2659
+ }
2660
+ });
2661
+ return;
2662
+ }
2663
+ if (req.method === "GET" && url2.pathname === "/api/tags") {
2664
+ const store = readTagStore(projectRoot);
2665
+ res.writeHead(200, { "Content-Type": "application/json" });
2666
+ res.end(JSON.stringify(store));
2667
+ return;
2668
+ }
2669
+ if (req.method === "POST" && url2.pathname === "/api/tags") {
2670
+ let body = "";
2671
+ req.on("data", (chunk) => {
2672
+ body += chunk.toString();
2673
+ });
2674
+ req.on("end", () => {
2675
+ try {
2676
+ const { nodeId, key, value } = JSON.parse(body);
2677
+ if (!nodeId || !key || !value) {
2678
+ res.writeHead(400, { "Content-Type": "application/json" });
2679
+ res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
2680
+ return;
2681
+ }
2682
+ setTag(projectRoot, nodeId, key, value);
2683
+ res.writeHead(200, { "Content-Type": "application/json" });
2684
+ res.end(JSON.stringify({ ok: true }));
2685
+ } catch (err) {
2686
+ res.writeHead(400, { "Content-Type": "application/json" });
2687
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2688
+ }
2689
+ });
2690
+ return;
2691
+ }
2692
+ if (req.method === "DELETE" && url2.pathname === "/api/tags") {
2693
+ let body = "";
2694
+ req.on("data", (chunk) => {
2695
+ body += chunk.toString();
2696
+ });
2697
+ req.on("end", () => {
2698
+ try {
2699
+ const { nodeId, key } = JSON.parse(body);
2700
+ if (!nodeId || !key) {
2701
+ res.writeHead(400, { "Content-Type": "application/json" });
2702
+ res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
2703
+ return;
2704
+ }
2705
+ removeTag(projectRoot, nodeId, key);
2217
2706
  res.writeHead(200, { "Content-Type": "application/json" });
2218
2707
  res.end(JSON.stringify({ ok: true }));
2219
2708
  } catch (err) {
@@ -2224,7 +2713,7 @@ async function startChartServer(opts = {}) {
2224
2713
  return;
2225
2714
  }
2226
2715
  if (url2.pathname !== "/") {
2227
- const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
2716
+ const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
2228
2717
  if (serveStatic(res, staticPath)) return;
2229
2718
  }
2230
2719
  serveIndex(res, clientDir);
@@ -2233,7 +2722,8 @@ async function startChartServer(opts = {}) {
2233
2722
  res.end(JSON.stringify({ error: String(err) }));
2234
2723
  }
2235
2724
  });
2236
- const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
2725
+ const startPort = opts.port ?? randomPort();
2726
+ const port = await bindWithFallback(server, startPort);
2237
2727
  const url = `http://localhost:${port}`;
2238
2728
  writeLock({
2239
2729
  pid: process.pid,
@@ -2241,9 +2731,9 @@ async function startChartServer(opts = {}) {
2241
2731
  cwd,
2242
2732
  url,
2243
2733
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2244
- });
2734
+ }, projectRoot);
2245
2735
  const cleanup = () => {
2246
- clearLock();
2736
+ clearLock(projectRoot);
2247
2737
  server.close();
2248
2738
  };
2249
2739
  process.once("SIGINT", () => {