uilint 0.2.108 → 0.2.110

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,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getDashboardStore
4
+ } from "./chunk-6CC356LV.js";
2
5
  import {
3
6
  detectCoverageSetup,
4
7
  detectNextAppRouter,
@@ -1604,6 +1607,190 @@ function writeVisionMarkdownReport(args) {
1604
1607
  return { outPath, content };
1605
1608
  }
1606
1609
 
1610
+ // src/commands/serve/dashboard/logger.ts
1611
+ import pc2 from "picocolors";
1612
+ function consoleInfo(message) {
1613
+ console.log(pc2.blue("i") + " " + message);
1614
+ }
1615
+ function consoleSuccess(message) {
1616
+ console.log(pc2.green("\u2713") + " " + message);
1617
+ }
1618
+ function consoleWarning(message) {
1619
+ console.log(pc2.yellow("\u26A0") + " " + message);
1620
+ }
1621
+ function consoleError(message) {
1622
+ console.log(pc2.red("\u2717") + " " + message);
1623
+ }
1624
+ var useDashboard = false;
1625
+ function enableDashboard() {
1626
+ useDashboard = true;
1627
+ }
1628
+ function disableDashboard() {
1629
+ useDashboard = false;
1630
+ }
1631
+ function isDashboardEnabled() {
1632
+ return useDashboard;
1633
+ }
1634
+ function logActivity(type, message, detail, isError, isWarning) {
1635
+ if (useDashboard) {
1636
+ const store = getDashboardStore();
1637
+ store.addActivity({ type, message, detail, isError, isWarning });
1638
+ } else {
1639
+ const prefix = `${pc2.dim("[ws]")} `;
1640
+ if (isError) {
1641
+ consoleError(prefix + message + (detail ? `
1642
+ ${detail}` : ""));
1643
+ } else if (isWarning) {
1644
+ consoleWarning(prefix + message + (detail ? `
1645
+ ${detail}` : ""));
1646
+ } else {
1647
+ consoleInfo(prefix + message + (detail ? `
1648
+ ${detail}` : ""));
1649
+ }
1650
+ }
1651
+ }
1652
+ function logLint(filePath, requestId) {
1653
+ const msg = filePath + (requestId ? ` (req ${requestId})` : "");
1654
+ logActivity("lint:file", msg);
1655
+ }
1656
+ function logLintDone(filePath, issueCount, elapsedMs) {
1657
+ logActivity("lint:done", `${filePath} \u2192 ${issueCount} issue(s) (${elapsedMs}ms)`);
1658
+ }
1659
+ function logSubscribe(filePath) {
1660
+ logActivity("subscribe", filePath);
1661
+ }
1662
+ function logCacheInvalidate(filePath) {
1663
+ logActivity("cache:invalidate", filePath ?? "(all)");
1664
+ }
1665
+ function logVisionAnalyze(route, requestId) {
1666
+ const msg = route + (requestId ? ` (req ${requestId})` : "");
1667
+ logActivity("vision:analyze", msg);
1668
+ }
1669
+ function logVisionDone(route, issueCount, elapsedMs) {
1670
+ logActivity("vision:done", `${route} \u2192 ${issueCount} issue(s) (${elapsedMs}ms)`);
1671
+ }
1672
+ function logVisionCheck(requestId) {
1673
+ logActivity("vision:check", requestId ? `(req ${requestId})` : "");
1674
+ }
1675
+ function logConfigSet(key, value) {
1676
+ logActivity("config:set", `${key} = ${JSON.stringify(value)}`);
1677
+ }
1678
+ function logRuleConfigSet(ruleId, severity, hasOptions) {
1679
+ logActivity(
1680
+ "rule:config:set",
1681
+ `${ruleId} \u2192 ${severity}${hasOptions ? " (with options)" : ""}`
1682
+ );
1683
+ }
1684
+ function logScreenshotSave(route, requestId) {
1685
+ const msg = route + (requestId ? ` (req ${requestId})` : "");
1686
+ logActivity("screenshot:save", msg);
1687
+ }
1688
+ function logScreenshotSaved(filename, sizeKb) {
1689
+ logActivity("screenshot:saved", `${filename} (${sizeKb}kb)`);
1690
+ }
1691
+ function logCoverageResult(fileCount) {
1692
+ logActivity("coverage:result", `${fileCount} files`);
1693
+ }
1694
+ function logClientConnect(totalClients) {
1695
+ if (useDashboard) {
1696
+ const store = getDashboardStore();
1697
+ store.incrementClients();
1698
+ store.addActivity({ type: "client:connect", message: `(${totalClients} total)` });
1699
+ } else {
1700
+ consoleInfo(`Client connected (${totalClients} total)`);
1701
+ }
1702
+ }
1703
+ function logClientDisconnect(totalClients) {
1704
+ if (useDashboard) {
1705
+ const store = getDashboardStore();
1706
+ store.decrementClients();
1707
+ store.addActivity({ type: "client:disconnect", message: `(${totalClients} total)` });
1708
+ } else {
1709
+ consoleInfo(`Client disconnected (${totalClients} total)`);
1710
+ }
1711
+ }
1712
+ function logServerError(message, detail) {
1713
+ logActivity("error", message, detail, true);
1714
+ }
1715
+ function logServerWarning(message, detail) {
1716
+ logActivity("warning", message, detail, false, true);
1717
+ }
1718
+ function logServerInfo(message, detail) {
1719
+ logActivity("info", message, detail);
1720
+ }
1721
+ function setWorkspaceInfo(workspaceRoot, appRoot, serverCwd) {
1722
+ if (useDashboard) {
1723
+ const store = getDashboardStore();
1724
+ store.setWorkspace({ workspaceRoot, appRoot, serverCwd });
1725
+ } else {
1726
+ consoleInfo(`Workspace root: ${pc2.dim(workspaceRoot)}`);
1727
+ consoleInfo(`App root: ${pc2.dim(appRoot)}`);
1728
+ consoleInfo(`Server cwd: ${pc2.dim(serverCwd)}`);
1729
+ }
1730
+ }
1731
+ function setServerRunning(port) {
1732
+ if (useDashboard) {
1733
+ const store = getDashboardStore();
1734
+ store.setPort(port);
1735
+ store.setRunning(true);
1736
+ } else {
1737
+ consoleSuccess(
1738
+ `UILint WebSocket server running on ${pc2.cyan(`ws://localhost:${port}`)}`
1739
+ );
1740
+ consoleInfo("Press Ctrl+C to stop");
1741
+ }
1742
+ }
1743
+ function updateSubscriptionCount(count) {
1744
+ if (useDashboard) {
1745
+ const store = getDashboardStore();
1746
+ store.updateStats({ subscriptions: count });
1747
+ }
1748
+ }
1749
+ function updateCacheCount(count) {
1750
+ if (useDashboard) {
1751
+ const store = getDashboardStore();
1752
+ store.setCacheEntries(count);
1753
+ }
1754
+ }
1755
+ function startBackgroundTask(id, name, message) {
1756
+ if (useDashboard) {
1757
+ const store = getDashboardStore();
1758
+ store.setBackgroundTask({
1759
+ id,
1760
+ name,
1761
+ status: "running",
1762
+ progress: 0,
1763
+ message
1764
+ });
1765
+ } else {
1766
+ consoleInfo(`${pc2.blue(name)}...`);
1767
+ }
1768
+ }
1769
+ function updateBackgroundTaskProgress(id, progress, current, total, message) {
1770
+ if (useDashboard) {
1771
+ const store = getDashboardStore();
1772
+ store.updateBackgroundTaskProgress(id, progress, current, total, message);
1773
+ } else if (message) {
1774
+ if (current !== void 0 && total !== void 0) {
1775
+ consoleInfo(` ${message} (${current}/${total})`);
1776
+ } else {
1777
+ consoleInfo(` ${message}`);
1778
+ }
1779
+ }
1780
+ }
1781
+ function completeBackgroundTask(id, successMessage, error) {
1782
+ if (useDashboard) {
1783
+ const store = getDashboardStore();
1784
+ store.completeBackgroundTask(id, error);
1785
+ } else {
1786
+ if (error) {
1787
+ consoleError(`Failed: ${error}`);
1788
+ } else if (successMessage) {
1789
+ consoleSuccess(successMessage);
1790
+ }
1791
+ }
1792
+ }
1793
+
1607
1794
  // src/commands/serve.ts
1608
1795
  import { ruleRegistry } from "uilint-eslint";
1609
1796
  function pickAppRoot(params) {
@@ -1916,25 +2103,17 @@ async function handleMessage(ws, data) {
1916
2103
  if (message.type === "lint:file" || message.type === "lint:element") {
1917
2104
  const fp = message.filePath;
1918
2105
  const rid = message.requestId;
1919
- logInfo(
1920
- `${pc.dim("[ws]")} ${pc.bold(message.type)} ${pc.dim(fp ?? "")}${rid ? ` ${pc.dim(`(req ${rid})`)}` : ""}`
1921
- );
2106
+ logLint(fp ?? "", rid);
1922
2107
  } else if (message.type === "subscribe:file") {
1923
- logInfo(`${pc.dim("[ws]")} subscribe:file ${pc.dim(message.filePath)}`);
2108
+ logSubscribe(message.filePath);
1924
2109
  } else if (message.type === "cache:invalidate") {
1925
- logInfo(
1926
- `${pc.dim("[ws]")} cache:invalidate ${pc.dim(
1927
- message.filePath ?? "(all)"
1928
- )}`
1929
- );
2110
+ logCacheInvalidate(message.filePath);
1930
2111
  } else if (message.type === "vision:analyze") {
1931
2112
  } else if (message.type === "vision:check") {
1932
2113
  } else if (message.type === "config:set") {
1933
2114
  } else if (message.type === "screenshot:save") {
1934
2115
  const rid = message.requestId;
1935
- logInfo(
1936
- `${pc.dim("[ws]")} ${pc.bold("screenshot:save")} ${pc.dim(message.route)}${rid ? ` ${pc.dim(`(req ${rid})`)}` : ""}`
1937
- );
2116
+ logScreenshotSave(message.route, rid);
1938
2117
  }
1939
2118
  switch (message.type) {
1940
2119
  case "lint:file": {
@@ -1950,28 +2129,17 @@ async function handleMessage(ws, data) {
1950
2129
  if (!existsSync5(resolved)) {
1951
2130
  const cwd = process.cwd();
1952
2131
  const wsRoot = findWorkspaceRoot4(cwd);
1953
- logWarning(
1954
- [
1955
- `${pc.dim("[ws]")} File not found for request`,
1956
- ` filePath: ${pc.dim(filePath)}`,
1957
- ` resolved: ${pc.dim(resolved)}`,
1958
- ` cwd: ${pc.dim(cwd)}`,
1959
- ` wsRoot: ${pc.dim(wsRoot)}`,
1960
- ` hint: In monorepos, ensure paths like ${pc.dim(
1961
- "app/page.tsx"
1962
- )} exist under ${pc.dim("apps/*/")} or use absolute paths.`
1963
- ].join("\n")
2132
+ logServerWarning(
2133
+ `File not found: ${filePath}`,
2134
+ `resolved: ${resolved}, cwd: ${cwd}, wsRoot: ${wsRoot}`
1964
2135
  );
1965
2136
  }
1966
2137
  const issues = await lintFile(filePath, (phase) => {
1967
2138
  sendMessage(ws, { type: "lint:progress", filePath, requestId, phase });
1968
2139
  });
1969
2140
  const elapsed = Date.now() - startedAt;
1970
- logInfo(
1971
- `${pc.dim("[ws]")} lint:file done ${pc.dim(filePath)} \u2192 ${pc.bold(
1972
- `${issues.length}`
1973
- )} issue(s) ${pc.dim(`(${elapsed}ms)`)}`
1974
- );
2141
+ logLintDone(filePath, issues.length, elapsed);
2142
+ updateCacheCount(cache.size);
1975
2143
  sendMessage(ws, {
1976
2144
  type: "lint:progress",
1977
2145
  filePath,
@@ -2023,6 +2191,7 @@ async function handleMessage(ws, data) {
2023
2191
  }
2024
2192
  }
2025
2193
  subscribers.add({ ws, clientFilePath: filePath });
2194
+ updateSubscriptionCount(subscriptions.size);
2026
2195
  break;
2027
2196
  }
2028
2197
  case "cache:invalidate": {
@@ -2033,6 +2202,7 @@ async function handleMessage(ws, data) {
2033
2202
  } else {
2034
2203
  cache.clear();
2035
2204
  }
2205
+ updateCacheCount(cache.size);
2036
2206
  break;
2037
2207
  }
2038
2208
  case "vision:analyze": {
@@ -2044,9 +2214,7 @@ async function handleMessage(ws, data) {
2044
2214
  manifest,
2045
2215
  requestId
2046
2216
  } = message;
2047
- logInfo(
2048
- `${pc.dim("[ws]")} ${pc.bold("vision:analyze")} ${pc.dim(route)}${requestId ? ` ${pc.dim(`(req ${requestId})`)}` : ""}`
2049
- );
2217
+ logVisionAnalyze(route, requestId);
2050
2218
  sendMessage(ws, {
2051
2219
  type: "vision:progress",
2052
2220
  route,
@@ -2059,20 +2227,22 @@ async function handleMessage(ws, data) {
2059
2227
  const screenshotBytes = typeof screenshot === "string" ? Buffer.byteLength(screenshot) : 0;
2060
2228
  const analyzerModel = typeof analyzer.getModel === "function" ? analyzer.getModel() : void 0;
2061
2229
  const analyzerBaseUrl = typeof analyzer.getBaseUrl === "function" ? analyzer.getBaseUrl() : void 0;
2062
- logInfo(
2063
- [
2064
- `${pc.dim("[ws]")} ${pc.dim("vision")} details`,
2065
- ` route: ${pc.dim(route)}`,
2066
- ` requestId: ${pc.dim(requestId ?? "(none)")}`,
2067
- ` manifest: ${pc.dim(String(manifest.length))} element(s)`,
2068
- ` screenshot: ${pc.dim(
2069
- screenshot ? `${Math.round(screenshotBytes / 1024)}kb` : "none"
2070
- )}`,
2071
- ` screenshotFile: ${pc.dim(screenshotFile ?? "(none)")}`,
2072
- ` ollamaUrl: ${pc.dim(analyzerBaseUrl ?? "(default)")}`,
2073
- ` visionModel: ${pc.dim(analyzerModel ?? "(default)")}`
2074
- ].join("\n")
2075
- );
2230
+ if (!isDashboardEnabled()) {
2231
+ logInfo(
2232
+ [
2233
+ `${pc.dim("[ws]")} ${pc.dim("vision")} details`,
2234
+ ` route: ${pc.dim(route)}`,
2235
+ ` requestId: ${pc.dim(requestId ?? "(none)")}`,
2236
+ ` manifest: ${pc.dim(String(manifest.length))} element(s)`,
2237
+ ` screenshot: ${pc.dim(
2238
+ screenshot ? `${Math.round(screenshotBytes / 1024)}kb` : "none"
2239
+ )}`,
2240
+ ` screenshotFile: ${pc.dim(screenshotFile ?? "(none)")}`,
2241
+ ` ollamaUrl: ${pc.dim(analyzerBaseUrl ?? "(default)")}`,
2242
+ ` visionModel: ${pc.dim(analyzerModel ?? "(default)")}`
2243
+ ].join("\n")
2244
+ );
2245
+ }
2076
2246
  if (!screenshot) {
2077
2247
  sendMessage(ws, {
2078
2248
  type: "vision:result",
@@ -2103,10 +2273,9 @@ async function handleMessage(ws, data) {
2103
2273
  });
2104
2274
  if (typeof screenshotFile === "string" && screenshotFile.length > 0) {
2105
2275
  if (!isValidScreenshotFilename(screenshotFile)) {
2106
- logWarning(
2107
- `Skipping vision report write: invalid screenshotFile ${pc.dim(
2108
- screenshotFile
2109
- )}`
2276
+ logServerWarning(
2277
+ `Skipping vision report write: invalid screenshotFile`,
2278
+ screenshotFile
2110
2279
  );
2111
2280
  } else {
2112
2281
  const screenshotsDir = join3(
@@ -2117,10 +2286,9 @@ async function handleMessage(ws, data) {
2117
2286
  const imagePath = join3(screenshotsDir, screenshotFile);
2118
2287
  try {
2119
2288
  if (!existsSync5(imagePath)) {
2120
- logWarning(
2121
- `Skipping vision report write: screenshot file not found ${pc.dim(
2122
- imagePath
2123
- )}`
2289
+ logServerWarning(
2290
+ `Skipping vision report write: screenshot file not found`,
2291
+ imagePath
2124
2292
  );
2125
2293
  } else {
2126
2294
  const report = writeVisionMarkdownReport({
@@ -2139,32 +2307,18 @@ async function handleMessage(ws, data) {
2139
2307
  requestId: requestId ?? null
2140
2308
  }
2141
2309
  });
2142
- logInfo(
2143
- `${pc.dim("[ws]")} wrote vision report ${pc.dim(
2144
- report.outPath
2145
- )}`
2146
- );
2310
+ logServerInfo(`Wrote vision report`, report.outPath);
2147
2311
  }
2148
2312
  } catch (e) {
2149
- logWarning(
2150
- `Failed to write vision report for ${pc.dim(screenshotFile)}: ${e instanceof Error ? e.message : String(e)}`
2313
+ logServerWarning(
2314
+ `Failed to write vision report for ${screenshotFile}`,
2315
+ e instanceof Error ? e.message : String(e)
2151
2316
  );
2152
2317
  }
2153
2318
  }
2154
2319
  }
2155
2320
  const elapsed = Date.now() - startedAt;
2156
- logInfo(
2157
- `${pc.dim("[ws]")} vision:analyze done ${pc.dim(route)} \u2192 ${pc.bold(
2158
- `${result.issues.length}`
2159
- )} issue(s) ${pc.dim(`(${elapsed}ms)`)}`
2160
- );
2161
- if (result.rawResponse) {
2162
- logInfo(
2163
- `${pc.dim("[ws]")} vision rawResponse ${pc.dim(
2164
- `${result.rawResponse.length} chars`
2165
- )}`
2166
- );
2167
- }
2321
+ logVisionDone(route, result.issues.length, elapsed);
2168
2322
  sendMessage(ws, {
2169
2323
  type: "vision:result",
2170
2324
  route,
@@ -2174,16 +2328,9 @@ async function handleMessage(ws, data) {
2174
2328
  });
2175
2329
  } catch (error) {
2176
2330
  const errorMessage = error instanceof Error ? error.message : String(error);
2177
- const stack = error instanceof Error ? error.stack : void 0;
2178
- logError(
2179
- [
2180
- `Vision analysis failed`,
2181
- ` route: ${route}`,
2182
- ` requestId: ${requestId ?? "(none)"}`,
2183
- ` error: ${errorMessage}`,
2184
- stack ? ` stack:
2185
- ${stack}` : ""
2186
- ].filter(Boolean).join("\n")
2331
+ logServerError(
2332
+ `Vision analysis failed for ${route}`,
2333
+ errorMessage
2187
2334
  );
2188
2335
  sendMessage(ws, {
2189
2336
  type: "vision:result",
@@ -2198,9 +2345,7 @@ ${stack}` : ""
2198
2345
  }
2199
2346
  case "vision:check": {
2200
2347
  const { requestId } = message;
2201
- logInfo(
2202
- `${pc.dim("[ws]")} ${pc.bold("vision:check")}${requestId ? ` ${pc.dim(`(req ${requestId})`)}` : ""}`
2203
- );
2348
+ logVisionCheck(requestId);
2204
2349
  try {
2205
2350
  const analyzer = getVisionAnalyzerInstance();
2206
2351
  const model = typeof analyzer.getModel === "function" ? analyzer.getModel() : void 0;
@@ -2276,7 +2421,7 @@ ${stack}` : ""
2276
2421
  break;
2277
2422
  }
2278
2423
  const coverageData = JSON.parse(readFileSync(coveragePath, "utf-8"));
2279
- logInfo(`${pc.dim("[ws]")} coverage:result ${pc.dim(`${Object.keys(coverageData).length} files`)}`);
2424
+ logCoverageResult(Object.keys(coverageData).length);
2280
2425
  sendMessage(ws, {
2281
2426
  type: "coverage:result",
2282
2427
  coverage: coverageData,
@@ -2285,7 +2430,7 @@ ${stack}` : ""
2285
2430
  });
2286
2431
  } catch (error) {
2287
2432
  const errorMessage = error instanceof Error ? error.message : String(error);
2288
- logError(`${pc.dim("[ws]")} coverage:error ${errorMessage}`);
2433
+ logServerError(`coverage:error`, errorMessage);
2289
2434
  sendMessage(ws, {
2290
2435
  type: "coverage:error",
2291
2436
  error: errorMessage,
@@ -2341,11 +2486,7 @@ ${stack}` : ""
2341
2486
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
2342
2487
  };
2343
2488
  writeFileSync4(sidecarPath, JSON.stringify(sidecarData, null, 2));
2344
- logInfo(
2345
- `${pc.dim("[ws]")} screenshot:saved ${pc.dim(filename)} ${pc.dim(
2346
- `(${Math.round(imageBuffer.length / 1024)}kb)`
2347
- )}`
2348
- );
2489
+ logScreenshotSaved(filename, Math.round(imageBuffer.length / 1024));
2349
2490
  sendMessage(ws, {
2350
2491
  type: "screenshot:saved",
2351
2492
  filename,
@@ -2354,7 +2495,7 @@ ${stack}` : ""
2354
2495
  });
2355
2496
  } catch (error) {
2356
2497
  const errorMessage = error instanceof Error ? error.message : String(error);
2357
- logError(`${pc.dim("[ws]")} screenshot:error ${errorMessage}`);
2498
+ logServerError(`screenshot:error`, errorMessage);
2358
2499
  sendMessage(ws, {
2359
2500
  type: "screenshot:error",
2360
2501
  error: errorMessage,
@@ -2379,6 +2520,7 @@ function handleDisconnect(ws) {
2379
2520
  }
2380
2521
  }
2381
2522
  }
2523
+ updateSubscriptionCount(subscriptions.size);
2382
2524
  }
2383
2525
  function handleFileChange(filePath) {
2384
2526
  const subscribers = subscriptions.get(filePath);
@@ -2391,7 +2533,7 @@ function handleFileChange(filePath) {
2391
2533
  function handleCoverageFileChange(filePath) {
2392
2534
  try {
2393
2535
  const coverageData = JSON.parse(readFileSync(filePath, "utf-8"));
2394
- logInfo(`${pc.dim("[ws]")} coverage:changed ${pc.dim(`${Object.keys(coverageData).length} files`)}`);
2536
+ logCoverageResult(Object.keys(coverageData).length);
2395
2537
  broadcast({
2396
2538
  type: "coverage:result",
2397
2539
  coverage: coverageData,
@@ -2399,7 +2541,7 @@ function handleCoverageFileChange(filePath) {
2399
2541
  });
2400
2542
  } catch (error) {
2401
2543
  const errorMessage = error instanceof Error ? error.message : String(error);
2402
- logError(`${pc.dim("[ws]")} Failed to read coverage data: ${errorMessage}`);
2544
+ logServerError(`Failed to read coverage data`, errorMessage);
2403
2545
  broadcast({
2404
2546
  type: "coverage:error",
2405
2547
  error: `Failed to read coverage: ${errorMessage}`
@@ -2416,7 +2558,7 @@ function broadcastConfigUpdate(key, value) {
2416
2558
  }
2417
2559
  function handleConfigSet(key, value) {
2418
2560
  configStore.set(key, value);
2419
- logInfo(`${pc.dim("[ws]")} config:set ${pc.bold(key)} = ${pc.dim(JSON.stringify(value))}`);
2561
+ logConfigSet(key, value);
2420
2562
  broadcastConfigUpdate(key, value);
2421
2563
  }
2422
2564
  function broadcastRuleConfigChange(ruleId, severity, options) {
@@ -2443,17 +2585,14 @@ async function buildDuplicatesIndex(appRoot) {
2443
2585
  return;
2444
2586
  }
2445
2587
  isIndexing = true;
2446
- logInfo(`${pc.blue("Building duplicates index...")}`);
2588
+ startBackgroundTask("duplicates-index", "Duplicates Index", "Starting...");
2447
2589
  broadcast({ type: "duplicates:indexing:start" });
2448
2590
  try {
2449
2591
  const { indexDirectory } = await import("uilint-duplicates");
2450
2592
  const result = await indexDirectory(appRoot, {
2451
2593
  onProgress: (message, current, total) => {
2452
- if (current !== void 0 && total !== void 0) {
2453
- logInfo(` ${message} (${current}/${total})`);
2454
- } else {
2455
- logInfo(` ${message}`);
2456
- }
2594
+ const progress = total && total > 0 ? Math.round((current || 0) / total * 100) : 0;
2595
+ updateBackgroundTaskProgress("duplicates-index", progress, current, total, message);
2457
2596
  broadcast({
2458
2597
  type: "duplicates:indexing:progress",
2459
2598
  message,
@@ -2462,9 +2601,8 @@ async function buildDuplicatesIndex(appRoot) {
2462
2601
  });
2463
2602
  }
2464
2603
  });
2465
- logSuccess(
2466
- `${pc.green("Index complete:")} ${result.totalChunks} chunks (${result.added} added, ${result.modified} modified, ${result.deleted} deleted) in ${(result.duration / 1e3).toFixed(1)}s`
2467
- );
2604
+ const successMsg = `${result.totalChunks} chunks (${result.added} added, ${result.modified} modified, ${result.deleted} deleted) in ${(result.duration / 1e3).toFixed(1)}s`;
2605
+ completeBackgroundTask("duplicates-index", `Index complete: ${successMsg}`);
2468
2606
  broadcast({
2469
2607
  type: "duplicates:indexing:complete",
2470
2608
  added: result.added,
@@ -2475,7 +2613,7 @@ async function buildDuplicatesIndex(appRoot) {
2475
2613
  });
2476
2614
  } catch (error) {
2477
2615
  const msg = error instanceof Error ? error.message : String(error);
2478
- logError(`Index failed: ${msg}`);
2616
+ completeBackgroundTask("duplicates-index", void 0, msg);
2479
2617
  broadcast({ type: "duplicates:indexing:error", error: msg });
2480
2618
  } finally {
2481
2619
  isIndexing = false;
@@ -2488,7 +2626,7 @@ function scheduleReindex(appRoot, filePath) {
2488
2626
  reindexTimeout = setTimeout(async () => {
2489
2627
  const count = pendingIndexChanges.size;
2490
2628
  pendingIndexChanges.clear();
2491
- logInfo(`${pc.dim(`[index] ${count} file(s) changed, updating index...`)}`);
2629
+ logServerInfo(`${count} file(s) changed, updating index...`);
2492
2630
  await buildDuplicatesIndex(appRoot);
2493
2631
  }, 2e3);
2494
2632
  }
@@ -2506,15 +2644,15 @@ async function buildCoverageData(appRoot) {
2506
2644
  isPreparingCoverage = true;
2507
2645
  try {
2508
2646
  if (!isCoverageRuleEnabled(appRoot)) {
2509
- logInfo(`${pc.dim("Coverage rule not enabled, skipping preparation")}`);
2647
+ logServerInfo("Coverage rule not enabled, skipping preparation");
2510
2648
  return;
2511
2649
  }
2512
2650
  const setup = detectCoverageSetup(appRoot);
2513
2651
  if (!needsCoveragePreparation(setup)) {
2514
- logInfo(`${pc.dim("Coverage data is up-to-date")}`);
2652
+ logServerInfo("Coverage data is up-to-date");
2515
2653
  return;
2516
2654
  }
2517
- logInfo(`${pc.blue("Preparing coverage data...")}`);
2655
+ startBackgroundTask("coverage-prep", "Coverage Prep", "Starting...");
2518
2656
  broadcast({ type: "coverage:setup:start" });
2519
2657
  const skipPackageInstall = process.env.UILINT_SKIP_COVERAGE_INSTALL === "1";
2520
2658
  const skipTests = process.env.UILINT_SKIP_COVERAGE_TESTS === "1";
@@ -2523,20 +2661,21 @@ async function buildCoverageData(appRoot) {
2523
2661
  skipPackageInstall,
2524
2662
  skipTests,
2525
2663
  onProgress: (message, phase) => {
2526
- logInfo(` ${message}`);
2664
+ updateBackgroundTaskProgress("coverage-prep", 50, void 0, void 0, message);
2527
2665
  broadcast({ type: "coverage:setup:progress", message, phase });
2528
2666
  }
2529
2667
  });
2530
2668
  if (result.error) {
2531
- logWarning(`Coverage preparation completed with errors: ${result.error}`);
2669
+ completeBackgroundTask("coverage-prep", void 0, result.error);
2532
2670
  } else {
2533
2671
  const parts = [];
2534
2672
  if (result.packageAdded) parts.push("package installed");
2535
2673
  if (result.configModified) parts.push("config modified");
2536
2674
  if (result.testsRan) parts.push("tests ran");
2537
2675
  if (result.coverageGenerated) parts.push("coverage generated");
2538
- logSuccess(
2539
- `${pc.green("Coverage prepared:")} ${parts.join(", ")} in ${(result.duration / 1e3).toFixed(1)}s`
2676
+ completeBackgroundTask(
2677
+ "coverage-prep",
2678
+ `Coverage prepared: ${parts.join(", ")} in ${(result.duration / 1e3).toFixed(1)}s`
2540
2679
  );
2541
2680
  }
2542
2681
  broadcast({
@@ -2545,20 +2684,18 @@ async function buildCoverageData(appRoot) {
2545
2684
  });
2546
2685
  } catch (error) {
2547
2686
  const msg = error instanceof Error ? error.message : String(error);
2548
- logError(`Coverage preparation failed: ${msg}`);
2687
+ completeBackgroundTask("coverage-prep", void 0, msg);
2549
2688
  broadcast({ type: "coverage:setup:error", error: msg });
2550
2689
  } finally {
2551
2690
  isPreparingCoverage = false;
2552
2691
  }
2553
2692
  }
2554
2693
  function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2555
- logInfo(
2556
- `${pc.dim("[ws]")} rule:config:set ${pc.bold(ruleId)} -> ${pc.dim(severity)}${options ? ` with options` : ""}`
2557
- );
2694
+ logRuleConfigSet(ruleId, severity, !!options);
2558
2695
  const configPath = findEslintConfigFile(serverAppRootForVision);
2559
2696
  if (!configPath) {
2560
2697
  const error = `No ESLint config file found in ${serverAppRootForVision}`;
2561
- logError(`${pc.dim("[ws]")} ${error}`);
2698
+ logServerError(error);
2562
2699
  sendMessage(ws, {
2563
2700
  type: "rule:config:result",
2564
2701
  ruleId,
@@ -2577,11 +2714,10 @@ function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2577
2714
  result = updateRuleSeverityInConfig(configPath, ruleId, severity);
2578
2715
  }
2579
2716
  if (result.success) {
2580
- logSuccess(
2581
- `${pc.dim("[ws]")} Updated ${pc.bold(`uilint/${ruleId}`)} -> ${pc.dim(severity)}`
2582
- );
2717
+ logServerInfo(`Updated uilint/${ruleId} -> ${severity}`);
2583
2718
  eslintInstances.clear();
2584
2719
  cache.clear();
2720
+ updateCacheCount(0);
2585
2721
  sendMessage(ws, {
2586
2722
  type: "rule:config:result",
2587
2723
  ruleId,
@@ -2592,7 +2728,7 @@ function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2592
2728
  });
2593
2729
  broadcastRuleConfigChange(ruleId, severity, options);
2594
2730
  } else {
2595
- logError(`${pc.dim("[ws]")} Failed to update rule: ${result.error}`);
2731
+ logServerError(`Failed to update rule: ${result.error}`);
2596
2732
  sendMessage(ws, {
2597
2733
  type: "rule:config:result",
2598
2734
  ruleId,
@@ -2606,13 +2742,17 @@ function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2606
2742
  }
2607
2743
  async function serve(options) {
2608
2744
  const port = options.port || 9234;
2745
+ const useDashboardUI = process.stdout.isTTY && !options.noDashboard;
2746
+ if (useDashboardUI) {
2747
+ enableDashboard();
2748
+ } else {
2749
+ disableDashboard();
2750
+ }
2609
2751
  const cwd = process.cwd();
2610
2752
  const wsRoot = findWorkspaceRoot4(cwd);
2611
2753
  const appRoot = pickAppRoot({ cwd, workspaceRoot: wsRoot });
2612
2754
  serverAppRootForVision = appRoot;
2613
- logInfo(`Workspace root: ${pc.dim(wsRoot)}`);
2614
- logInfo(`App root: ${pc.dim(appRoot)}`);
2615
- logInfo(`Server cwd: ${pc.dim(cwd)}`);
2755
+ setWorkspaceInfo(wsRoot, appRoot, cwd);
2616
2756
  fileWatcher = watch([], {
2617
2757
  persistent: true,
2618
2758
  ignoreInitial: true
@@ -2629,19 +2769,19 @@ async function serve(options) {
2629
2769
  const coveragePath = join3(appRoot, "coverage", "coverage-final.json");
2630
2770
  if (existsSync5(coveragePath)) {
2631
2771
  fileWatcher.add(coveragePath);
2632
- logInfo(`Watching coverage: ${pc.dim(coveragePath)}`);
2772
+ logServerInfo(`Watching coverage`, coveragePath);
2633
2773
  }
2634
2774
  const wss = new WebSocketServer({ port });
2635
2775
  buildDuplicatesIndex(appRoot).catch((err) => {
2636
- logError(`Failed to build duplicates index: ${err.message}`);
2776
+ logServerError(`Failed to build duplicates index`, err.message);
2637
2777
  });
2638
2778
  buildCoverageData(appRoot).catch((err) => {
2639
- logWarning(`Failed to prepare coverage: ${err.message}`);
2779
+ logServerWarning(`Failed to prepare coverage`, err.message);
2640
2780
  });
2641
2781
  wss.on("connection", (ws) => {
2642
2782
  connectedClients += 1;
2643
2783
  connectedClientsSet.add(ws);
2644
- logInfo(`Client connected (${connectedClients} total)`);
2784
+ logClientConnect(connectedClients);
2645
2785
  sendMessage(ws, {
2646
2786
  type: "workspace:info",
2647
2787
  appRoot,
@@ -2654,7 +2794,7 @@ async function serve(options) {
2654
2794
  postToolUseHook: hookInfo
2655
2795
  });
2656
2796
  if (hookInfo.enabled) {
2657
- logInfo(`${pc.dim("[ws]")} Post-tool-use hook detected: ${pc.bold(hookInfo.provider)}`);
2797
+ logServerInfo(`Post-tool-use hook detected: ${hookInfo.provider}`);
2658
2798
  }
2659
2799
  const eslintConfigPath = findEslintConfigFile(appRoot);
2660
2800
  const currentRuleConfigs = eslintConfigPath ? readRuleConfigsFromConfig(eslintConfigPath) : /* @__PURE__ */ new Map();
@@ -2685,28 +2825,39 @@ async function serve(options) {
2685
2825
  ws.on("close", () => {
2686
2826
  connectedClients = Math.max(0, connectedClients - 1);
2687
2827
  connectedClientsSet.delete(ws);
2688
- logInfo(`Client disconnected (${connectedClients} total)`);
2828
+ logClientDisconnect(connectedClients);
2689
2829
  handleDisconnect(ws);
2690
2830
  });
2691
2831
  ws.on("error", (error) => {
2692
- logError(`WebSocket error: ${error.message}`);
2832
+ logServerError(`WebSocket error`, error.message);
2693
2833
  });
2694
2834
  });
2695
2835
  wss.on("error", (error) => {
2696
- logError(`Server error: ${error.message}`);
2836
+ logServerError(`Server error`, error.message);
2697
2837
  });
2698
- logSuccess(
2699
- `UILint WebSocket server running on ${pc.cyan(`ws://localhost:${port}`)}`
2700
- );
2701
- logInfo("Press Ctrl+C to stop");
2702
- await new Promise((resolve11) => {
2703
- process.on("SIGINT", () => {
2704
- logInfo("Shutting down...");
2705
- wss.close();
2706
- fileWatcher?.close();
2707
- resolve11();
2838
+ setServerRunning(port);
2839
+ if (useDashboardUI) {
2840
+ const { renderDashboard } = await import("./render-KO44SVF3.js");
2841
+ const { waitUntilExit } = renderDashboard({
2842
+ onQuit: () => {
2843
+ wss.close();
2844
+ fileWatcher?.close();
2845
+ },
2846
+ onRebuildIndex: () => {
2847
+ buildDuplicatesIndex(appRoot);
2848
+ }
2708
2849
  });
2709
- });
2850
+ await waitUntilExit();
2851
+ } else {
2852
+ await new Promise((resolve11) => {
2853
+ process.on("SIGINT", () => {
2854
+ logServerInfo("Shutting down...");
2855
+ wss.close();
2856
+ fileWatcher?.close();
2857
+ resolve11();
2858
+ });
2859
+ });
2860
+ }
2710
2861
  }
2711
2862
 
2712
2863
  // src/commands/vision.ts
@@ -4977,9 +5128,10 @@ program.command("remove").description("Remove UILint components from your projec
4977
5128
  const { removeUI } = await import("./remove-ui-O4B6EYSF.js");
4978
5129
  await removeUI({ dryRun: options.dryRun, yes: options.yes });
4979
5130
  });
4980
- program.command("serve").description("Start WebSocket server for real-time UI linting").option("-p, --port <number>", "Port to listen on", "9234").action(async (options) => {
5131
+ 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) => {
4981
5132
  await serve({
4982
- port: parseInt(options.port, 10)
5133
+ port: parseInt(options.port, 10),
5134
+ noDashboard: !options.dashboard
4983
5135
  });
4984
5136
  });
4985
5137
  program.command("vision").description("Analyze a screenshot with Ollama vision models (requires a manifest)").option("--list", "List available .uilint/screenshots sidecars and exit").option(