uilint 0.2.145 → 0.2.147

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getDashboardStore
4
- } from "./chunk-ZOLQHFVQ.js";
4
+ } from "./chunk-JEYSQ5KF.js";
5
5
  import {
6
6
  detectCoverageSetup,
7
7
  detectNextAppRouter,
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  discoverPlugins,
18
18
  loadPluginESLintRules
19
- } from "./chunk-WG2WZTB2.js";
19
+ } from "./chunk-5QUW7BNW.js";
20
20
  import {
21
21
  createSpinner,
22
22
  intro,
@@ -1508,6 +1508,19 @@ function logVisionDone(route, issueCount, elapsedMs) {
1508
1508
  function logVisionCheck(requestId) {
1509
1509
  logActivity("vision:check", requestId ? `(req ${requestId})` : "");
1510
1510
  }
1511
+ function logSemanticAnalyze(filePath, requestId) {
1512
+ const msg = filePath + (requestId ? ` (req ${requestId})` : "");
1513
+ logActivity("semantic:analyze", msg);
1514
+ }
1515
+ function logSemanticDone(filePath, issueCount, elapsedMs) {
1516
+ logActivity(
1517
+ "semantic:done",
1518
+ `${filePath} \u2192 ${issueCount} issue(s) (${elapsedMs}ms)`
1519
+ );
1520
+ }
1521
+ function logSemanticSkipped(filePath, reason) {
1522
+ logActivity("semantic:skip", `${filePath} \u2014 ${reason}`);
1523
+ }
1511
1524
  function logConfigSet(key, value) {
1512
1525
  logActivity("config:set", `${key} = ${JSON.stringify(value)}`);
1513
1526
  }
@@ -1635,6 +1648,40 @@ function completeBackgroundTask(id, successMessage, error) {
1635
1648
  function logRuleInternalError(ruleId, filePath, detail) {
1636
1649
  logActivity("error", `Rule error [${ruleId}] ${filePath}`, detail, true);
1637
1650
  }
1651
+ function registerPlugin(id, name, model) {
1652
+ if (useDashboard) {
1653
+ const store = getDashboardStore();
1654
+ store.registerPlugin(id, name, model);
1655
+ } else {
1656
+ consoleInfo(`Plugin registered: ${name} (${model})`);
1657
+ }
1658
+ }
1659
+ function setPluginStatus(id, status, message) {
1660
+ if (useDashboard) {
1661
+ const store = getDashboardStore();
1662
+ store.setPluginState(id, {
1663
+ status,
1664
+ message,
1665
+ error: status === "error" ? message : void 0,
1666
+ // Clear progress on terminal states
1667
+ ...status === "idle" || status === "complete" || status === "error" ? { progress: void 0, current: void 0, total: void 0 } : {}
1668
+ });
1669
+ } else if (status === "error") {
1670
+ consoleError(`Plugin ${id}: ${message}`);
1671
+ }
1672
+ }
1673
+ function updatePluginProgress(id, progress, current, total, message) {
1674
+ if (useDashboard) {
1675
+ const store = getDashboardStore();
1676
+ store.setPluginState(id, { progress, current, total, message });
1677
+ }
1678
+ }
1679
+ function setPluginModel(id, model) {
1680
+ if (useDashboard) {
1681
+ const store = getDashboardStore();
1682
+ store.setPluginState(id, { model });
1683
+ }
1684
+ }
1638
1685
 
1639
1686
  // src/commands/serve.ts
1640
1687
  import { ruleRegistry } from "uilint-eslint";
@@ -2302,13 +2349,58 @@ async function getVisionAnalyzerInstance() {
2302
2349
  return visionAnalyzer;
2303
2350
  }
2304
2351
  var ollamaMutexPromise = Promise.resolve();
2305
- function acquireOllamaMutex() {
2352
+ var ollamaMutexHolder = null;
2353
+ var ollamaMutexQueue = [];
2354
+ function acquireOllamaMutex(pluginId) {
2306
2355
  let release;
2307
2356
  const prev = ollamaMutexPromise;
2308
2357
  ollamaMutexPromise = new Promise((resolve11) => {
2309
2358
  release = resolve11;
2310
2359
  });
2311
- return prev.then(() => release);
2360
+ ollamaMutexQueue.push(pluginId);
2361
+ setPluginStatus(pluginId, "waiting-for-ollama", "Queued for Ollama...");
2362
+ return prev.then(() => {
2363
+ const idx = ollamaMutexQueue.indexOf(pluginId);
2364
+ if (idx !== -1) ollamaMutexQueue.splice(idx, 1);
2365
+ ollamaMutexHolder = pluginId;
2366
+ setPluginStatus(pluginId, "using-ollama", "Using Ollama...");
2367
+ return () => {
2368
+ if (ollamaMutexHolder === pluginId) {
2369
+ ollamaMutexHolder = null;
2370
+ }
2371
+ release();
2372
+ };
2373
+ });
2374
+ }
2375
+ var semanticFilesRequested = 0;
2376
+ var semanticFilesCompleted = 0;
2377
+ var semanticIdleResetTimer = null;
2378
+ function updateSemanticBatchProgress(message) {
2379
+ const progress = semanticFilesRequested > 0 ? Math.round(semanticFilesCompleted / semanticFilesRequested * 100) : 0;
2380
+ updatePluginProgress(
2381
+ "semantic",
2382
+ progress,
2383
+ semanticFilesCompleted,
2384
+ semanticFilesRequested,
2385
+ message
2386
+ );
2387
+ }
2388
+ function completeSemanticFile(terminalStatus, message) {
2389
+ semanticFilesCompleted++;
2390
+ if (semanticFilesCompleted >= semanticFilesRequested) {
2391
+ const summary = terminalStatus === "complete" ? `Done: ${semanticFilesCompleted} file(s) analyzed` : message;
2392
+ setPluginStatus("semantic", terminalStatus, summary);
2393
+ semanticIdleResetTimer = setTimeout(() => {
2394
+ setPluginStatus("semantic", "idle");
2395
+ semanticFilesRequested = 0;
2396
+ semanticFilesCompleted = 0;
2397
+ semanticIdleResetTimer = null;
2398
+ }, 3e3);
2399
+ } else {
2400
+ updateSemanticBatchProgress(
2401
+ `${semanticFilesCompleted}/${semanticFilesRequested} files analyzed`
2402
+ );
2403
+ }
2312
2404
  }
2313
2405
  var serverAppRootForVision = process.cwd();
2314
2406
  function isValidScreenshotFilename(filename) {
@@ -2454,23 +2546,6 @@ function resolveRequestedFilePath(filePath) {
2454
2546
  resolvedPathCache.set(filePath, fromCwd);
2455
2547
  return fromCwd;
2456
2548
  }
2457
- async function getESLintForProject2(projectCwd) {
2458
- const cached = eslintInstances2.get(projectCwd);
2459
- if (cached) return cached;
2460
- try {
2461
- const req = createRequire3(join3(projectCwd, "package.json"));
2462
- const mod = req("eslint");
2463
- const modDefault = mod?.default;
2464
- const ESLintCtor = mod?.ESLint ?? modDefault?.ESLint ?? mod?.default ?? mod;
2465
- if (!ESLintCtor) return null;
2466
- const Ctor = ESLintCtor;
2467
- const eslint = new Ctor({ cwd: projectCwd });
2468
- eslintInstances2.set(projectCwd, eslint);
2469
- return eslint;
2470
- } catch {
2471
- return null;
2472
- }
2473
- }
2474
2549
  async function getESLintWithOverrides(projectCwd, overrideRules, instanceCache) {
2475
2550
  const cacheKey = `${projectCwd}::${JSON.stringify(overrideRules)}`;
2476
2551
  const cached = instanceCache.get(cacheKey);
@@ -2597,39 +2672,329 @@ async function lintFileFast(filePath, onProgress) {
2597
2672
  return [];
2598
2673
  }
2599
2674
  }
2600
- async function lintFileSemantic(filePath, onProgress) {
2675
+ async function runSemanticAnalysisAsync(filePath, ws, requestId) {
2676
+ const startTime = Date.now();
2677
+ logSemanticAnalyze(filePath, requestId);
2601
2678
  const absolutePath = resolveRequestedFilePath(filePath);
2602
- if (!existsSync5(absolutePath)) return [];
2603
- const mtimeMs = (() => {
2604
- try {
2605
- return statSync2(absolutePath).mtimeMs;
2606
- } catch {
2607
- return 0;
2608
- }
2609
- })();
2610
- const cached = semanticCache.get(absolutePath);
2611
- if (cached && cached.mtimeMs === mtimeMs) {
2612
- onProgress("Semantic cache hit (unchanged)");
2613
- return cached.issues;
2679
+ if (!existsSync5(absolutePath)) {
2680
+ logSemanticSkipped(filePath, "file not found");
2681
+ return;
2614
2682
  }
2683
+ const eslintConfigPath = findEslintConfigFile(serverAppRootForVision);
2684
+ const ruleConfigs = eslintConfigPath ? readRuleConfigsFromConfig(eslintConfigPath) : /* @__PURE__ */ new Map();
2685
+ const semanticConfig = ruleConfigs.get("semantic");
2686
+ const model = semanticConfig?.options?.model || "qwen3-vl:8b-instruct";
2687
+ const styleguidePath = semanticConfig?.options?.styleguidePath || void 0;
2688
+ setPluginModel("semantic", model);
2689
+ const { getStyleguide, hashContentSync, setCacheEntry, getCacheEntry } = await import("uilint-eslint");
2615
2690
  const fileDir = dirname5(absolutePath);
2616
- const projectCwd = findESLintCwd2(fileDir);
2617
- const eslint = await getESLintForProject2(projectCwd);
2618
- if (!eslint) return [];
2691
+ const { content: styleguide } = getStyleguide(fileDir, styleguidePath);
2692
+ if (!styleguide) {
2693
+ logSemanticSkipped(filePath, "no styleguide found");
2694
+ return;
2695
+ }
2696
+ let fileContent;
2619
2697
  try {
2620
- onProgress("Running semantic analysis...");
2621
- const results = await eslint.lintFiles([absolutePath]);
2622
- const messages = Array.isArray(results) && results.length > 0 ? results[0].messages || [] : [];
2623
- const semanticMessages = messages.filter(
2624
- (m) => m.ruleId === "uilint/semantic"
2698
+ fileContent = readFileSync2(absolutePath, "utf-8");
2699
+ } catch {
2700
+ logSemanticSkipped(filePath, "file read error");
2701
+ return;
2702
+ }
2703
+ const fileHash = hashContentSync(fileContent);
2704
+ const styleguideHash = hashContentSync(styleguide);
2705
+ const projectRoot = findWorkspaceRoot4(fileDir) || fileDir;
2706
+ const relativeFilePath = relative2(projectRoot, absolutePath);
2707
+ const cached = getCacheEntry(projectRoot, relativeFilePath, fileHash, styleguideHash);
2708
+ if (cached) {
2709
+ logSemanticSkipped(filePath, "cache fresh");
2710
+ return;
2711
+ }
2712
+ if (semanticIdleResetTimer) {
2713
+ clearTimeout(semanticIdleResetTimer);
2714
+ semanticIdleResetTimer = null;
2715
+ }
2716
+ semanticFilesRequested++;
2717
+ updateSemanticBatchProgress(
2718
+ `Queued ${relative2(serverAppRootForVision, absolutePath)}...`
2719
+ );
2720
+ sendMessage(ws, {
2721
+ type: "lint:progress",
2722
+ filePath,
2723
+ requestId,
2724
+ phase: "Running semantic analysis (async)..."
2725
+ });
2726
+ startBackgroundTask("semantic-analysis", "Semantic Analysis", `Analyzing ${filePath}...`);
2727
+ broadcast({
2728
+ type: "plugin:operation:start",
2729
+ pluginId: "semantic",
2730
+ operationName: "analysis",
2731
+ message: `Analyzing ${relative2(serverAppRootForVision, absolutePath)}...`
2732
+ });
2733
+ const release = await acquireOllamaMutex("semantic");
2734
+ try {
2735
+ const { OllamaClient: OllamaClient2, buildSourceScanPrompt: buildSourceScanPrompt2 } = await import("uilint-core/node");
2736
+ const client = new OllamaClient2({ model });
2737
+ const ok = await client.isAvailable();
2738
+ if (!ok) {
2739
+ logServerWarning("Semantic analysis: Ollama not available");
2740
+ updateBackgroundTaskProgress("semantic-analysis", 0, 0, 0, "Ollama not available");
2741
+ completeBackgroundTask("semantic-analysis", void 0, "Ollama not available");
2742
+ broadcast({
2743
+ type: "plugin:operation:error",
2744
+ pluginId: "semantic",
2745
+ operationName: "analysis",
2746
+ error: "Ollama not available"
2747
+ });
2748
+ completeSemanticFile("error", "Ollama not available");
2749
+ return;
2750
+ }
2751
+ updateBackgroundTaskProgress("semantic-analysis", 50, 0, 0, "Waiting for LLM response...");
2752
+ updateSemanticBatchProgress(
2753
+ `Analyzing ${relative2(serverAppRootForVision, absolutePath)}...`
2625
2754
  );
2626
- if (semanticMessages.length === 0) return [];
2627
- const issues = processLintResults(absolutePath, projectCwd, semanticMessages, onProgress);
2628
- semanticCache.set(absolutePath, { issues, mtimeMs, timestamp: Date.now() });
2629
- return issues;
2755
+ broadcast({
2756
+ type: "plugin:operation:progress",
2757
+ pluginId: "semantic",
2758
+ operationName: "analysis",
2759
+ message: "Waiting for LLM response..."
2760
+ });
2761
+ const prompt = buildSourceScanPrompt2(fileContent, styleguide, {
2762
+ filePath: relative2(serverAppRootForVision, absolutePath)
2763
+ });
2764
+ const responseText = await client.complete(prompt, { json: true });
2765
+ const parsed = JSON.parse(responseText);
2766
+ const issues = (parsed.issues || []).map((issue) => ({
2767
+ line: issue.line || 1,
2768
+ column: issue.column,
2769
+ message: issue.message || "Semantic issue detected",
2770
+ ruleId: "uilint/semantic",
2771
+ severity: 1
2772
+ }));
2773
+ setCacheEntry(projectRoot, relativeFilePath, {
2774
+ fileHash,
2775
+ styleguideHash,
2776
+ issues,
2777
+ timestamp: Date.now()
2778
+ });
2779
+ const fileDir2 = dirname5(absolutePath);
2780
+ const projectCwd = findESLintCwd2(fileDir2);
2781
+ const dataLocFile = normalizeDataLocFilePath2(absolutePath, projectCwd);
2782
+ const lintIssues = issues.map((issue) => ({
2783
+ ruleId: "uilint/semantic",
2784
+ severity: issue.severity,
2785
+ message: issue.message,
2786
+ line: issue.line,
2787
+ column: issue.column || 0,
2788
+ nodeType: null,
2789
+ source: null,
2790
+ dataLoc: `${dataLocFile}:${issue.line}:${issue.column || 0}`
2791
+ }));
2792
+ sendMessage(ws, {
2793
+ type: "lint:result",
2794
+ filePath,
2795
+ requestId,
2796
+ issues: lintIssues
2797
+ });
2798
+ const elapsed = Date.now() - startTime;
2799
+ const msg = `${issues.length} issue(s) found`;
2800
+ logSemanticDone(filePath, issues.length, elapsed);
2801
+ completeBackgroundTask("semantic-analysis", msg);
2802
+ completeSemanticFile("complete", msg);
2803
+ broadcast({
2804
+ type: "plugin:operation:complete",
2805
+ pluginId: "semantic",
2806
+ operationName: "analysis",
2807
+ message: msg
2808
+ });
2809
+ sendMessage(ws, {
2810
+ type: "lint:progress",
2811
+ filePath,
2812
+ requestId,
2813
+ phase: `Done (semantic: ${issues.length} issues)`
2814
+ });
2630
2815
  } catch (error) {
2631
- logServerError("Semantic pass failed", error instanceof Error ? error.message : String(error));
2632
- return [];
2816
+ const errorMessage = error instanceof Error ? error.message : String(error);
2817
+ logServerError("Async semantic analysis failed", errorMessage);
2818
+ completeBackgroundTask("semantic-analysis", void 0, errorMessage);
2819
+ completeSemanticFile("error", errorMessage);
2820
+ broadcast({
2821
+ type: "plugin:operation:error",
2822
+ pluginId: "semantic",
2823
+ operationName: "analysis",
2824
+ error: errorMessage
2825
+ });
2826
+ } finally {
2827
+ release();
2828
+ }
2829
+ }
2830
+ async function runVisionAnalysisInBackground(ws, message) {
2831
+ const { route, timestamp, screenshot, screenshotFile, manifest, requestId } = message;
2832
+ setPluginStatus("vision", "processing", `Analyzing ${route}...`);
2833
+ startBackgroundTask("vision-analysis", "Vision Analysis", `Analyzing ${route}...`);
2834
+ broadcast({
2835
+ type: "plugin:operation:start",
2836
+ pluginId: "vision",
2837
+ operationName: "analysis",
2838
+ message: `Analyzing ${route}...`
2839
+ });
2840
+ const visionMod = await getVisionModule();
2841
+ if (!visionMod) {
2842
+ sendMessage(ws, {
2843
+ type: "vision:result",
2844
+ route,
2845
+ issues: [],
2846
+ analysisTime: 0,
2847
+ error: "uilint-vision is not installed",
2848
+ requestId
2849
+ });
2850
+ completeBackgroundTask("vision-analysis", void 0, "uilint-vision not installed");
2851
+ setPluginStatus("vision", "error", "uilint-vision not installed");
2852
+ setTimeout(() => setPluginStatus("vision", "idle"), 3e3);
2853
+ broadcast({
2854
+ type: "plugin:operation:error",
2855
+ pluginId: "vision",
2856
+ operationName: "analysis",
2857
+ error: "uilint-vision not installed"
2858
+ });
2859
+ return;
2860
+ }
2861
+ sendMessage(ws, {
2862
+ type: "vision:progress",
2863
+ route,
2864
+ requestId,
2865
+ phase: "Starting vision analysis..."
2866
+ });
2867
+ const startedAt = Date.now();
2868
+ const analyzer = await getVisionAnalyzerInstance();
2869
+ updateBackgroundTaskProgress("vision-analysis", 10, 0, 0, "Waiting for Ollama...");
2870
+ const releaseOllama = await acquireOllamaMutex("vision");
2871
+ try {
2872
+ const analyzerObj = analyzer;
2873
+ const analyzerModel = typeof analyzerObj?.getModel === "function" ? analyzerObj.getModel() : void 0;
2874
+ const analyzerBaseUrl = typeof analyzerObj?.getBaseUrl === "function" ? analyzerObj.getBaseUrl() : void 0;
2875
+ if (analyzerModel) {
2876
+ setPluginModel("vision", analyzerModel);
2877
+ }
2878
+ if (!screenshot) {
2879
+ sendMessage(ws, {
2880
+ type: "vision:result",
2881
+ route,
2882
+ issues: [],
2883
+ analysisTime: Date.now() - startedAt,
2884
+ error: "No screenshot provided for vision analysis",
2885
+ requestId
2886
+ });
2887
+ completeBackgroundTask("vision-analysis", void 0, "No screenshot provided");
2888
+ setPluginStatus("vision", "error", "No screenshot provided");
2889
+ setTimeout(() => setPluginStatus("vision", "idle"), 3e3);
2890
+ broadcast({
2891
+ type: "plugin:operation:error",
2892
+ pluginId: "vision",
2893
+ operationName: "analysis",
2894
+ error: "No screenshot provided"
2895
+ });
2896
+ return;
2897
+ }
2898
+ updateBackgroundTaskProgress("vision-analysis", 30, 0, 0, "Running vision analysis...");
2899
+ broadcast({
2900
+ type: "plugin:operation:progress",
2901
+ pluginId: "vision",
2902
+ operationName: "analysis",
2903
+ message: "Running vision analysis..."
2904
+ });
2905
+ const result = await visionMod.runVisionAnalysis({
2906
+ imageBase64: screenshot,
2907
+ manifest,
2908
+ projectPath: serverAppRootForVision,
2909
+ baseUrl: analyzerBaseUrl,
2910
+ model: analyzerModel,
2911
+ analyzer,
2912
+ onPhase: (phase) => {
2913
+ sendMessage(ws, {
2914
+ type: "vision:progress",
2915
+ route,
2916
+ requestId,
2917
+ phase
2918
+ });
2919
+ updateBackgroundTaskProgress("vision-analysis", 50, 0, 0, phase);
2920
+ },
2921
+ pathResolver: resolvePathSpecifier
2922
+ });
2923
+ if (typeof screenshotFile === "string" && screenshotFile.length > 0) {
2924
+ if (isValidScreenshotFilename(screenshotFile)) {
2925
+ const screenshotsDir = join3(serverAppRootForVision, ".uilint", "screenshots");
2926
+ const imagePath = join3(screenshotsDir, screenshotFile);
2927
+ try {
2928
+ if (existsSync5(imagePath)) {
2929
+ const report = visionMod.writeVisionMarkdownReport({
2930
+ imagePath,
2931
+ route,
2932
+ timestamp,
2933
+ visionModel: result.visionModel,
2934
+ baseUrl: result.baseUrl,
2935
+ analysisTimeMs: result.analysisTime,
2936
+ prompt: result.prompt ?? null,
2937
+ rawResponse: result.rawResponse ?? null,
2938
+ metadata: {
2939
+ screenshotFile: parse(imagePath).base,
2940
+ appRoot: serverAppRootForVision,
2941
+ manifestElements: manifest.length,
2942
+ requestId: requestId ?? null
2943
+ }
2944
+ });
2945
+ logServerInfo(`Wrote vision report`, report.outPath);
2946
+ }
2947
+ } catch (e) {
2948
+ logServerWarning(
2949
+ `Failed to write vision report for ${screenshotFile}`,
2950
+ e instanceof Error ? e.message : String(e)
2951
+ );
2952
+ }
2953
+ }
2954
+ }
2955
+ const elapsed = Date.now() - startedAt;
2956
+ const resultIssues = result.issues;
2957
+ logVisionDone(route, resultIssues.length, elapsed);
2958
+ sendMessage(ws, {
2959
+ type: "vision:result",
2960
+ route,
2961
+ issues: resultIssues,
2962
+ analysisTime: result.analysisTime,
2963
+ requestId
2964
+ });
2965
+ const msg = `${resultIssues.length} issue(s) in ${(elapsed / 1e3).toFixed(1)}s`;
2966
+ completeBackgroundTask("vision-analysis", msg);
2967
+ setPluginStatus("vision", "complete", msg);
2968
+ setTimeout(() => setPluginStatus("vision", "idle"), 3e3);
2969
+ broadcast({
2970
+ type: "plugin:operation:complete",
2971
+ pluginId: "vision",
2972
+ operationName: "analysis",
2973
+ message: msg
2974
+ });
2975
+ } catch (error) {
2976
+ const errorMessage = error instanceof Error ? error.message : String(error);
2977
+ const elapsed = Date.now() - startedAt;
2978
+ logServerError(`Vision analysis failed for ${route}`, errorMessage);
2979
+ sendMessage(ws, {
2980
+ type: "vision:result",
2981
+ route,
2982
+ issues: [],
2983
+ analysisTime: elapsed,
2984
+ error: errorMessage,
2985
+ requestId
2986
+ });
2987
+ completeBackgroundTask("vision-analysis", void 0, errorMessage);
2988
+ setPluginStatus("vision", "error", errorMessage);
2989
+ setTimeout(() => setPluginStatus("vision", "idle"), 3e3);
2990
+ broadcast({
2991
+ type: "plugin:operation:error",
2992
+ pluginId: "vision",
2993
+ operationName: "analysis",
2994
+ error: errorMessage
2995
+ });
2996
+ } finally {
2997
+ releaseOllama();
2633
2998
  }
2634
2999
  }
2635
3000
  function sendMessage(ws, message) {
@@ -2690,52 +3055,18 @@ async function handleMessage(ws, data) {
2690
3055
  logLintDone(filePath, fastClientIssues.length, fastElapsed);
2691
3056
  updateCacheCount(fastCache.size + semanticCache.size);
2692
3057
  sendMessage(ws, { type: "lint:result", filePath, requestId, issues: fastClientIssues });
3058
+ sendMessage(ws, {
3059
+ type: "lint:progress",
3060
+ filePath,
3061
+ requestId,
3062
+ phase: `Done (${fastClientIssues.length} issues, ${fastElapsed}ms)`
3063
+ });
2693
3064
  if (isSemanticRuleEnabled()) {
2694
- sendMessage(ws, {
2695
- type: "lint:progress",
2696
- filePath,
2697
- requestId,
2698
- phase: `Fast lint done (${fastClientIssues.length} issues, ${fastElapsed}ms). Running semantic...`
2699
- });
2700
- setImmediate(async () => {
2701
- try {
2702
- const _semanticStart = Date.now();
2703
- const semanticIssues = await lintFileSemantic(filePath, (phase) => {
2704
- sendMessage(ws, { type: "lint:progress", filePath, requestId, phase });
2705
- });
2706
- const semanticSentinels = semanticIssues.filter(isSentinelIssue);
2707
- for (const se of semanticSentinels) {
2708
- logRuleInternalError(se.ruleId ?? "unknown", filePath, se.message);
2709
- }
2710
- const semanticClientIssues = semanticIssues.filter((i) => !isSentinelIssue(i));
2711
- const totalElapsed = Date.now() - startedAt;
2712
- const totalIssues = fastClientIssues.length + semanticClientIssues.length;
2713
- if (semanticClientIssues.length > 0) {
2714
- sendMessage(ws, { type: "lint:result", filePath, requestId, issues: semanticClientIssues });
2715
- }
2716
- sendMessage(ws, {
2717
- type: "lint:progress",
2718
- filePath,
2719
- requestId,
2720
- phase: `Done (${totalIssues} issues, ${totalElapsed}ms)`
2721
- });
2722
- } catch (error) {
2723
- logServerError("Semantic pass failed", error instanceof Error ? error.message : String(error));
2724
- sendMessage(ws, {
2725
- type: "lint:progress",
2726
- filePath,
2727
- requestId,
2728
- phase: `Done (${fastClientIssues.length} issues, ${Date.now() - startedAt}ms)`
2729
- });
2730
- }
3065
+ runSemanticAnalysisAsync(filePath, ws, requestId).catch((err) => {
3066
+ logServerError("Async semantic analysis failed", err instanceof Error ? err.message : String(err));
2731
3067
  });
2732
3068
  } else {
2733
- sendMessage(ws, {
2734
- type: "lint:progress",
2735
- filePath,
2736
- requestId,
2737
- phase: `Done (${fastClientIssues.length} issues, ${fastElapsed}ms)`
2738
- });
3069
+ logSemanticSkipped(filePath, "rule not enabled");
2739
3070
  }
2740
3071
  break;
2741
3072
  }
@@ -2757,39 +3088,7 @@ async function handleMessage(ws, data) {
2757
3088
  }
2758
3089
  const fastFiltered = fastIssues.filter((i) => !isSentinelIssue(i)).filter((issue) => issue.dataLoc === dataLoc);
2759
3090
  sendMessage(ws, { type: "lint:result", filePath, requestId, issues: fastFiltered });
2760
- if (isSemanticRuleEnabled()) {
2761
- setImmediate(async () => {
2762
- try {
2763
- const semanticIssues = await lintFileSemantic(filePath, (phase) => {
2764
- sendMessage(ws, { type: "lint:progress", filePath, requestId, phase });
2765
- });
2766
- const semanticSentinels = semanticIssues.filter(isSentinelIssue);
2767
- for (const se of semanticSentinels) {
2768
- logRuleInternalError(se.ruleId ?? "unknown", filePath, se.message);
2769
- }
2770
- const semanticFiltered = semanticIssues.filter((i) => !isSentinelIssue(i)).filter((issue) => issue.dataLoc === dataLoc);
2771
- const totalFiltered = fastFiltered.length + semanticFiltered.length;
2772
- const elapsed = Date.now() - startedAt;
2773
- if (semanticFiltered.length > 0) {
2774
- sendMessage(ws, { type: "lint:result", filePath, requestId, issues: semanticFiltered });
2775
- }
2776
- sendMessage(ws, {
2777
- type: "lint:progress",
2778
- filePath,
2779
- requestId,
2780
- phase: `Done (${totalFiltered} issues, ${elapsed}ms)`
2781
- });
2782
- } catch (error) {
2783
- logServerError("Semantic pass failed", error instanceof Error ? error.message : String(error));
2784
- sendMessage(ws, {
2785
- type: "lint:progress",
2786
- filePath,
2787
- requestId,
2788
- phase: `Done (${fastFiltered.length} issues, ${Date.now() - startedAt}ms)`
2789
- });
2790
- }
2791
- });
2792
- } else {
3091
+ {
2793
3092
  const elapsed = Date.now() - startedAt;
2794
3093
  sendMessage(ws, {
2795
3094
  type: "lint:progress",
@@ -2798,6 +3097,13 @@ async function handleMessage(ws, data) {
2798
3097
  phase: `Done (${fastFiltered.length} issues, ${elapsed}ms)`
2799
3098
  });
2800
3099
  }
3100
+ if (isSemanticRuleEnabled()) {
3101
+ runSemanticAnalysisAsync(filePath, ws, requestId).catch((err) => {
3102
+ logServerError("Async semantic analysis failed", err instanceof Error ? err.message : String(err));
3103
+ });
3104
+ } else {
3105
+ logSemanticSkipped(filePath, "rule not enabled");
3106
+ }
2801
3107
  break;
2802
3108
  }
2803
3109
  case "subscribe:file": {
@@ -2831,125 +3137,11 @@ async function handleMessage(ws, data) {
2831
3137
  break;
2832
3138
  }
2833
3139
  case "vision:analyze": {
2834
- const {
2835
- route,
2836
- timestamp,
2837
- screenshot,
2838
- screenshotFile,
2839
- manifest,
2840
- requestId
2841
- } = message;
2842
- logVisionAnalyze(route, requestId);
2843
- const visionMod = await getVisionModule();
2844
- if (!visionMod) {
2845
- sendMessage(ws, {
2846
- type: "vision:result",
2847
- route,
2848
- issues: [],
2849
- analysisTime: 0,
2850
- error: "uilint-vision is not installed",
2851
- requestId
2852
- });
2853
- break;
2854
- }
2855
- sendMessage(ws, {
2856
- type: "vision:progress",
2857
- route,
2858
- requestId,
2859
- phase: "Starting vision analysis..."
3140
+ const visionMsg = message;
3141
+ logVisionAnalyze(visionMsg.route, visionMsg.requestId);
3142
+ runVisionAnalysisInBackground(ws, visionMsg).catch((err) => {
3143
+ logServerError("Vision analysis failed", err instanceof Error ? err.message : String(err));
2860
3144
  });
2861
- const startedAt = Date.now();
2862
- const analyzer = await getVisionAnalyzerInstance();
2863
- const releaseOllama = await acquireOllamaMutex();
2864
- try {
2865
- const analyzerObj = analyzer;
2866
- const analyzerModel = typeof analyzerObj?.getModel === "function" ? analyzerObj.getModel() : void 0;
2867
- const analyzerBaseUrl = typeof analyzerObj?.getBaseUrl === "function" ? analyzerObj.getBaseUrl() : void 0;
2868
- if (!screenshot) {
2869
- sendMessage(ws, {
2870
- type: "vision:result",
2871
- route,
2872
- issues: [],
2873
- analysisTime: Date.now() - startedAt,
2874
- error: "No screenshot provided for vision analysis",
2875
- requestId
2876
- });
2877
- break;
2878
- }
2879
- const result = await visionMod.runVisionAnalysis({
2880
- imageBase64: screenshot,
2881
- manifest,
2882
- projectPath: serverAppRootForVision,
2883
- baseUrl: analyzerBaseUrl,
2884
- model: analyzerModel,
2885
- analyzer,
2886
- onPhase: (phase) => {
2887
- sendMessage(ws, {
2888
- type: "vision:progress",
2889
- route,
2890
- requestId,
2891
- phase
2892
- });
2893
- },
2894
- pathResolver: resolvePathSpecifier
2895
- });
2896
- if (typeof screenshotFile === "string" && screenshotFile.length > 0) {
2897
- if (isValidScreenshotFilename(screenshotFile)) {
2898
- const screenshotsDir = join3(serverAppRootForVision, ".uilint", "screenshots");
2899
- const imagePath = join3(screenshotsDir, screenshotFile);
2900
- try {
2901
- if (existsSync5(imagePath)) {
2902
- const report = visionMod.writeVisionMarkdownReport({
2903
- imagePath,
2904
- route,
2905
- timestamp,
2906
- visionModel: result.visionModel,
2907
- baseUrl: result.baseUrl,
2908
- analysisTimeMs: result.analysisTime,
2909
- prompt: result.prompt ?? null,
2910
- rawResponse: result.rawResponse ?? null,
2911
- metadata: {
2912
- screenshotFile: parse(imagePath).base,
2913
- appRoot: serverAppRootForVision,
2914
- manifestElements: manifest.length,
2915
- requestId: requestId ?? null
2916
- }
2917
- });
2918
- logServerInfo(`Wrote vision report`, report.outPath);
2919
- }
2920
- } catch (e) {
2921
- logServerWarning(
2922
- `Failed to write vision report for ${screenshotFile}`,
2923
- e instanceof Error ? e.message : String(e)
2924
- );
2925
- }
2926
- }
2927
- }
2928
- const elapsed = Date.now() - startedAt;
2929
- const resultIssues = result.issues;
2930
- logVisionDone(route, resultIssues.length, elapsed);
2931
- sendMessage(ws, {
2932
- type: "vision:result",
2933
- route,
2934
- issues: resultIssues,
2935
- analysisTime: result.analysisTime,
2936
- requestId
2937
- });
2938
- } catch (error) {
2939
- const errorMessage = error instanceof Error ? error.message : String(error);
2940
- const elapsed = Date.now() - startedAt;
2941
- logServerError(`Vision analysis failed for ${route}`, errorMessage);
2942
- sendMessage(ws, {
2943
- type: "vision:result",
2944
- route,
2945
- issues: [],
2946
- analysisTime: elapsed,
2947
- error: errorMessage,
2948
- requestId
2949
- });
2950
- } finally {
2951
- releaseOllama();
2952
- }
2953
3145
  break;
2954
3146
  }
2955
3147
  case "vision:check": {
@@ -3204,13 +3396,20 @@ async function buildDuplicatesIndex(appRoot) {
3204
3396
  return;
3205
3397
  }
3206
3398
  isIndexing = true;
3399
+ setPluginStatus("duplicates", "processing", "Preparing index...");
3207
3400
  startBackgroundTask(
3208
3401
  "duplicates-index",
3209
3402
  "Duplicates Index",
3210
3403
  "Waiting for Ollama..."
3211
3404
  );
3212
- const release = await acquireOllamaMutex();
3405
+ const release = await acquireOllamaMutex("duplicates");
3213
3406
  broadcast({ type: "duplicates:indexing:start" });
3407
+ broadcast({
3408
+ type: "plugin:operation:start",
3409
+ pluginId: "duplicates",
3410
+ operationName: "indexing",
3411
+ message: "Building duplicates index..."
3412
+ });
3214
3413
  try {
3215
3414
  const { indexDirectory } = await import("uilint-duplicates");
3216
3415
  const result = await indexDirectory(appRoot, {
@@ -3223,16 +3422,27 @@ async function buildDuplicatesIndex(appRoot) {
3223
3422
  total,
3224
3423
  message
3225
3424
  );
3425
+ updatePluginProgress("duplicates", progress, current, total, message);
3226
3426
  broadcast({
3227
3427
  type: "duplicates:indexing:progress",
3228
3428
  message,
3229
3429
  current,
3230
3430
  total
3231
3431
  });
3432
+ broadcast({
3433
+ type: "plugin:operation:progress",
3434
+ pluginId: "duplicates",
3435
+ operationName: "indexing",
3436
+ current,
3437
+ total,
3438
+ message
3439
+ });
3232
3440
  }
3233
3441
  });
3234
3442
  const successMsg = `${result.totalChunks} chunks (${result.added} added, ${result.modified} modified, ${result.deleted} deleted) in ${(result.duration / 1e3).toFixed(1)}s`;
3235
3443
  completeBackgroundTask("duplicates-index", `Index complete: ${successMsg}`);
3444
+ setPluginStatus("duplicates", "complete", successMsg);
3445
+ setTimeout(() => setPluginStatus("duplicates", "idle"), 3e3);
3236
3446
  broadcast({
3237
3447
  type: "duplicates:indexing:complete",
3238
3448
  added: result.added,
@@ -3241,10 +3451,24 @@ async function buildDuplicatesIndex(appRoot) {
3241
3451
  totalChunks: result.totalChunks,
3242
3452
  duration: result.duration
3243
3453
  });
3454
+ broadcast({
3455
+ type: "plugin:operation:complete",
3456
+ pluginId: "duplicates",
3457
+ operationName: "indexing",
3458
+ message: successMsg
3459
+ });
3244
3460
  } catch (error) {
3245
3461
  const msg = error instanceof Error ? error.message : String(error);
3246
3462
  completeBackgroundTask("duplicates-index", void 0, msg);
3463
+ setPluginStatus("duplicates", "error", msg);
3464
+ setTimeout(() => setPluginStatus("duplicates", "idle"), 3e3);
3247
3465
  broadcast({ type: "duplicates:indexing:error", error: msg });
3466
+ broadcast({
3467
+ type: "plugin:operation:error",
3468
+ pluginId: "duplicates",
3469
+ operationName: "indexing",
3470
+ error: msg
3471
+ });
3248
3472
  } finally {
3249
3473
  release();
3250
3474
  isIndexing = false;
@@ -3392,6 +3616,9 @@ async function serve(options) {
3392
3616
  const useDashboardUI = process.stdout.isTTY && !options.noDashboard;
3393
3617
  if (useDashboardUI) {
3394
3618
  enableDashboard();
3619
+ registerPlugin("semantic", "Semantic", "qwen3-vl:8b-instruct");
3620
+ registerPlugin("vision", "Vision", "gemma3:4b");
3621
+ registerPlugin("duplicates", "Duplicates", "nomic-embed-text");
3395
3622
  } else {
3396
3623
  disableDashboard();
3397
3624
  }
@@ -3561,7 +3788,7 @@ async function serve(options) {
3561
3788
  });
3562
3789
  setServerRunning(port);
3563
3790
  if (useDashboardUI) {
3564
- const { renderDashboard } = await import("./render-43OMCORR.js");
3791
+ const { renderDashboard } = await import("./render-2P4YWHXV.js");
3565
3792
  const { waitUntilExit } = renderDashboard({
3566
3793
  onQuit: () => {
3567
3794
  clearInterval(pingInterval);
@@ -5704,7 +5931,7 @@ program.command("update").description("Update existing style guide with new styl
5704
5931
  });
5705
5932
  var initCommand = program.command("init").description("Initialize UILint integration").option("--force", "Overwrite existing configuration files").option("--react", "Install React DevTool (non-interactive)").option("--eslint", "Install ESLint rules (non-interactive)").option("--genstyleguide", "Generate styleguide (non-interactive)").option("--skill", "Install Claude skill (non-interactive)");
5706
5933
  program.command("remove").description("Remove UILint components from your project").option("--dry-run", "Preview changes without removing anything").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
5707
- const { removeUI } = await import("./remove-ui-ZHW4GUFL.js");
5934
+ const { removeUI } = await import("./remove-ui-GZRFA2AC.js");
5708
5935
  await removeUI({ dryRun: options.dryRun, yes: options.yes });
5709
5936
  });
5710
5937
  program.command("serve").description("Start WebSocket server for real-time UI linting").option("-p, --port <number>", "Port to listen on", "9234").option("--no-dashboard", "Disable dashboard UI (use simple logging)").action(async (options) => {
@@ -5756,7 +5983,7 @@ program.addCommand(createDuplicatesCommand());
5756
5983
  program.addCommand(createManifestCommand());
5757
5984
  program.addCommand(createSocketCommand());
5758
5985
  program.command("upgrade").description("Update installed ESLint rules to latest versions").option("--check", "Show available updates without applying").option("-y, --yes", "Auto-confirm all updates").option("--dry-run", "Show what would change without modifying files").option("--rule <id>", "Upgrade only a specific rule").action(async (options) => {
5759
- const { upgrade } = await import("./upgrade-TPZ62BT2.js");
5986
+ const { upgrade } = await import("./upgrade-2UKW3SIQ.js");
5760
5987
  await upgrade({
5761
5988
  check: options.check,
5762
5989
  yes: options.yes,
@@ -5765,14 +5992,14 @@ program.command("upgrade").description("Update installed ESLint rules to latest
5765
5992
  });
5766
5993
  });
5767
5994
  async function main() {
5768
- const { discoverPlugins: discoverPlugins2 } = await import("./plugin-loader-O6PNFN6D.js");
5995
+ const { discoverPlugins: discoverPlugins2 } = await import("./plugin-loader-LUIV7MLR.js");
5769
5996
  const pluginManifests = await discoverPlugins2();
5770
5997
  for (const manifest of pluginManifests) {
5771
5998
  initCommand.option(`--${manifest.cliFlag}`, manifest.cliDescription);
5772
5999
  }
5773
6000
  initCommand.action(async (options) => {
5774
6001
  const plugins = pluginManifests.filter((m) => options[m.cliFlag]).map((m) => m.cliFlag);
5775
- const { initUI } = await import("./init-ui-KJYYI5DH.js");
6002
+ const { initUI } = await import("./init-ui-QQXIZSKI.js");
5776
6003
  await initUI({
5777
6004
  force: options.force,
5778
6005
  react: options.react,