uilint 0.2.123 → 0.2.124

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/index.js CHANGED
@@ -1407,9 +1407,9 @@ async function update(options) {
1407
1407
  }
1408
1408
 
1409
1409
  // src/commands/serve.ts
1410
- import { existsSync as existsSync5, statSync as statSync3, readdirSync, readFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1411
- import { createRequire } from "module";
1412
- import { dirname as dirname5, resolve as resolve5, relative, join as join3, parse as parse2 } from "path";
1410
+ import { existsSync as existsSync6, statSync as statSync3, readdirSync, readFileSync as readFileSync2, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1411
+ import { createRequire as createRequire3 } from "module";
1412
+ import { dirname as dirname6, resolve as resolve6, relative as relative2, join as join4, parse as parse2 } from "path";
1413
1413
  import { WebSocketServer, WebSocket } from "ws";
1414
1414
  import { watch } from "chokidar";
1415
1415
  import {
@@ -1792,7 +1792,563 @@ function completeBackgroundTask(id, successMessage, error) {
1792
1792
  }
1793
1793
 
1794
1794
  // src/commands/serve.ts
1795
- import { ruleRegistry } from "uilint-eslint";
1795
+ import { ruleRegistry } from "uilint-eslint";
1796
+
1797
+ // src/utils/eslint-utils.ts
1798
+ import { existsSync as existsSync5, readFileSync } from "fs";
1799
+ import { createRequire as createRequire2 } from "module";
1800
+ import { dirname as dirname5, resolve as resolve5, relative, join as join3 } from "path";
1801
+
1802
+ // src/scope-extractor.ts
1803
+ import { createRequire } from "module";
1804
+ function isComponentName(name) {
1805
+ if (!name) return false;
1806
+ return /^[A-Z]/.test(name);
1807
+ }
1808
+ function isHookName(name) {
1809
+ if (!name) return false;
1810
+ return /^use[A-Z]/.test(name);
1811
+ }
1812
+ function containsJsxReturn(node) {
1813
+ if (!node) return false;
1814
+ if (node.type === "JSXElement" || node.type === "JSXFragment") {
1815
+ return true;
1816
+ }
1817
+ if (node.type === "BlockStatement" && node.body) {
1818
+ for (const stmt of node.body) {
1819
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
1820
+ if (stmt.argument.type === "JSXElement" || stmt.argument.type === "JSXFragment") {
1821
+ return true;
1822
+ }
1823
+ if (stmt.argument.type === "ConditionalExpression") {
1824
+ if (stmt.argument.consequent?.type === "JSXElement" || stmt.argument.consequent?.type === "JSXFragment" || stmt.argument.alternate?.type === "JSXElement" || stmt.argument.alternate?.type === "JSXFragment") {
1825
+ return true;
1826
+ }
1827
+ }
1828
+ }
1829
+ }
1830
+ }
1831
+ return false;
1832
+ }
1833
+ function determineScopeType(name, isArrow, isMethod, returnsJsx, isExportDefault = false) {
1834
+ if (isMethod) return "method";
1835
+ if (isHookName(name)) return "hook";
1836
+ if (isExportDefault && returnsJsx) return "component";
1837
+ if (isComponentName(name) && returnsJsx) return "component";
1838
+ if (isArrow) return "arrow-function";
1839
+ return "function";
1840
+ }
1841
+ function getIdentifierName(node) {
1842
+ if (!node) return null;
1843
+ if (node.type === "Identifier") return node.name;
1844
+ if (node.type === "PrivateIdentifier") return `#${node.name}`;
1845
+ return null;
1846
+ }
1847
+ function getJsxElementType(node) {
1848
+ if (!node || node.type !== "JSXElement") return null;
1849
+ const opening = node.openingElement;
1850
+ if (!opening || !opening.name) return null;
1851
+ const name = opening.name;
1852
+ if (name.type === "JSXIdentifier") return name.name;
1853
+ if (name.type === "JSXMemberExpression") {
1854
+ const parts = [];
1855
+ let current = name;
1856
+ while (current) {
1857
+ if (current.type === "JSXMemberExpression") {
1858
+ parts.unshift(current.property.name);
1859
+ current = current.object;
1860
+ } else if (current.type === "JSXIdentifier") {
1861
+ parts.unshift(current.name);
1862
+ break;
1863
+ } else {
1864
+ break;
1865
+ }
1866
+ }
1867
+ return parts.join(".");
1868
+ }
1869
+ return null;
1870
+ }
1871
+ function lineColToOffset(code, line, column) {
1872
+ const lines = code.split("\n");
1873
+ let offset = 0;
1874
+ for (let i = 0; i < line - 1 && i < lines.length; i++) {
1875
+ offset += lines[i].length + 1;
1876
+ }
1877
+ return offset + column;
1878
+ }
1879
+ function collectScopeBoundaries(ast) {
1880
+ const boundaries = [];
1881
+ function walk(node, parentName = null) {
1882
+ if (!node || typeof node !== "object") return;
1883
+ const range = node.range;
1884
+ const loc = node.loc;
1885
+ if (node.type === "FunctionDeclaration") {
1886
+ const name = getIdentifierName(node.id);
1887
+ const returnsJsx = containsJsxReturn(node.body);
1888
+ const scopeType = determineScopeType(name, false, false, returnsJsx);
1889
+ if (range && loc) {
1890
+ boundaries.push({
1891
+ name,
1892
+ type: scopeType,
1893
+ start: range[0],
1894
+ end: range[1],
1895
+ line: loc.start.line,
1896
+ column: loc.start.column
1897
+ });
1898
+ }
1899
+ walk(node.body, name);
1900
+ return;
1901
+ }
1902
+ if (node.type === "ArrowFunctionExpression") {
1903
+ let name = null;
1904
+ const returnsJsx = containsJsxReturn(node.body);
1905
+ if (range && loc) {
1906
+ const scopeType = determineScopeType(name, true, false, returnsJsx);
1907
+ boundaries.push({
1908
+ name,
1909
+ type: scopeType,
1910
+ start: range[0],
1911
+ end: range[1],
1912
+ line: loc.start.line,
1913
+ column: loc.start.column
1914
+ });
1915
+ }
1916
+ walk(node.body, name || parentName);
1917
+ return;
1918
+ }
1919
+ if (node.type === "FunctionExpression") {
1920
+ const name = getIdentifierName(node.id);
1921
+ const returnsJsx = containsJsxReturn(node.body);
1922
+ const scopeType = determineScopeType(name, false, false, returnsJsx);
1923
+ if (range && loc) {
1924
+ boundaries.push({
1925
+ name,
1926
+ type: scopeType,
1927
+ start: range[0],
1928
+ end: range[1],
1929
+ line: loc.start.line,
1930
+ column: loc.start.column
1931
+ });
1932
+ }
1933
+ walk(node.body, name || parentName);
1934
+ return;
1935
+ }
1936
+ if (node.type === "VariableDeclarator" && node.init) {
1937
+ const varName = getIdentifierName(node.id);
1938
+ const init = node.init;
1939
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
1940
+ const funcOwnName = getIdentifierName(init.id);
1941
+ const effectiveName = funcOwnName || varName;
1942
+ const returnsJsx = containsJsxReturn(init.body);
1943
+ const isArrow = init.type === "ArrowFunctionExpression";
1944
+ const scopeType = determineScopeType(effectiveName, isArrow, false, returnsJsx);
1945
+ const initRange = init.range;
1946
+ const initLoc = init.loc;
1947
+ if (initRange && initLoc) {
1948
+ const existing = boundaries.find(
1949
+ (b) => b.start === initRange[0] && b.end === initRange[1]
1950
+ );
1951
+ if (existing) {
1952
+ existing.name = effectiveName;
1953
+ existing.type = scopeType;
1954
+ } else {
1955
+ boundaries.push({
1956
+ name: effectiveName,
1957
+ type: scopeType,
1958
+ start: initRange[0],
1959
+ end: initRange[1],
1960
+ line: initLoc.start.line,
1961
+ column: initLoc.start.column
1962
+ });
1963
+ }
1964
+ }
1965
+ walk(init.body, effectiveName);
1966
+ return;
1967
+ }
1968
+ }
1969
+ if (node.type === "MethodDefinition" || node.type === "Property") {
1970
+ const isMethod = node.type === "MethodDefinition" || node.type === "Property" && node.method;
1971
+ if (isMethod && node.value) {
1972
+ const name = getIdentifierName(node.key);
1973
+ const returnsJsx = containsJsxReturn(node.value.body);
1974
+ const scopeType = determineScopeType(name, false, true, returnsJsx);
1975
+ if (range && loc) {
1976
+ boundaries.push({
1977
+ name,
1978
+ type: scopeType,
1979
+ start: range[0],
1980
+ end: range[1],
1981
+ line: loc.start.line,
1982
+ column: loc.start.column
1983
+ });
1984
+ }
1985
+ walk(node.value.body, name);
1986
+ return;
1987
+ }
1988
+ }
1989
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
1990
+ const name = getIdentifierName(node.id);
1991
+ if (range && loc) {
1992
+ boundaries.push({
1993
+ name,
1994
+ type: "class",
1995
+ start: range[0],
1996
+ end: range[1],
1997
+ line: loc.start.line,
1998
+ column: loc.start.column
1999
+ });
2000
+ }
2001
+ if (node.body) {
2002
+ walk(node.body, name);
2003
+ }
2004
+ return;
2005
+ }
2006
+ if (node.type === "ExportDefaultDeclaration" && node.declaration) {
2007
+ const decl = node.declaration;
2008
+ if (decl.type === "FunctionDeclaration") {
2009
+ const name = getIdentifierName(decl.id);
2010
+ const returnsJsx = containsJsxReturn(decl.body);
2011
+ const scopeType = determineScopeType(name, false, false, returnsJsx, !name);
2012
+ const declRange = decl.range;
2013
+ const declLoc = decl.loc;
2014
+ if (declRange && declLoc) {
2015
+ boundaries.push({
2016
+ name: name || "default",
2017
+ type: scopeType,
2018
+ start: declRange[0],
2019
+ end: declRange[1],
2020
+ line: declLoc.start.line,
2021
+ column: declLoc.start.column
2022
+ });
2023
+ }
2024
+ walk(decl.body, name || "default");
2025
+ return;
2026
+ }
2027
+ if (decl.type === "ClassDeclaration") {
2028
+ walk(decl, parentName);
2029
+ return;
2030
+ }
2031
+ if (decl.type === "FunctionExpression" || decl.type === "ArrowFunctionExpression") {
2032
+ const funcOwnName = getIdentifierName(decl.id);
2033
+ const name = funcOwnName || "default";
2034
+ const returnsJsx = containsJsxReturn(decl.body);
2035
+ const isArrow = decl.type === "ArrowFunctionExpression";
2036
+ const scopeType = determineScopeType(name, isArrow, false, returnsJsx, !funcOwnName);
2037
+ const declRange = decl.range;
2038
+ const declLoc = decl.loc;
2039
+ if (declRange && declLoc) {
2040
+ boundaries.push({
2041
+ name,
2042
+ type: scopeType,
2043
+ start: declRange[0],
2044
+ end: declRange[1],
2045
+ line: declLoc.start.line,
2046
+ column: declLoc.start.column
2047
+ });
2048
+ }
2049
+ walk(decl.body, name);
2050
+ return;
2051
+ }
2052
+ }
2053
+ for (const key of Object.keys(node)) {
2054
+ if (key === "loc" || key === "range" || key === "parent") continue;
2055
+ const child = node[key];
2056
+ if (Array.isArray(child)) {
2057
+ for (const item of child) {
2058
+ walk(item, parentName);
2059
+ }
2060
+ } else if (child && typeof child === "object" && child.type) {
2061
+ walk(child, parentName);
2062
+ }
2063
+ }
2064
+ }
2065
+ walk(ast);
2066
+ return boundaries;
2067
+ }
2068
+ function findContainingJsxElement(ast, offset) {
2069
+ const result = {
2070
+ innermost: null
2071
+ };
2072
+ function walk(node) {
2073
+ if (!node || typeof node !== "object") return;
2074
+ if (node.type === "JSXElement") {
2075
+ const range = node.range;
2076
+ if (range && range[0] <= offset && offset < range[1]) {
2077
+ const size = range[1] - range[0];
2078
+ if (!result.innermost || size < result.innermost.size) {
2079
+ const elementType = getJsxElementType(node);
2080
+ if (elementType) {
2081
+ result.innermost = { type: elementType, size };
2082
+ }
2083
+ }
2084
+ }
2085
+ }
2086
+ for (const key of Object.keys(node)) {
2087
+ if (key === "loc" || key === "range" || key === "parent") continue;
2088
+ const child = node[key];
2089
+ if (Array.isArray(child)) {
2090
+ for (const item of child) walk(item);
2091
+ } else if (child && typeof child === "object" && child.type) {
2092
+ walk(child);
2093
+ }
2094
+ }
2095
+ }
2096
+ walk(ast);
2097
+ return result.innermost?.type ?? null;
2098
+ }
2099
+ function findEnclosingScopeBatch(source, positions, options) {
2100
+ const localRequire2 = createRequire(import.meta.url);
2101
+ const { parse: parse3 } = localRequire2("@typescript-eslint/typescript-estree");
2102
+ let ast;
2103
+ try {
2104
+ ast = parse3(source, {
2105
+ loc: true,
2106
+ range: true,
2107
+ jsx: true,
2108
+ comment: false,
2109
+ errorOnUnknownASTType: false
2110
+ });
2111
+ } catch {
2112
+ return positions.map(() => null);
2113
+ }
2114
+ const boundaries = collectScopeBoundaries(ast);
2115
+ return positions.map(({ line, column }) => {
2116
+ const offset = lineColToOffset(source, line, column);
2117
+ const containingScopes = boundaries.filter((b) => b.start <= offset && offset < b.end).sort((a, b) => a.end - a.start - (b.end - b.start));
2118
+ const jsxElementType = findContainingJsxElement(ast, offset);
2119
+ if (containingScopes.length === 0) {
2120
+ return {
2121
+ enclosingScope: null,
2122
+ scopeType: "module",
2123
+ jsxElementType: jsxElementType ?? void 0
2124
+ };
2125
+ }
2126
+ const innermost = containingScopes[0];
2127
+ const parent = containingScopes.length > 1 ? containingScopes[1] : null;
2128
+ const scopeName = innermost.name || "anonymous";
2129
+ return {
2130
+ enclosingScope: scopeName,
2131
+ scopeType: innermost.type,
2132
+ parentScope: parent?.name ?? void 0,
2133
+ jsxElementType: jsxElementType ?? void 0
2134
+ };
2135
+ });
2136
+ }
2137
+
2138
+ // src/utils/eslint-utils.ts
2139
+ var ESLINT_CONFIG_FILES = [
2140
+ // Flat config (ESLint v9+)
2141
+ "eslint.config.js",
2142
+ "eslint.config.mjs",
2143
+ "eslint.config.cjs",
2144
+ "eslint.config.ts",
2145
+ // Legacy config
2146
+ ".eslintrc",
2147
+ ".eslintrc.js",
2148
+ ".eslintrc.cjs",
2149
+ ".eslintrc.json",
2150
+ ".eslintrc.yaml",
2151
+ ".eslintrc.yml"
2152
+ ];
2153
+ function buildLineStarts(code) {
2154
+ const starts = [0];
2155
+ for (let i = 0; i < code.length; i++) {
2156
+ if (code.charCodeAt(i) === 10) starts.push(i + 1);
2157
+ }
2158
+ return starts;
2159
+ }
2160
+ function offsetFromLineCol(lineStarts, line1, col0, codeLength) {
2161
+ const lineIndex = Math.max(0, Math.min(lineStarts.length - 1, line1 - 1));
2162
+ const base = lineStarts[lineIndex] ?? 0;
2163
+ return Math.max(0, Math.min(codeLength, base + Math.max(0, col0)));
2164
+ }
2165
+ function buildJsxElementSpans(code, dataLocFile) {
2166
+ const localRequire2 = createRequire2(import.meta.url);
2167
+ const { parse: parse3 } = localRequire2("@typescript-eslint/typescript-estree");
2168
+ const ast = parse3(code, {
2169
+ loc: true,
2170
+ range: true,
2171
+ jsx: true,
2172
+ comment: false,
2173
+ errorOnUnknownASTType: false
2174
+ });
2175
+ const spans = [];
2176
+ function walk(node) {
2177
+ if (!node || typeof node !== "object") return;
2178
+ if (node.type === "JSXElement") {
2179
+ const range = node.range;
2180
+ const opening = node.openingElement;
2181
+ const loc = opening?.loc?.start;
2182
+ if (range && typeof range[0] === "number" && typeof range[1] === "number" && loc && typeof loc.line === "number" && typeof loc.column === "number") {
2183
+ const dataLoc = `${dataLocFile}:${loc.line}:${loc.column}`;
2184
+ spans.push({ start: range[0], end: range[1], dataLoc });
2185
+ }
2186
+ }
2187
+ for (const key of Object.keys(node)) {
2188
+ const child = node[key];
2189
+ if (Array.isArray(child)) {
2190
+ for (const item of child) walk(item);
2191
+ } else if (child && typeof child === "object") {
2192
+ walk(child);
2193
+ }
2194
+ }
2195
+ }
2196
+ walk(ast);
2197
+ spans.sort((a, b) => a.end - a.start - (b.end - b.start));
2198
+ return spans;
2199
+ }
2200
+ function mapMessageToDataLoc(params) {
2201
+ const col0 = typeof params.messageCol1 === "number" ? Math.max(0, params.messageCol1 - 1) : 0;
2202
+ const offset = offsetFromLineCol(
2203
+ params.lineStarts,
2204
+ params.messageLine1,
2205
+ col0,
2206
+ params.codeLength
2207
+ );
2208
+ for (const s of params.spans) {
2209
+ if (s.start <= offset && offset < s.end) return s.dataLoc;
2210
+ }
2211
+ return void 0;
2212
+ }
2213
+ function normalizePathSlashes(p) {
2214
+ return p.replace(/\\/g, "/");
2215
+ }
2216
+ function normalizeDataLocFilePath(absoluteFilePath, projectCwd) {
2217
+ const abs = normalizePathSlashes(resolve5(absoluteFilePath));
2218
+ const cwd = normalizePathSlashes(resolve5(projectCwd));
2219
+ if (abs === cwd || abs.startsWith(cwd + "/")) {
2220
+ return normalizePathSlashes(relative(cwd, abs));
2221
+ }
2222
+ return abs;
2223
+ }
2224
+ function findESLintCwd(startDir) {
2225
+ let dir = startDir;
2226
+ for (let i = 0; i < 30; i++) {
2227
+ for (const cfg of ESLINT_CONFIG_FILES) {
2228
+ if (existsSync5(join3(dir, cfg))) return dir;
2229
+ }
2230
+ if (existsSync5(join3(dir, "package.json"))) return dir;
2231
+ const parent = dirname5(dir);
2232
+ if (parent === dir) break;
2233
+ dir = parent;
2234
+ }
2235
+ return startDir;
2236
+ }
2237
+ var eslintInstances = /* @__PURE__ */ new Map();
2238
+ async function getESLintForProject(projectCwd) {
2239
+ const cached = eslintInstances.get(projectCwd);
2240
+ if (cached) return cached;
2241
+ try {
2242
+ const req = createRequire2(join3(projectCwd, "package.json"));
2243
+ const mod = req("eslint");
2244
+ const ESLintCtor = mod?.ESLint ?? mod?.default?.ESLint ?? mod?.default ?? mod;
2245
+ if (!ESLintCtor) return null;
2246
+ const eslint = new ESLintCtor({ cwd: projectCwd });
2247
+ eslintInstances.set(projectCwd, eslint);
2248
+ return eslint;
2249
+ } catch {
2250
+ return null;
2251
+ }
2252
+ }
2253
+ async function lintFileWithDataLoc(absolutePath, projectCwd, onProgress) {
2254
+ const progress = onProgress ?? (() => {
2255
+ });
2256
+ if (!existsSync5(absolutePath)) {
2257
+ progress(`File not found: ${absolutePath}`);
2258
+ return [];
2259
+ }
2260
+ progress(`Resolving ESLint project... ${projectCwd}`);
2261
+ const eslint = await getESLintForProject(projectCwd);
2262
+ if (!eslint) {
2263
+ progress("ESLint not available");
2264
+ return [];
2265
+ }
2266
+ try {
2267
+ progress("Running ESLint...");
2268
+ const results = await eslint.lintFiles([absolutePath]);
2269
+ const messages = Array.isArray(results) && results.length > 0 ? results[0].messages || [] : [];
2270
+ const dataLocFile = normalizeDataLocFilePath(absolutePath, projectCwd);
2271
+ let spans = [];
2272
+ let lineStarts = [];
2273
+ let codeLength = 0;
2274
+ let fileCode = null;
2275
+ try {
2276
+ progress("Building JSX map...");
2277
+ fileCode = readFileSync(absolutePath, "utf-8");
2278
+ codeLength = fileCode.length;
2279
+ lineStarts = buildLineStarts(fileCode);
2280
+ spans = buildJsxElementSpans(fileCode, dataLocFile);
2281
+ progress(`JSX map: ${spans.length} element(s)`);
2282
+ } catch (e) {
2283
+ progress("JSX map failed (falling back to unmapped issues)");
2284
+ spans = [];
2285
+ lineStarts = [];
2286
+ codeLength = 0;
2287
+ fileCode = null;
2288
+ }
2289
+ let issues = messages.filter((m) => typeof m?.message === "string").map((m) => {
2290
+ const line = typeof m.line === "number" ? m.line : 1;
2291
+ const column = typeof m.column === "number" ? m.column : void 0;
2292
+ const mappedDataLoc = spans.length > 0 && lineStarts.length > 0 && codeLength > 0 ? mapMessageToDataLoc({
2293
+ spans,
2294
+ lineStarts,
2295
+ codeLength,
2296
+ messageLine1: line,
2297
+ messageCol1: column
2298
+ }) : void 0;
2299
+ return {
2300
+ line,
2301
+ column,
2302
+ message: m.message,
2303
+ ruleId: typeof m.ruleId === "string" ? m.ruleId : void 0,
2304
+ dataLoc: mappedDataLoc
2305
+ };
2306
+ });
2307
+ const mappedCount = issues.filter((i) => Boolean(i.dataLoc)).length;
2308
+ if (issues.length > 0) {
2309
+ progress(`Mapped ${mappedCount}/${issues.length} issue(s) to JSX elements`);
2310
+ }
2311
+ if (fileCode && issues.length > 0) {
2312
+ progress("Extracting scope info...");
2313
+ issues = enrichIssuesWithScopeInfo(issues, fileCode);
2314
+ const scopeCount = issues.filter((i) => Boolean(i.scopeInfo)).length;
2315
+ progress(`Enriched ${scopeCount}/${issues.length} issue(s) with scope info`);
2316
+ }
2317
+ return issues;
2318
+ } catch (error) {
2319
+ progress(`ESLint failed: ${error instanceof Error ? error.message : String(error)}`);
2320
+ return [];
2321
+ }
2322
+ }
2323
+ function extractSourceSnippet(code, centerLine, contextLines = 3) {
2324
+ const allLines = code.split("\n");
2325
+ const startLine = Math.max(1, centerLine - contextLines);
2326
+ const endLine = Math.min(allLines.length, centerLine + contextLines);
2327
+ return {
2328
+ lines: allLines.slice(startLine - 1, endLine),
2329
+ startLine,
2330
+ endLine
2331
+ };
2332
+ }
2333
+ function enrichIssuesWithScopeInfo(issues, code) {
2334
+ if (issues.length === 0) {
2335
+ return issues;
2336
+ }
2337
+ const positions = issues.map((issue) => ({
2338
+ line: issue.line,
2339
+ column: issue.column ?? 0
2340
+ }));
2341
+ const scopeInfos = findEnclosingScopeBatch(code, positions);
2342
+ return issues.map((issue, index) => {
2343
+ const scopeInfo = scopeInfos[index];
2344
+ if (scopeInfo) {
2345
+ return { ...issue, scopeInfo };
2346
+ }
2347
+ return issue;
2348
+ });
2349
+ }
2350
+
2351
+ // src/commands/serve.ts
1796
2352
  function pickAppRoot(params) {
1797
2353
  const { cwd, workspaceRoot } = params;
1798
2354
  if (detectNextAppRouter(cwd)) return cwd;
@@ -1806,10 +2362,10 @@ function pickAppRoot(params) {
1806
2362
  return matches[0].projectPath;
1807
2363
  }
1808
2364
  function detectPostToolUseHook(projectRoot) {
1809
- const claudeSettingsPath = join3(projectRoot, ".claude", "settings.json");
1810
- if (existsSync5(claudeSettingsPath)) {
2365
+ const claudeSettingsPath = join4(projectRoot, ".claude", "settings.json");
2366
+ if (existsSync6(claudeSettingsPath)) {
1811
2367
  try {
1812
- const content = readFileSync(claudeSettingsPath, "utf-8");
2368
+ const content = readFileSync2(claudeSettingsPath, "utf-8");
1813
2369
  const settings = JSON.parse(content);
1814
2370
  const hooks = settings.hooks?.PostToolUse;
1815
2371
  if (Array.isArray(hooks)) {
@@ -1823,10 +2379,10 @@ function detectPostToolUseHook(projectRoot) {
1823
2379
  } catch {
1824
2380
  }
1825
2381
  }
1826
- const cursorHooksPath = join3(projectRoot, ".cursor", "hooks.json");
1827
- if (existsSync5(cursorHooksPath)) {
2382
+ const cursorHooksPath = join4(projectRoot, ".cursor", "hooks.json");
2383
+ if (existsSync6(cursorHooksPath)) {
1828
2384
  try {
1829
- const content = readFileSync(cursorHooksPath, "utf-8");
2385
+ const content = readFileSync2(cursorHooksPath, "utf-8");
1830
2386
  const hooks = JSON.parse(content);
1831
2387
  if (hooks.hooks?.afterFileEdit?.length > 0) {
1832
2388
  return { enabled: true, provider: "cursor" };
@@ -1837,7 +2393,7 @@ function detectPostToolUseHook(projectRoot) {
1837
2393
  return { enabled: false, provider: null };
1838
2394
  }
1839
2395
  var cache = /* @__PURE__ */ new Map();
1840
- var eslintInstances = /* @__PURE__ */ new Map();
2396
+ var eslintInstances2 = /* @__PURE__ */ new Map();
1841
2397
  var visionAnalyzer = null;
1842
2398
  function getVisionAnalyzerInstance() {
1843
2399
  if (!visionAnalyzer) {
@@ -1854,20 +2410,20 @@ var resolvedPathCache = /* @__PURE__ */ new Map();
1854
2410
  var subscriptions = /* @__PURE__ */ new Map();
1855
2411
  var fileWatcher = null;
1856
2412
  var connectedClients = 0;
1857
- var localRequire = createRequire(import.meta.url);
1858
- function buildLineStarts(code) {
2413
+ var localRequire = createRequire3(import.meta.url);
2414
+ function buildLineStarts2(code) {
1859
2415
  const starts = [0];
1860
2416
  for (let i = 0; i < code.length; i++) {
1861
2417
  if (code.charCodeAt(i) === 10) starts.push(i + 1);
1862
2418
  }
1863
2419
  return starts;
1864
2420
  }
1865
- function offsetFromLineCol(lineStarts, line1, col0, codeLength) {
2421
+ function offsetFromLineCol2(lineStarts, line1, col0, codeLength) {
1866
2422
  const lineIndex = Math.max(0, Math.min(lineStarts.length - 1, line1 - 1));
1867
2423
  const base = lineStarts[lineIndex] ?? 0;
1868
2424
  return Math.max(0, Math.min(codeLength, base + Math.max(0, col0)));
1869
2425
  }
1870
- function buildJsxElementSpans(code, dataLocFile) {
2426
+ function buildJsxElementSpans2(code, dataLocFile) {
1871
2427
  const { parse: parse3 } = localRequire("@typescript-eslint/typescript-estree");
1872
2428
  const ast = parse3(code, {
1873
2429
  loc: true,
@@ -1901,9 +2457,9 @@ function buildJsxElementSpans(code, dataLocFile) {
1901
2457
  spans.sort((a, b) => a.end - a.start - (b.end - b.start));
1902
2458
  return spans;
1903
2459
  }
1904
- function mapMessageToDataLoc(params) {
2460
+ function mapMessageToDataLoc2(params) {
1905
2461
  const col0 = typeof params.messageCol1 === "number" ? Math.max(0, params.messageCol1 - 1) : 0;
1906
- const offset = offsetFromLineCol(
2462
+ const offset = offsetFromLineCol2(
1907
2463
  params.lineStarts,
1908
2464
  params.messageLine1,
1909
2465
  col0,
@@ -1914,7 +2470,7 @@ function mapMessageToDataLoc(params) {
1914
2470
  }
1915
2471
  return void 0;
1916
2472
  }
1917
- var ESLINT_CONFIG_FILES = [
2473
+ var ESLINT_CONFIG_FILES2 = [
1918
2474
  // Flat config (ESLint v9+)
1919
2475
  "eslint.config.js",
1920
2476
  "eslint.config.mjs",
@@ -1928,57 +2484,57 @@ var ESLINT_CONFIG_FILES = [
1928
2484
  ".eslintrc.yaml",
1929
2485
  ".eslintrc.yml"
1930
2486
  ];
1931
- function findESLintCwd(startDir) {
2487
+ function findESLintCwd2(startDir) {
1932
2488
  let dir = startDir;
1933
2489
  for (let i = 0; i < 30; i++) {
1934
- for (const cfg of ESLINT_CONFIG_FILES) {
1935
- if (existsSync5(join3(dir, cfg))) return dir;
2490
+ for (const cfg of ESLINT_CONFIG_FILES2) {
2491
+ if (existsSync6(join4(dir, cfg))) return dir;
1936
2492
  }
1937
- if (existsSync5(join3(dir, "package.json"))) return dir;
1938
- const parent = dirname5(dir);
2493
+ if (existsSync6(join4(dir, "package.json"))) return dir;
2494
+ const parent = dirname6(dir);
1939
2495
  if (parent === dir) break;
1940
2496
  dir = parent;
1941
2497
  }
1942
2498
  return startDir;
1943
2499
  }
1944
- function normalizePathSlashes(p) {
2500
+ function normalizePathSlashes2(p) {
1945
2501
  return p.replace(/\\/g, "/");
1946
2502
  }
1947
- function normalizeDataLocFilePath(absoluteFilePath, projectCwd) {
1948
- const abs = normalizePathSlashes(resolve5(absoluteFilePath));
1949
- const cwd = normalizePathSlashes(resolve5(projectCwd));
2503
+ function normalizeDataLocFilePath2(absoluteFilePath, projectCwd) {
2504
+ const abs = normalizePathSlashes2(resolve6(absoluteFilePath));
2505
+ const cwd = normalizePathSlashes2(resolve6(projectCwd));
1950
2506
  if (abs === cwd || abs.startsWith(cwd + "/")) {
1951
- return normalizePathSlashes(relative(cwd, abs));
2507
+ return normalizePathSlashes2(relative2(cwd, abs));
1952
2508
  }
1953
2509
  return abs;
1954
2510
  }
1955
2511
  function resolveRequestedFilePath(filePath) {
1956
2512
  if (filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath)) {
1957
- return resolve5(filePath);
2513
+ return resolve6(filePath);
1958
2514
  }
1959
2515
  const cached = resolvedPathCache.get(filePath);
1960
2516
  if (cached) return cached;
1961
2517
  const cwd = process.cwd();
1962
- const fromCwd = resolve5(cwd, filePath);
1963
- if (existsSync5(fromCwd)) {
2518
+ const fromCwd = resolve6(cwd, filePath);
2519
+ if (existsSync6(fromCwd)) {
1964
2520
  resolvedPathCache.set(filePath, fromCwd);
1965
2521
  return fromCwd;
1966
2522
  }
1967
2523
  const wsRoot = findWorkspaceRoot4(cwd);
1968
- const fromWs = resolve5(wsRoot, filePath);
1969
- if (existsSync5(fromWs)) {
2524
+ const fromWs = resolve6(wsRoot, filePath);
2525
+ if (existsSync6(fromWs)) {
1970
2526
  resolvedPathCache.set(filePath, fromWs);
1971
2527
  return fromWs;
1972
2528
  }
1973
2529
  for (const top of ["apps", "packages"]) {
1974
- const base = join3(wsRoot, top);
1975
- if (!existsSync5(base)) continue;
2530
+ const base = join4(wsRoot, top);
2531
+ if (!existsSync6(base)) continue;
1976
2532
  try {
1977
2533
  const entries = readdirSync(base, { withFileTypes: true });
1978
2534
  for (const ent of entries) {
1979
2535
  if (!ent.isDirectory()) continue;
1980
- const p = resolve5(base, ent.name, filePath);
1981
- if (existsSync5(p)) {
2536
+ const p = resolve6(base, ent.name, filePath);
2537
+ if (existsSync6(p)) {
1982
2538
  resolvedPathCache.set(filePath, p);
1983
2539
  return p;
1984
2540
  }
@@ -1989,16 +2545,16 @@ function resolveRequestedFilePath(filePath) {
1989
2545
  resolvedPathCache.set(filePath, fromCwd);
1990
2546
  return fromCwd;
1991
2547
  }
1992
- async function getESLintForProject(projectCwd) {
1993
- const cached = eslintInstances.get(projectCwd);
2548
+ async function getESLintForProject2(projectCwd) {
2549
+ const cached = eslintInstances2.get(projectCwd);
1994
2550
  if (cached) return cached;
1995
2551
  try {
1996
- const req = createRequire(join3(projectCwd, "package.json"));
2552
+ const req = createRequire3(join4(projectCwd, "package.json"));
1997
2553
  const mod = req("eslint");
1998
2554
  const ESLintCtor = mod?.ESLint ?? mod?.default?.ESLint ?? mod?.default ?? mod;
1999
2555
  if (!ESLintCtor) return null;
2000
2556
  const eslint = new ESLintCtor({ cwd: projectCwd });
2001
- eslintInstances.set(projectCwd, eslint);
2557
+ eslintInstances2.set(projectCwd, eslint);
2002
2558
  return eslint;
2003
2559
  } catch {
2004
2560
  return null;
@@ -2006,7 +2562,7 @@ async function getESLintForProject(projectCwd) {
2006
2562
  }
2007
2563
  async function lintFile(filePath, onProgress) {
2008
2564
  const absolutePath = resolveRequestedFilePath(filePath);
2009
- if (!existsSync5(absolutePath)) {
2565
+ if (!existsSync6(absolutePath)) {
2010
2566
  onProgress(`File not found: ${pc.dim(absolutePath)}`);
2011
2567
  return [];
2012
2568
  }
@@ -2022,10 +2578,10 @@ async function lintFile(filePath, onProgress) {
2022
2578
  onProgress("Cache hit (unchanged)");
2023
2579
  return cached.issues;
2024
2580
  }
2025
- const fileDir = dirname5(absolutePath);
2026
- const projectCwd = findESLintCwd(fileDir);
2581
+ const fileDir = dirname6(absolutePath);
2582
+ const projectCwd = findESLintCwd2(fileDir);
2027
2583
  onProgress(`Resolving ESLint project... ${pc.dim(projectCwd)}`);
2028
- const eslint = await getESLintForProject(projectCwd);
2584
+ const eslint = await getESLintForProject2(projectCwd);
2029
2585
  if (!eslint) {
2030
2586
  logWarning(
2031
2587
  `ESLint not found in project. Install it in ${pc.dim(
@@ -2039,16 +2595,17 @@ async function lintFile(filePath, onProgress) {
2039
2595
  onProgress("Running ESLint...");
2040
2596
  const results = await eslint.lintFiles([absolutePath]);
2041
2597
  const messages = Array.isArray(results) && results.length > 0 ? results[0].messages || [] : [];
2042
- const dataLocFile = normalizeDataLocFilePath(absolutePath, projectCwd);
2598
+ const dataLocFile = normalizeDataLocFilePath2(absolutePath, projectCwd);
2043
2599
  let spans = [];
2044
2600
  let lineStarts = [];
2045
2601
  let codeLength = 0;
2602
+ let fileCode = null;
2046
2603
  try {
2047
2604
  onProgress("Building JSX map...");
2048
- const code = readFileSync(absolutePath, "utf-8");
2049
- codeLength = code.length;
2050
- lineStarts = buildLineStarts(code);
2051
- spans = buildJsxElementSpans(code, dataLocFile);
2605
+ fileCode = readFileSync2(absolutePath, "utf-8");
2606
+ codeLength = fileCode.length;
2607
+ lineStarts = buildLineStarts2(fileCode);
2608
+ spans = buildJsxElementSpans2(fileCode, dataLocFile);
2052
2609
  onProgress(`JSX map: ${spans.length} element(s)`);
2053
2610
  } catch (e) {
2054
2611
  onProgress("JSX map failed (falling back to unmapped issues)");
@@ -2056,11 +2613,12 @@ async function lintFile(filePath, onProgress) {
2056
2613
  spans = [];
2057
2614
  lineStarts = [];
2058
2615
  codeLength = 0;
2616
+ fileCode = null;
2059
2617
  }
2060
- const issues = messages.filter((m) => typeof m?.message === "string").map((m) => {
2618
+ let issues = messages.filter((m) => typeof m?.message === "string").map((m) => {
2061
2619
  const line = typeof m.line === "number" ? m.line : 1;
2062
2620
  const column = typeof m.column === "number" ? m.column : void 0;
2063
- const mappedDataLoc = spans.length > 0 && lineStarts.length > 0 && codeLength > 0 ? mapMessageToDataLoc({
2621
+ const mappedDataLoc = spans.length > 0 && lineStarts.length > 0 && codeLength > 0 ? mapMessageToDataLoc2({
2064
2622
  spans,
2065
2623
  lineStarts,
2066
2624
  codeLength,
@@ -2081,6 +2639,12 @@ async function lintFile(filePath, onProgress) {
2081
2639
  `Mapped ${mappedCount}/${issues.length} issue(s) to JSX elements`
2082
2640
  );
2083
2641
  }
2642
+ if (fileCode && issues.length > 0) {
2643
+ onProgress("Extracting scope info...");
2644
+ issues = enrichIssuesWithScopeInfo(issues, fileCode);
2645
+ const scopeCount = issues.filter((i) => Boolean(i.scopeInfo)).length;
2646
+ onProgress(`Enriched ${scopeCount}/${issues.length} issue(s) with scope info`);
2647
+ }
2084
2648
  cache.set(absolutePath, { issues, mtimeMs, timestamp: Date.now() });
2085
2649
  return issues;
2086
2650
  } catch (error) {
@@ -2126,7 +2690,7 @@ async function handleMessage(ws, data) {
2126
2690
  });
2127
2691
  const startedAt = Date.now();
2128
2692
  const resolved = resolveRequestedFilePath(filePath);
2129
- if (!existsSync5(resolved)) {
2693
+ if (!existsSync6(resolved)) {
2130
2694
  const cwd = process.cwd();
2131
2695
  const wsRoot = findWorkspaceRoot4(cwd);
2132
2696
  logServerWarning(
@@ -2278,14 +2842,14 @@ async function handleMessage(ws, data) {
2278
2842
  screenshotFile
2279
2843
  );
2280
2844
  } else {
2281
- const screenshotsDir = join3(
2845
+ const screenshotsDir = join4(
2282
2846
  serverAppRootForVision,
2283
2847
  ".uilint",
2284
2848
  "screenshots"
2285
2849
  );
2286
- const imagePath = join3(screenshotsDir, screenshotFile);
2850
+ const imagePath = join4(screenshotsDir, screenshotFile);
2287
2851
  try {
2288
- if (!existsSync5(imagePath)) {
2852
+ if (!existsSync6(imagePath)) {
2289
2853
  logServerWarning(
2290
2854
  `Skipping vision report write: screenshot file not found`,
2291
2855
  imagePath
@@ -2377,7 +2941,7 @@ async function handleMessage(ws, data) {
2377
2941
  case "source:fetch": {
2378
2942
  const { filePath, requestId } = message;
2379
2943
  const absolutePath = resolveRequestedFilePath(filePath);
2380
- if (!existsSync5(absolutePath)) {
2944
+ if (!existsSync6(absolutePath)) {
2381
2945
  sendMessage(ws, {
2382
2946
  type: "source:error",
2383
2947
  filePath,
@@ -2387,9 +2951,9 @@ async function handleMessage(ws, data) {
2387
2951
  break;
2388
2952
  }
2389
2953
  try {
2390
- const content = readFileSync(absolutePath, "utf-8");
2954
+ const content = readFileSync2(absolutePath, "utf-8");
2391
2955
  const totalLines = content.split("\n").length;
2392
- const relativePath = normalizeDataLocFilePath(absolutePath, serverAppRootForVision);
2956
+ const relativePath = normalizeDataLocFilePath2(absolutePath, serverAppRootForVision);
2393
2957
  sendMessage(ws, {
2394
2958
  type: "source:result",
2395
2959
  filePath,
@@ -2411,8 +2975,8 @@ async function handleMessage(ws, data) {
2411
2975
  case "coverage:request": {
2412
2976
  const { requestId } = message;
2413
2977
  try {
2414
- const coveragePath = join3(serverAppRootForVision, "coverage", "coverage-final.json");
2415
- if (!existsSync5(coveragePath)) {
2978
+ const coveragePath = join4(serverAppRootForVision, "coverage", "coverage-final.json");
2979
+ if (!existsSync6(coveragePath)) {
2416
2980
  sendMessage(ws, {
2417
2981
  type: "coverage:error",
2418
2982
  error: "Coverage data not found. Run tests with coverage first (e.g., `vitest run --coverage`)",
@@ -2420,7 +2984,7 @@ async function handleMessage(ws, data) {
2420
2984
  });
2421
2985
  break;
2422
2986
  }
2423
- const coverageData = JSON.parse(readFileSync(coveragePath, "utf-8"));
2987
+ const coverageData = JSON.parse(readFileSync2(coveragePath, "utf-8"));
2424
2988
  logCoverageResult(Object.keys(coverageData).length);
2425
2989
  sendMessage(ws, {
2426
2990
  type: "coverage:result",
@@ -2470,15 +3034,15 @@ async function handleMessage(ws, data) {
2470
3034
  });
2471
3035
  break;
2472
3036
  }
2473
- const screenshotsDir = join3(serverAppRootForVision, ".uilint", "screenshots");
2474
- if (!existsSync5(screenshotsDir)) {
3037
+ const screenshotsDir = join4(serverAppRootForVision, ".uilint", "screenshots");
3038
+ if (!existsSync6(screenshotsDir)) {
2475
3039
  mkdirSync4(screenshotsDir, { recursive: true });
2476
3040
  }
2477
- const imagePath = join3(screenshotsDir, filename);
3041
+ const imagePath = join4(screenshotsDir, filename);
2478
3042
  const imageBuffer = Buffer.from(base64Data, "base64");
2479
3043
  writeFileSync4(imagePath, imageBuffer);
2480
3044
  const sidecarFilename = filename.replace(/\.png$/, ".json");
2481
- const sidecarPath = join3(screenshotsDir, sidecarFilename);
3045
+ const sidecarPath = join4(screenshotsDir, sidecarFilename);
2482
3046
  const sidecarData = {
2483
3047
  route,
2484
3048
  timestamp,
@@ -2532,7 +3096,7 @@ function handleFileChange(filePath) {
2532
3096
  }
2533
3097
  function handleCoverageFileChange(filePath) {
2534
3098
  try {
2535
- const coverageData = JSON.parse(readFileSync(filePath, "utf-8"));
3099
+ const coverageData = JSON.parse(readFileSync2(filePath, "utf-8"));
2536
3100
  logCoverageResult(Object.keys(coverageData).length);
2537
3101
  broadcast({
2538
3102
  type: "coverage:result",
@@ -2716,7 +3280,7 @@ function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2716
3280
  }
2717
3281
  if (result.success) {
2718
3282
  logServerInfo(`Updated uilint/${normalizedRuleId} -> ${severity}`);
2719
- eslintInstances.clear();
3283
+ eslintInstances2.clear();
2720
3284
  cache.clear();
2721
3285
  updateCacheCount(0);
2722
3286
  sendMessage(ws, {
@@ -2759,7 +3323,7 @@ async function serve(options) {
2759
3323
  ignoreInitial: true
2760
3324
  });
2761
3325
  fileWatcher.on("change", (path) => {
2762
- const resolvedPath = resolve5(path);
3326
+ const resolvedPath = resolve6(path);
2763
3327
  if (resolvedPath.endsWith("coverage-final.json")) {
2764
3328
  handleCoverageFileChange(resolvedPath);
2765
3329
  return;
@@ -2767,8 +3331,8 @@ async function serve(options) {
2767
3331
  handleFileChange(resolvedPath);
2768
3332
  scheduleReindex(appRoot, resolvedPath);
2769
3333
  });
2770
- const coveragePath = join3(appRoot, "coverage", "coverage-final.json");
2771
- if (existsSync5(coveragePath)) {
3334
+ const coveragePath = join4(appRoot, "coverage", "coverage-final.json");
3335
+ if (existsSync6(coveragePath)) {
2772
3336
  fileWatcher.add(coveragePath);
2773
3337
  logServerInfo(`Watching coverage`, coveragePath);
2774
3338
  }
@@ -2862,10 +3426,10 @@ async function serve(options) {
2862
3426
  }
2863
3427
 
2864
3428
  // src/commands/vision.ts
2865
- import { dirname as dirname6, resolve as resolve6, join as join4 } from "path";
3429
+ import { dirname as dirname7, resolve as resolve7, join as join5 } from "path";
2866
3430
  import {
2867
- existsSync as existsSync6,
2868
- readFileSync as readFileSync2,
3431
+ existsSync as existsSync7,
3432
+ readFileSync as readFileSync3,
2869
3433
  readdirSync as readdirSync2
2870
3434
  } from "fs";
2871
3435
  import {
@@ -2892,7 +3456,7 @@ function debugDumpPath3(options) {
2892
3456
  const v = options.debugDump ?? process.env.UILINT_DEBUG_DUMP;
2893
3457
  if (!v) return null;
2894
3458
  if (v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes") {
2895
- return resolve6(process.cwd(), ".uilint");
3459
+ return resolve7(process.cwd(), ".uilint");
2896
3460
  }
2897
3461
  return v;
2898
3462
  }
@@ -2911,17 +3475,17 @@ function debugLog3(enabled, message, obj) {
2911
3475
  function findScreenshotsDirUpwards(startDir) {
2912
3476
  let dir = startDir;
2913
3477
  for (let i = 0; i < 20; i++) {
2914
- const candidate = join4(dir, ".uilint", "screenshots");
2915
- if (existsSync6(candidate)) return candidate;
2916
- const parent = dirname6(dir);
3478
+ const candidate = join5(dir, ".uilint", "screenshots");
3479
+ if (existsSync7(candidate)) return candidate;
3480
+ const parent = dirname7(dir);
2917
3481
  if (parent === dir) break;
2918
3482
  dir = parent;
2919
3483
  }
2920
3484
  return null;
2921
3485
  }
2922
3486
  function listScreenshotSidecars(dirPath) {
2923
- if (!existsSync6(dirPath)) return [];
2924
- const entries = readdirSync2(dirPath).filter((f) => f.endsWith(".json")).map((f) => join4(dirPath, f));
3487
+ if (!existsSync7(dirPath)) return [];
3488
+ const entries = readdirSync2(dirPath).filter((f) => f.endsWith(".json")).map((f) => join5(dirPath, f));
2925
3489
  const out = [];
2926
3490
  for (const p of entries) {
2927
3491
  try {
@@ -2950,11 +3514,11 @@ function listScreenshotSidecars(dirPath) {
2950
3514
  return out;
2951
3515
  }
2952
3516
  function readImageAsBase64(imagePath) {
2953
- const bytes = readFileSync2(imagePath);
3517
+ const bytes = readFileSync3(imagePath);
2954
3518
  return { base64: bytes.toString("base64"), sizeBytes: bytes.byteLength };
2955
3519
  }
2956
3520
  function loadJsonFile(filePath) {
2957
- const raw = readFileSync2(filePath, "utf-8");
3521
+ const raw = readFileSync3(filePath, "utf-8");
2958
3522
  return JSON.parse(raw);
2959
3523
  }
2960
3524
  function formatIssuesText(issues) {
@@ -3028,13 +3592,13 @@ async function vision(options) {
3028
3592
  await flushLangfuse();
3029
3593
  process.exit(1);
3030
3594
  }
3031
- if (imagePath && !existsSync6(imagePath)) {
3595
+ if (imagePath && !existsSync7(imagePath)) {
3032
3596
  throw new Error(`Image not found: ${imagePath}`);
3033
3597
  }
3034
- if (sidecarPath && !existsSync6(sidecarPath)) {
3598
+ if (sidecarPath && !existsSync7(sidecarPath)) {
3035
3599
  throw new Error(`Sidecar not found: ${sidecarPath}`);
3036
3600
  }
3037
- if (manifestFilePath && !existsSync6(manifestFilePath)) {
3601
+ if (manifestFilePath && !existsSync7(manifestFilePath)) {
3038
3602
  throw new Error(`Manifest file not found: ${manifestFilePath}`);
3039
3603
  }
3040
3604
  const sidecar = sidecarPath ? loadJsonFile(sidecarPath) : null;
@@ -3059,7 +3623,7 @@ async function vision(options) {
3059
3623
  const resolved = await resolveVisionStyleGuide({
3060
3624
  projectPath,
3061
3625
  styleguide: options.styleguide,
3062
- startDir: startPath ? dirname6(startPath) : projectPath
3626
+ startDir: startPath ? dirname7(startPath) : projectPath
3063
3627
  });
3064
3628
  styleGuide = resolved.styleGuide;
3065
3629
  styleguideLocation = resolved.styleguideLocation;
@@ -3108,8 +3672,8 @@ async function vision(options) {
3108
3672
  const resolvedImagePath = imagePath || (() => {
3109
3673
  const screenshotFile = typeof sidecar?.screenshotFile === "string" ? sidecar.screenshotFile : typeof sidecar?.filename === "string" ? sidecar.filename : void 0;
3110
3674
  if (!screenshotFile) return null;
3111
- const baseDir = sidecarPath ? dirname6(sidecarPath) : projectPath;
3112
- const abs = resolve6(baseDir, screenshotFile);
3675
+ const baseDir = sidecarPath ? dirname7(sidecarPath) : projectPath;
3676
+ const abs = resolve7(baseDir, screenshotFile);
3113
3677
  return abs;
3114
3678
  })();
3115
3679
  if (!resolvedImagePath) {
@@ -3117,7 +3681,7 @@ async function vision(options) {
3117
3681
  "No image path could be resolved. Provide --image or a sidecar with `screenshotFile`/`filename`."
3118
3682
  );
3119
3683
  }
3120
- if (!existsSync6(resolvedImagePath)) {
3684
+ if (!existsSync7(resolvedImagePath)) {
3121
3685
  throw new Error(`Image not found: ${resolvedImagePath}`);
3122
3686
  }
3123
3687
  const { base64, sizeBytes } = readImageAsBase64(resolvedImagePath);
@@ -3667,7 +4231,7 @@ function indexCommand() {
3667
4231
 
3668
4232
  // src/commands/duplicates/find.ts
3669
4233
  import { Command as Command2 } from "commander";
3670
- import { relative as relative2 } from "path";
4234
+ import { relative as relative3 } from "path";
3671
4235
  import chalk3 from "chalk";
3672
4236
  function findCommand() {
3673
4237
  return new Command2("find").description("Find semantic duplicate groups in the codebase").option(
@@ -3715,7 +4279,7 @@ function findCommand() {
3715
4279
  )
3716
4280
  );
3717
4281
  group.members.forEach((member) => {
3718
- const relPath = relative2(projectRoot, member.filePath);
4282
+ const relPath = relative3(projectRoot, member.filePath);
3719
4283
  const location = `${relPath}:${member.startLine}-${member.endLine}`;
3720
4284
  const name = member.name || "(anonymous)";
3721
4285
  const score = member.score === 1 ? "" : chalk3.dim(` (${Math.round(member.score * 100)}%)`);
@@ -3739,7 +4303,7 @@ function findCommand() {
3739
4303
 
3740
4304
  // src/commands/duplicates/search.ts
3741
4305
  import { Command as Command3 } from "commander";
3742
- import { relative as relative3 } from "path";
4306
+ import { relative as relative4 } from "path";
3743
4307
  import chalk4 from "chalk";
3744
4308
  function searchCommand() {
3745
4309
  return new Command3("search").description("Semantic search for similar code").argument("<query>", "Search query (natural language)").option("-k, --top <n>", "Number of results (default: 10)", parseInt).option("--threshold <n>", "Minimum similarity (default: 0.5)", parseFloat).option("-o, --output <format>", "Output format: text or json", "text").action(async (query, options) => {
@@ -3763,7 +4327,7 @@ function searchCommand() {
3763
4327
  console.log(chalk4.bold(`Found ${results.length} matching results:
3764
4328
  `));
3765
4329
  results.forEach((result, idx) => {
3766
- const relPath = relative3(projectRoot, result.filePath);
4330
+ const relPath = relative4(projectRoot, result.filePath);
3767
4331
  const location = `${relPath}:${result.startLine}-${result.endLine}`;
3768
4332
  const name = result.name || "(anonymous)";
3769
4333
  const score = Math.round(result.score * 100);
@@ -3789,7 +4353,7 @@ function searchCommand() {
3789
4353
 
3790
4354
  // src/commands/duplicates/similar.ts
3791
4355
  import { Command as Command4 } from "commander";
3792
- import { relative as relative4, resolve as resolve7, isAbsolute as isAbsolute2 } from "path";
4356
+ import { relative as relative5, resolve as resolve8, isAbsolute as isAbsolute2 } from "path";
3793
4357
  import chalk5 from "chalk";
3794
4358
  function similarCommand() {
3795
4359
  return new Command4("similar").description("Find code similar to a specific location").argument("<location>", "File location in format file:line (e.g., src/Button.tsx:15)").option("-k, --top <n>", "Number of results (default: 10)", parseInt).option("--threshold <n>", "Minimum similarity (default: 0.7)", parseFloat).option("-o, --output <format>", "Output format: text or json", "text").action(async (location, options) => {
@@ -3818,7 +4382,7 @@ function similarCommand() {
3818
4382
  }
3819
4383
  process.exit(1);
3820
4384
  }
3821
- const filePath = isAbsolute2(filePart) ? filePart : resolve7(projectRoot, filePart);
4385
+ const filePath = isAbsolute2(filePart) ? filePart : resolve8(projectRoot, filePart);
3822
4386
  try {
3823
4387
  const results = await findSimilarAtLocation({
3824
4388
  path: projectRoot,
@@ -3837,12 +4401,12 @@ function similarCommand() {
3837
4401
  }
3838
4402
  console.log(
3839
4403
  chalk5.bold(
3840
- `Found ${results.length} similar code locations to ${relative4(projectRoot, filePath)}:${line}:
4404
+ `Found ${results.length} similar code locations to ${relative5(projectRoot, filePath)}:${line}:
3841
4405
  `
3842
4406
  )
3843
4407
  );
3844
4408
  results.forEach((result, idx) => {
3845
- const relPath = relative4(projectRoot, result.filePath);
4409
+ const relPath = relative5(projectRoot, result.filePath);
3846
4410
  const locationStr = `${relPath}:${result.startLine}-${result.endLine}`;
3847
4411
  const name = result.name || "(anonymous)";
3848
4412
  const score = Math.round(result.score * 100);
@@ -3893,199 +4457,6 @@ import { glob } from "glob";
3893
4457
  import { execSync } from "child_process";
3894
4458
  import { findWorkspaceRoot as findWorkspaceRoot5 } from "uilint-core/node";
3895
4459
  import { ruleRegistry as ruleRegistry2 } from "uilint-eslint";
3896
-
3897
- // src/utils/eslint-utils.ts
3898
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
3899
- import { createRequire as createRequire2 } from "module";
3900
- import { dirname as dirname7, resolve as resolve8, relative as relative5, join as join5 } from "path";
3901
- var ESLINT_CONFIG_FILES2 = [
3902
- // Flat config (ESLint v9+)
3903
- "eslint.config.js",
3904
- "eslint.config.mjs",
3905
- "eslint.config.cjs",
3906
- "eslint.config.ts",
3907
- // Legacy config
3908
- ".eslintrc",
3909
- ".eslintrc.js",
3910
- ".eslintrc.cjs",
3911
- ".eslintrc.json",
3912
- ".eslintrc.yaml",
3913
- ".eslintrc.yml"
3914
- ];
3915
- function buildLineStarts2(code) {
3916
- const starts = [0];
3917
- for (let i = 0; i < code.length; i++) {
3918
- if (code.charCodeAt(i) === 10) starts.push(i + 1);
3919
- }
3920
- return starts;
3921
- }
3922
- function offsetFromLineCol2(lineStarts, line1, col0, codeLength) {
3923
- const lineIndex = Math.max(0, Math.min(lineStarts.length - 1, line1 - 1));
3924
- const base = lineStarts[lineIndex] ?? 0;
3925
- return Math.max(0, Math.min(codeLength, base + Math.max(0, col0)));
3926
- }
3927
- function buildJsxElementSpans2(code, dataLocFile) {
3928
- const localRequire2 = createRequire2(import.meta.url);
3929
- const { parse: parse3 } = localRequire2("@typescript-eslint/typescript-estree");
3930
- const ast = parse3(code, {
3931
- loc: true,
3932
- range: true,
3933
- jsx: true,
3934
- comment: false,
3935
- errorOnUnknownASTType: false
3936
- });
3937
- const spans = [];
3938
- function walk(node) {
3939
- if (!node || typeof node !== "object") return;
3940
- if (node.type === "JSXElement") {
3941
- const range = node.range;
3942
- const opening = node.openingElement;
3943
- const loc = opening?.loc?.start;
3944
- if (range && typeof range[0] === "number" && typeof range[1] === "number" && loc && typeof loc.line === "number" && typeof loc.column === "number") {
3945
- const dataLoc = `${dataLocFile}:${loc.line}:${loc.column}`;
3946
- spans.push({ start: range[0], end: range[1], dataLoc });
3947
- }
3948
- }
3949
- for (const key of Object.keys(node)) {
3950
- const child = node[key];
3951
- if (Array.isArray(child)) {
3952
- for (const item of child) walk(item);
3953
- } else if (child && typeof child === "object") {
3954
- walk(child);
3955
- }
3956
- }
3957
- }
3958
- walk(ast);
3959
- spans.sort((a, b) => a.end - a.start - (b.end - b.start));
3960
- return spans;
3961
- }
3962
- function mapMessageToDataLoc2(params) {
3963
- const col0 = typeof params.messageCol1 === "number" ? Math.max(0, params.messageCol1 - 1) : 0;
3964
- const offset = offsetFromLineCol2(
3965
- params.lineStarts,
3966
- params.messageLine1,
3967
- col0,
3968
- params.codeLength
3969
- );
3970
- for (const s of params.spans) {
3971
- if (s.start <= offset && offset < s.end) return s.dataLoc;
3972
- }
3973
- return void 0;
3974
- }
3975
- function normalizePathSlashes2(p) {
3976
- return p.replace(/\\/g, "/");
3977
- }
3978
- function normalizeDataLocFilePath2(absoluteFilePath, projectCwd) {
3979
- const abs = normalizePathSlashes2(resolve8(absoluteFilePath));
3980
- const cwd = normalizePathSlashes2(resolve8(projectCwd));
3981
- if (abs === cwd || abs.startsWith(cwd + "/")) {
3982
- return normalizePathSlashes2(relative5(cwd, abs));
3983
- }
3984
- return abs;
3985
- }
3986
- function findESLintCwd2(startDir) {
3987
- let dir = startDir;
3988
- for (let i = 0; i < 30; i++) {
3989
- for (const cfg of ESLINT_CONFIG_FILES2) {
3990
- if (existsSync7(join5(dir, cfg))) return dir;
3991
- }
3992
- if (existsSync7(join5(dir, "package.json"))) return dir;
3993
- const parent = dirname7(dir);
3994
- if (parent === dir) break;
3995
- dir = parent;
3996
- }
3997
- return startDir;
3998
- }
3999
- var eslintInstances2 = /* @__PURE__ */ new Map();
4000
- async function getESLintForProject2(projectCwd) {
4001
- const cached = eslintInstances2.get(projectCwd);
4002
- if (cached) return cached;
4003
- try {
4004
- const req = createRequire2(join5(projectCwd, "package.json"));
4005
- const mod = req("eslint");
4006
- const ESLintCtor = mod?.ESLint ?? mod?.default?.ESLint ?? mod?.default ?? mod;
4007
- if (!ESLintCtor) return null;
4008
- const eslint = new ESLintCtor({ cwd: projectCwd });
4009
- eslintInstances2.set(projectCwd, eslint);
4010
- return eslint;
4011
- } catch {
4012
- return null;
4013
- }
4014
- }
4015
- async function lintFileWithDataLoc(absolutePath, projectCwd, onProgress) {
4016
- const progress = onProgress ?? (() => {
4017
- });
4018
- if (!existsSync7(absolutePath)) {
4019
- progress(`File not found: ${absolutePath}`);
4020
- return [];
4021
- }
4022
- progress(`Resolving ESLint project... ${projectCwd}`);
4023
- const eslint = await getESLintForProject2(projectCwd);
4024
- if (!eslint) {
4025
- progress("ESLint not available");
4026
- return [];
4027
- }
4028
- try {
4029
- progress("Running ESLint...");
4030
- const results = await eslint.lintFiles([absolutePath]);
4031
- const messages = Array.isArray(results) && results.length > 0 ? results[0].messages || [] : [];
4032
- const dataLocFile = normalizeDataLocFilePath2(absolutePath, projectCwd);
4033
- let spans = [];
4034
- let lineStarts = [];
4035
- let codeLength = 0;
4036
- try {
4037
- progress("Building JSX map...");
4038
- const code = readFileSync3(absolutePath, "utf-8");
4039
- codeLength = code.length;
4040
- lineStarts = buildLineStarts2(code);
4041
- spans = buildJsxElementSpans2(code, dataLocFile);
4042
- progress(`JSX map: ${spans.length} element(s)`);
4043
- } catch (e) {
4044
- progress("JSX map failed (falling back to unmapped issues)");
4045
- spans = [];
4046
- lineStarts = [];
4047
- codeLength = 0;
4048
- }
4049
- const issues = messages.filter((m) => typeof m?.message === "string").map((m) => {
4050
- const line = typeof m.line === "number" ? m.line : 1;
4051
- const column = typeof m.column === "number" ? m.column : void 0;
4052
- const mappedDataLoc = spans.length > 0 && lineStarts.length > 0 && codeLength > 0 ? mapMessageToDataLoc2({
4053
- spans,
4054
- lineStarts,
4055
- codeLength,
4056
- messageLine1: line,
4057
- messageCol1: column
4058
- }) : void 0;
4059
- return {
4060
- line,
4061
- column,
4062
- message: m.message,
4063
- ruleId: typeof m.ruleId === "string" ? m.ruleId : void 0,
4064
- dataLoc: mappedDataLoc
4065
- };
4066
- });
4067
- const mappedCount = issues.filter((i) => Boolean(i.dataLoc)).length;
4068
- if (issues.length > 0) {
4069
- progress(`Mapped ${mappedCount}/${issues.length} issue(s) to JSX elements`);
4070
- }
4071
- return issues;
4072
- } catch (error) {
4073
- progress(`ESLint failed: ${error instanceof Error ? error.message : String(error)}`);
4074
- return [];
4075
- }
4076
- }
4077
- function extractSourceSnippet(code, centerLine, contextLines = 3) {
4078
- const allLines = code.split("\n");
4079
- const startLine = Math.max(1, centerLine - contextLines);
4080
- const endLine = Math.min(allLines.length, centerLine + contextLines);
4081
- return {
4082
- lines: allLines.slice(startLine - 1, endLine),
4083
- startLine,
4084
- endLine
4085
- };
4086
- }
4087
-
4088
- // src/commands/manifest/generator.ts
4089
4460
  var DEFAULT_INCLUDE = ["**/*.tsx", "**/*.jsx"];
4090
4461
  var DEFAULT_EXCLUDE = [
4091
4462
  "node_modules/**",
@@ -4166,36 +4537,50 @@ async function generateManifest(options = {}) {
4166
4537
  const relativePath = relative6(cwd, absolutePath);
4167
4538
  onProgress(`Linting ${relativePath}...`, i + 1, files.length);
4168
4539
  const fileDir = dirname8(absolutePath);
4169
- const projectCwd = findESLintCwd2(fileDir);
4540
+ const projectCwd = findESLintCwd(fileDir);
4170
4541
  const issues = await lintFileWithDataLoc(absolutePath, projectCwd);
4171
4542
  if (issues.length === 0) continue;
4172
- const manifestIssues = issues.filter((issue) => Boolean(issue.dataLoc)).map((issue) => ({
4543
+ const filteredIssues = issues.filter(
4544
+ (issue) => Boolean(issue.dataLoc)
4545
+ );
4546
+ if (filteredIssues.length === 0) continue;
4547
+ let fileContent;
4548
+ let snippets;
4549
+ try {
4550
+ fileContent = readFileSync4(absolutePath, "utf-8");
4551
+ } catch {
4552
+ fileContent = void 0;
4553
+ }
4554
+ let scopeInfos = [];
4555
+ if (fileContent) {
4556
+ try {
4557
+ const positions = filteredIssues.map((issue) => ({
4558
+ line: issue.line,
4559
+ column: issue.column ?? 0
4560
+ }));
4561
+ scopeInfos = findEnclosingScopeBatch(fileContent, positions);
4562
+ } catch {
4563
+ scopeInfos = filteredIssues.map(() => null);
4564
+ }
4565
+ }
4566
+ const manifestIssues = filteredIssues.map((issue, index) => ({
4173
4567
  line: issue.line,
4174
4568
  column: issue.column,
4175
4569
  message: issue.message,
4176
4570
  ruleId: issue.ruleId,
4177
- dataLoc: issue.dataLoc
4571
+ dataLoc: issue.dataLoc,
4572
+ scopeInfo: scopeInfos[index] ?? void 0
4178
4573
  }));
4179
- if (manifestIssues.length === 0) continue;
4180
- let fileContent;
4181
- let snippets;
4182
- if (includeSource || includeSnippets) {
4183
- try {
4184
- fileContent = readFileSync4(absolutePath, "utf-8");
4185
- if (includeSnippets) {
4186
- snippets = {};
4187
- const issuesByDataLoc = /* @__PURE__ */ new Map();
4188
- for (const issue of manifestIssues) {
4189
- if (!issuesByDataLoc.has(issue.dataLoc)) {
4190
- issuesByDataLoc.set(issue.dataLoc, issue);
4191
- }
4192
- }
4193
- for (const [dataLoc, issue] of issuesByDataLoc) {
4194
- snippets[dataLoc] = extractSourceSnippet(fileContent, issue.line, snippetContextLines);
4195
- }
4574
+ if (includeSnippets && fileContent) {
4575
+ snippets = {};
4576
+ const issuesByDataLoc = /* @__PURE__ */ new Map();
4577
+ for (const issue of manifestIssues) {
4578
+ if (!issuesByDataLoc.has(issue.dataLoc)) {
4579
+ issuesByDataLoc.set(issue.dataLoc, issue);
4196
4580
  }
4197
- } catch {
4198
- fileContent = void 0;
4581
+ }
4582
+ for (const [dataLoc, issue] of issuesByDataLoc) {
4583
+ snippets[dataLoc] = extractSourceSnippet(fileContent, issue.line, snippetContextLines);
4199
4584
  }
4200
4585
  }
4201
4586
  for (const issue of manifestIssues) {
@@ -4212,7 +4597,7 @@ async function generateManifest(options = {}) {
4212
4597
  }
4213
4598
  }
4214
4599
  totalIssues += manifestIssues.length;
4215
- const dataLocFilePath = normalizeDataLocFilePath2(absolutePath, projectCwd);
4600
+ const dataLocFilePath = normalizeDataLocFilePath(absolutePath, projectCwd);
4216
4601
  manifestFiles.push({
4217
4602
  filePath: dataLocFilePath,
4218
4603
  issues: manifestIssues,