uilint 0.2.22 → 0.2.26

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,21 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSpinner,
4
+ detectCoverageSetup,
4
5
  detectNextAppRouter,
6
+ findEslintConfigFile,
5
7
  findNextAppRouterProjects,
6
8
  intro,
7
9
  logError,
8
10
  logInfo,
9
11
  logSuccess,
10
12
  logWarning,
13
+ needsCoveragePreparation,
11
14
  note,
12
15
  outro,
13
16
  pc,
17
+ prepareCoverage,
18
+ readRuleConfigsFromConfig,
19
+ updateRuleConfigInConfig,
20
+ updateRuleSeverityInConfig,
14
21
  withSpinner
15
- } from "./chunk-FRNXXIEM.js";
22
+ } from "./chunk-VNANPKR2.js";
23
+ import "./chunk-P4I4RKBY.js";
16
24
 
17
25
  // src/index.ts
18
- import { Command } from "commander";
26
+ import { Command as Command6 } from "commander";
19
27
 
20
28
  // src/commands/scan.ts
21
29
  import { dirname, resolve as resolve2 } from "path";
@@ -251,8 +259,8 @@ async function initializeLangfuseIfEnabled() {
251
259
  },
252
260
  { asType: "generation" }
253
261
  );
254
- await new Promise((resolve7) => {
255
- resolveTrace = resolve7;
262
+ await new Promise((resolve8) => {
263
+ resolveTrace = resolve8;
256
264
  });
257
265
  if (endData && generationRef) {
258
266
  const usageDetails = endData.usage ? Object.fromEntries(
@@ -1168,14 +1176,14 @@ import {
1168
1176
  } from "uilint-core";
1169
1177
  import { ensureOllamaReady as ensureOllamaReady3 } from "uilint-core/node";
1170
1178
  async function readStdin2() {
1171
- return new Promise((resolve7) => {
1179
+ return new Promise((resolve8) => {
1172
1180
  let data = "";
1173
1181
  const rl = createInterface({ input: process.stdin });
1174
1182
  rl.on("line", (line) => {
1175
1183
  data += line;
1176
1184
  });
1177
1185
  rl.on("close", () => {
1178
- resolve7(data);
1186
+ resolve8(data);
1179
1187
  });
1180
1188
  });
1181
1189
  }
@@ -2154,6 +2162,42 @@ ${stack}` : ""
2154
2162
  handleConfigSet(key, value);
2155
2163
  break;
2156
2164
  }
2165
+ case "rule:config:set": {
2166
+ const { ruleId, severity, options, requestId } = message;
2167
+ handleRuleConfigSet(ws, ruleId, severity, options, requestId);
2168
+ break;
2169
+ }
2170
+ case "coverage:request": {
2171
+ const { requestId } = message;
2172
+ try {
2173
+ const coveragePath = join3(serverAppRootForVision, "coverage", "coverage-final.json");
2174
+ if (!existsSync5(coveragePath)) {
2175
+ sendMessage(ws, {
2176
+ type: "coverage:error",
2177
+ error: "Coverage data not found. Run tests with coverage first (e.g., `vitest run --coverage`)",
2178
+ requestId
2179
+ });
2180
+ break;
2181
+ }
2182
+ const coverageData = JSON.parse(readFileSync(coveragePath, "utf-8"));
2183
+ logInfo(`${pc.dim("[ws]")} coverage:result ${pc.dim(`${Object.keys(coverageData).length} files`)}`);
2184
+ sendMessage(ws, {
2185
+ type: "coverage:result",
2186
+ coverage: coverageData,
2187
+ timestamp: Date.now(),
2188
+ requestId
2189
+ });
2190
+ } catch (error) {
2191
+ const errorMessage = error instanceof Error ? error.message : String(error);
2192
+ logError(`${pc.dim("[ws]")} coverage:error ${errorMessage}`);
2193
+ sendMessage(ws, {
2194
+ type: "coverage:error",
2195
+ error: errorMessage,
2196
+ requestId
2197
+ });
2198
+ }
2199
+ break;
2200
+ }
2157
2201
  }
2158
2202
  }
2159
2203
  function handleDisconnect(ws) {
@@ -2179,6 +2223,24 @@ function handleFileChange(filePath) {
2179
2223
  sendMessage(ws, { type: "file:changed", filePath: clientFilePath });
2180
2224
  }
2181
2225
  }
2226
+ function handleCoverageFileChange(filePath) {
2227
+ try {
2228
+ const coverageData = JSON.parse(readFileSync(filePath, "utf-8"));
2229
+ logInfo(`${pc.dim("[ws]")} coverage:changed ${pc.dim(`${Object.keys(coverageData).length} files`)}`);
2230
+ broadcast({
2231
+ type: "coverage:result",
2232
+ coverage: coverageData,
2233
+ timestamp: Date.now()
2234
+ });
2235
+ } catch (error) {
2236
+ const errorMessage = error instanceof Error ? error.message : String(error);
2237
+ logError(`${pc.dim("[ws]")} Failed to read coverage data: ${errorMessage}`);
2238
+ broadcast({
2239
+ type: "coverage:error",
2240
+ error: `Failed to read coverage: ${errorMessage}`
2241
+ });
2242
+ }
2243
+ }
2182
2244
  var configStore = /* @__PURE__ */ new Map();
2183
2245
  var connectedClientsSet = /* @__PURE__ */ new Set();
2184
2246
  function broadcastConfigUpdate(key, value) {
@@ -2192,6 +2254,191 @@ function handleConfigSet(key, value) {
2192
2254
  logInfo(`${pc.dim("[ws]")} config:set ${pc.bold(key)} = ${pc.dim(JSON.stringify(value))}`);
2193
2255
  broadcastConfigUpdate(key, value);
2194
2256
  }
2257
+ function broadcastRuleConfigChange(ruleId, severity, options) {
2258
+ const message = {
2259
+ type: "rule:config:changed",
2260
+ ruleId,
2261
+ severity,
2262
+ options
2263
+ };
2264
+ for (const ws of connectedClientsSet) {
2265
+ sendMessage(ws, message);
2266
+ }
2267
+ }
2268
+ function broadcast(message) {
2269
+ for (const ws of connectedClientsSet) {
2270
+ sendMessage(ws, message);
2271
+ }
2272
+ }
2273
+ var isIndexing = false;
2274
+ var reindexTimeout = null;
2275
+ var pendingIndexChanges = /* @__PURE__ */ new Set();
2276
+ async function buildDuplicatesIndex(appRoot) {
2277
+ if (isIndexing) {
2278
+ return;
2279
+ }
2280
+ isIndexing = true;
2281
+ logInfo(`${pc.blue("Building duplicates index...")}`);
2282
+ broadcast({ type: "duplicates:indexing:start" });
2283
+ try {
2284
+ const { indexDirectory } = await import("uilint-duplicates");
2285
+ const result = await indexDirectory(appRoot, {
2286
+ onProgress: (message, current, total) => {
2287
+ if (current !== void 0 && total !== void 0) {
2288
+ logInfo(` ${message} (${current}/${total})`);
2289
+ } else {
2290
+ logInfo(` ${message}`);
2291
+ }
2292
+ broadcast({
2293
+ type: "duplicates:indexing:progress",
2294
+ message,
2295
+ current,
2296
+ total
2297
+ });
2298
+ }
2299
+ });
2300
+ logSuccess(
2301
+ `${pc.green("Index complete:")} ${result.totalChunks} chunks (${result.added} added, ${result.modified} modified, ${result.deleted} deleted) in ${(result.duration / 1e3).toFixed(1)}s`
2302
+ );
2303
+ broadcast({
2304
+ type: "duplicates:indexing:complete",
2305
+ added: result.added,
2306
+ modified: result.modified,
2307
+ deleted: result.deleted,
2308
+ totalChunks: result.totalChunks,
2309
+ duration: result.duration
2310
+ });
2311
+ } catch (error) {
2312
+ const msg = error instanceof Error ? error.message : String(error);
2313
+ logError(`Index failed: ${msg}`);
2314
+ broadcast({ type: "duplicates:indexing:error", error: msg });
2315
+ } finally {
2316
+ isIndexing = false;
2317
+ }
2318
+ }
2319
+ function scheduleReindex(appRoot, filePath) {
2320
+ if (!/\.(tsx?|jsx?)$/.test(filePath)) return;
2321
+ pendingIndexChanges.add(filePath);
2322
+ if (reindexTimeout) clearTimeout(reindexTimeout);
2323
+ reindexTimeout = setTimeout(async () => {
2324
+ const count = pendingIndexChanges.size;
2325
+ pendingIndexChanges.clear();
2326
+ logInfo(`${pc.dim(`[index] ${count} file(s) changed, updating index...`)}`);
2327
+ await buildDuplicatesIndex(appRoot);
2328
+ }, 2e3);
2329
+ }
2330
+ var isPreparingCoverage = false;
2331
+ function isCoverageRuleEnabled(appRoot) {
2332
+ const eslintConfigPath = findEslintConfigFile(appRoot);
2333
+ if (!eslintConfigPath) return false;
2334
+ const ruleConfigs = readRuleConfigsFromConfig(eslintConfigPath);
2335
+ const coverageConfig = ruleConfigs.get("require-test-coverage");
2336
+ if (!coverageConfig) return false;
2337
+ return coverageConfig.severity !== "off";
2338
+ }
2339
+ async function buildCoverageData(appRoot) {
2340
+ if (isPreparingCoverage) return;
2341
+ isPreparingCoverage = true;
2342
+ try {
2343
+ if (!isCoverageRuleEnabled(appRoot)) {
2344
+ logInfo(`${pc.dim("Coverage rule not enabled, skipping preparation")}`);
2345
+ return;
2346
+ }
2347
+ const setup = detectCoverageSetup(appRoot);
2348
+ if (!needsCoveragePreparation(setup)) {
2349
+ logInfo(`${pc.dim("Coverage data is up-to-date")}`);
2350
+ return;
2351
+ }
2352
+ logInfo(`${pc.blue("Preparing coverage data...")}`);
2353
+ broadcast({ type: "coverage:setup:start" });
2354
+ const skipPackageInstall = process.env.UILINT_SKIP_COVERAGE_INSTALL === "1";
2355
+ const skipTests = process.env.UILINT_SKIP_COVERAGE_TESTS === "1";
2356
+ const result = await prepareCoverage({
2357
+ appRoot,
2358
+ skipPackageInstall,
2359
+ skipTests,
2360
+ onProgress: (message, phase) => {
2361
+ logInfo(` ${message}`);
2362
+ broadcast({ type: "coverage:setup:progress", message, phase });
2363
+ }
2364
+ });
2365
+ if (result.error) {
2366
+ logWarning(`Coverage preparation completed with errors: ${result.error}`);
2367
+ } else {
2368
+ const parts = [];
2369
+ if (result.packageAdded) parts.push("package installed");
2370
+ if (result.configModified) parts.push("config modified");
2371
+ if (result.testsRan) parts.push("tests ran");
2372
+ if (result.coverageGenerated) parts.push("coverage generated");
2373
+ logSuccess(
2374
+ `${pc.green("Coverage prepared:")} ${parts.join(", ")} in ${(result.duration / 1e3).toFixed(1)}s`
2375
+ );
2376
+ }
2377
+ broadcast({
2378
+ type: "coverage:setup:complete",
2379
+ ...result
2380
+ });
2381
+ } catch (error) {
2382
+ const msg = error instanceof Error ? error.message : String(error);
2383
+ logError(`Coverage preparation failed: ${msg}`);
2384
+ broadcast({ type: "coverage:setup:error", error: msg });
2385
+ } finally {
2386
+ isPreparingCoverage = false;
2387
+ }
2388
+ }
2389
+ function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2390
+ logInfo(
2391
+ `${pc.dim("[ws]")} rule:config:set ${pc.bold(ruleId)} -> ${pc.dim(severity)}${options ? ` with options` : ""}`
2392
+ );
2393
+ const configPath = findEslintConfigFile(serverAppRootForVision);
2394
+ if (!configPath) {
2395
+ const error = `No ESLint config file found in ${serverAppRootForVision}`;
2396
+ logError(`${pc.dim("[ws]")} ${error}`);
2397
+ sendMessage(ws, {
2398
+ type: "rule:config:result",
2399
+ ruleId,
2400
+ severity,
2401
+ options,
2402
+ success: false,
2403
+ error,
2404
+ requestId
2405
+ });
2406
+ return;
2407
+ }
2408
+ let result;
2409
+ if (options && Object.keys(options).length > 0) {
2410
+ result = updateRuleConfigInConfig(configPath, ruleId, severity, options);
2411
+ } else {
2412
+ result = updateRuleSeverityInConfig(configPath, ruleId, severity);
2413
+ }
2414
+ if (result.success) {
2415
+ logSuccess(
2416
+ `${pc.dim("[ws]")} Updated ${pc.bold(`uilint/${ruleId}`)} -> ${pc.dim(severity)}`
2417
+ );
2418
+ eslintInstances.clear();
2419
+ cache.clear();
2420
+ sendMessage(ws, {
2421
+ type: "rule:config:result",
2422
+ ruleId,
2423
+ severity,
2424
+ options,
2425
+ success: true,
2426
+ requestId
2427
+ });
2428
+ broadcastRuleConfigChange(ruleId, severity, options);
2429
+ } else {
2430
+ logError(`${pc.dim("[ws]")} Failed to update rule: ${result.error}`);
2431
+ sendMessage(ws, {
2432
+ type: "rule:config:result",
2433
+ ruleId,
2434
+ severity,
2435
+ options,
2436
+ success: false,
2437
+ error: result.error,
2438
+ requestId
2439
+ });
2440
+ }
2441
+ }
2195
2442
  async function serve(options) {
2196
2443
  const port = options.port || 9234;
2197
2444
  const cwd = process.cwd();
@@ -2206,9 +2453,26 @@ async function serve(options) {
2206
2453
  ignoreInitial: true
2207
2454
  });
2208
2455
  fileWatcher.on("change", (path) => {
2209
- handleFileChange(resolve5(path));
2456
+ const resolvedPath = resolve5(path);
2457
+ if (resolvedPath.endsWith("coverage-final.json")) {
2458
+ handleCoverageFileChange(resolvedPath);
2459
+ return;
2460
+ }
2461
+ handleFileChange(resolvedPath);
2462
+ scheduleReindex(appRoot, resolvedPath);
2210
2463
  });
2464
+ const coveragePath = join3(appRoot, "coverage", "coverage-final.json");
2465
+ if (existsSync5(coveragePath)) {
2466
+ fileWatcher.add(coveragePath);
2467
+ logInfo(`Watching coverage: ${pc.dim(coveragePath)}`);
2468
+ }
2211
2469
  const wss = new WebSocketServer({ port });
2470
+ buildDuplicatesIndex(appRoot).catch((err) => {
2471
+ logError(`Failed to build duplicates index: ${err.message}`);
2472
+ });
2473
+ buildCoverageData(appRoot).catch((err) => {
2474
+ logWarning(`Failed to prepare coverage: ${err.message}`);
2475
+ });
2212
2476
  wss.on("connection", (ws) => {
2213
2477
  connectedClients += 1;
2214
2478
  connectedClientsSet.add(ws);
@@ -2219,15 +2483,25 @@ async function serve(options) {
2219
2483
  workspaceRoot: wsRoot,
2220
2484
  serverCwd: cwd
2221
2485
  });
2486
+ const eslintConfigPath = findEslintConfigFile(appRoot);
2487
+ const currentRuleConfigs = eslintConfigPath ? readRuleConfigsFromConfig(eslintConfigPath) : /* @__PURE__ */ new Map();
2222
2488
  sendMessage(ws, {
2223
2489
  type: "rules:metadata",
2224
- rules: ruleRegistry.map((rule) => ({
2225
- id: rule.id,
2226
- name: rule.name,
2227
- description: rule.description,
2228
- category: rule.category,
2229
- defaultSeverity: rule.defaultSeverity
2230
- }))
2490
+ rules: ruleRegistry.filter((rule) => currentRuleConfigs.has(rule.id)).map((rule) => {
2491
+ const currentConfig = currentRuleConfigs.get(rule.id);
2492
+ return {
2493
+ id: rule.id,
2494
+ name: rule.name,
2495
+ description: rule.description,
2496
+ category: rule.category,
2497
+ defaultSeverity: rule.defaultSeverity,
2498
+ currentSeverity: currentConfig?.severity,
2499
+ currentOptions: currentConfig?.options,
2500
+ docs: rule.docs,
2501
+ optionSchema: rule.optionSchema,
2502
+ defaultOptions: rule.defaultOptions
2503
+ };
2504
+ })
2231
2505
  });
2232
2506
  for (const [key, value] of configStore) {
2233
2507
  sendMessage(ws, { type: "config:update", key, value });
@@ -2252,12 +2526,12 @@ async function serve(options) {
2252
2526
  `UILint WebSocket server running on ${pc.cyan(`ws://localhost:${port}`)}`
2253
2527
  );
2254
2528
  logInfo("Press Ctrl+C to stop");
2255
- await new Promise((resolve7) => {
2529
+ await new Promise((resolve8) => {
2256
2530
  process.on("SIGINT", () => {
2257
2531
  logInfo("Shutting down...");
2258
2532
  wss.close();
2259
2533
  fileWatcher?.close();
2260
- resolve7();
2534
+ resolve8();
2261
2535
  });
2262
2536
  });
2263
2537
  }
@@ -2776,8 +3050,32 @@ function presetToPosition(preset) {
2776
3050
  };
2777
3051
  return positions[preset] || { x: 500, y: 30 };
2778
3052
  }
3053
+ function parseRuleConfig(value) {
3054
+ const match = value.match(/^([^:]+):(error|warn|off)$/);
3055
+ if (!match) {
3056
+ return null;
3057
+ }
3058
+ return {
3059
+ ruleId: match[1],
3060
+ severity: match[2]
3061
+ };
3062
+ }
3063
+ function parseOptionsJson(optionsStr) {
3064
+ if (!optionsStr) {
3065
+ return void 0;
3066
+ }
3067
+ try {
3068
+ const parsed = JSON.parse(optionsStr);
3069
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3070
+ return void 0;
3071
+ }
3072
+ return parsed;
3073
+ } catch {
3074
+ return void 0;
3075
+ }
3076
+ }
2779
3077
  async function sendConfigMessage(port, key, value) {
2780
- return new Promise((resolve7) => {
3078
+ return new Promise((resolve8) => {
2781
3079
  const url = `ws://localhost:${port}`;
2782
3080
  const ws = new WebSocket2(url);
2783
3081
  let resolved = false;
@@ -2785,7 +3083,7 @@ async function sendConfigMessage(port, key, value) {
2785
3083
  if (!resolved) {
2786
3084
  resolved = true;
2787
3085
  ws.close();
2788
- resolve7(false);
3086
+ resolve8(false);
2789
3087
  }
2790
3088
  }, 5e3);
2791
3089
  ws.on("open", () => {
@@ -2796,7 +3094,7 @@ async function sendConfigMessage(port, key, value) {
2796
3094
  resolved = true;
2797
3095
  clearTimeout(timeout);
2798
3096
  ws.close();
2799
- resolve7(true);
3097
+ resolve8(true);
2800
3098
  }
2801
3099
  }, 100);
2802
3100
  });
@@ -2804,12 +3102,61 @@ async function sendConfigMessage(port, key, value) {
2804
3102
  if (!resolved) {
2805
3103
  resolved = true;
2806
3104
  clearTimeout(timeout);
2807
- resolve7(false);
3105
+ resolve8(false);
3106
+ }
3107
+ });
3108
+ });
3109
+ }
3110
+ async function sendRuleConfigMessage(port, ruleId, severity, options) {
3111
+ return new Promise((resolve8) => {
3112
+ const url = `ws://localhost:${port}`;
3113
+ const ws = new WebSocket2(url);
3114
+ let resolved = false;
3115
+ const requestId = `cli-${Date.now()}`;
3116
+ const timeout = setTimeout(() => {
3117
+ if (!resolved) {
3118
+ resolved = true;
3119
+ ws.close();
3120
+ resolve8({ success: false, error: "Request timed out" });
3121
+ }
3122
+ }, 1e4);
3123
+ ws.on("open", () => {
3124
+ const message = JSON.stringify({
3125
+ type: "rule:config:set",
3126
+ ruleId,
3127
+ severity,
3128
+ options,
3129
+ requestId
3130
+ });
3131
+ ws.send(message);
3132
+ });
3133
+ ws.on("message", (data) => {
3134
+ try {
3135
+ const msg = JSON.parse(data.toString());
3136
+ if (msg.type === "rule:config:result" && msg.requestId === requestId) {
3137
+ if (!resolved) {
3138
+ resolved = true;
3139
+ clearTimeout(timeout);
3140
+ ws.close();
3141
+ resolve8({
3142
+ success: msg.success,
3143
+ error: msg.error
3144
+ });
3145
+ }
3146
+ }
3147
+ } catch {
3148
+ }
3149
+ });
3150
+ ws.on("error", () => {
3151
+ if (!resolved) {
3152
+ resolved = true;
3153
+ clearTimeout(timeout);
3154
+ resolve8({ success: false, error: "Connection error" });
2808
3155
  }
2809
3156
  });
2810
3157
  });
2811
3158
  }
2812
- async function handleSet(key, value, port) {
3159
+ async function handleSet(key, value, port, extraArg) {
2813
3160
  switch (key) {
2814
3161
  case "position": {
2815
3162
  const parsed = parsePosition(value);
@@ -2839,6 +3186,50 @@ Expected format: x,y (e.g., 100,50) or preset (top-center, top-left, etc.)`
2839
3186
  } else {
2840
3187
  logError(
2841
3188
  `Failed to set position. Is the server running?
3189
+ Start it with: ${pc.bold("npx uilint serve")}`
3190
+ );
3191
+ process.exit(1);
3192
+ }
3193
+ break;
3194
+ }
3195
+ case "rule": {
3196
+ const parsed = parseRuleConfig(value);
3197
+ if (!parsed) {
3198
+ logError(
3199
+ `Invalid rule config: ${value}
3200
+ Expected format: <ruleId>:<severity>
3201
+ severity: error, warn, or off
3202
+
3203
+ Examples:
3204
+ uilint config set rule no-arbitrary-tailwind:warn
3205
+ uilint config set rule no-prop-drilling-depth:error '{"maxDepth":3}'`
3206
+ );
3207
+ process.exit(1);
3208
+ }
3209
+ const options = parseOptionsJson(extraArg);
3210
+ if (extraArg && !options) {
3211
+ logError(
3212
+ `Invalid options JSON: ${extraArg}
3213
+ Expected a valid JSON object, e.g., '{"maxDepth": 3}'`
3214
+ );
3215
+ process.exit(1);
3216
+ }
3217
+ logInfo(
3218
+ `Setting rule "${parsed.ruleId}" to ${parsed.severity}` + (options ? ` with options: ${JSON.stringify(options)}` : "")
3219
+ );
3220
+ const result = await sendRuleConfigMessage(
3221
+ port,
3222
+ parsed.ruleId,
3223
+ parsed.severity,
3224
+ options
3225
+ );
3226
+ if (result.success) {
3227
+ logSuccess(
3228
+ `Rule "${parsed.ruleId}" set to ${parsed.severity}` + (options ? ` with options` : "")
3229
+ );
3230
+ } else {
3231
+ logError(
3232
+ result.error || `Failed to set rule config. Is the server running?
2842
3233
  Start it with: ${pc.bold("npx uilint serve")}`
2843
3234
  );
2844
3235
  process.exit(1);
@@ -2847,7 +3238,7 @@ Start it with: ${pc.bold("npx uilint serve")}`
2847
3238
  }
2848
3239
  default:
2849
3240
  logError(`Unknown config key: ${key}`);
2850
- logInfo(`Available keys: position`);
3241
+ logInfo(`Available keys: position, rule`);
2851
3242
  process.exit(1);
2852
3243
  }
2853
3244
  }
@@ -2866,7 +3257,7 @@ To view it, check your browser's dev tools:
2866
3257
  process.exit(1);
2867
3258
  }
2868
3259
  }
2869
- async function config2(action, key, value, options = {}) {
3260
+ async function config2(action, key, value, extraArg, options = {}) {
2870
3261
  const port = options.port || 9234;
2871
3262
  switch (action) {
2872
3263
  case "set":
@@ -2874,18 +3265,287 @@ async function config2(action, key, value, options = {}) {
2874
3265
  logError(`Missing value for config set ${key}`);
2875
3266
  process.exit(1);
2876
3267
  }
2877
- await handleSet(key, value, port);
3268
+ await handleSet(key, value, port, extraArg);
2878
3269
  break;
2879
3270
  case "get":
2880
3271
  await handleGet(key, port);
2881
3272
  break;
2882
3273
  default:
2883
3274
  logError(`Unknown action: ${action}`);
2884
- logInfo(`Usage: uilint config <set|get> <key> [value]`);
3275
+ logInfo(`Usage: uilint config <set|get> <key> [value] [options]`);
2885
3276
  process.exit(1);
2886
3277
  }
2887
3278
  }
2888
3279
 
3280
+ // src/commands/duplicates/index.ts
3281
+ import { Command as Command5 } from "commander";
3282
+
3283
+ // src/commands/duplicates/index-cmd.ts
3284
+ import { Command } from "commander";
3285
+ import chalk2 from "chalk";
3286
+ import ora from "ora";
3287
+ function indexCommand() {
3288
+ return new Command("index").description("Build or update the semantic duplicates index").option("--force", "Rebuild index from scratch").option("--model <name>", "Embedding model (default: nomic-embed-text)").option(
3289
+ "--exclude <glob>",
3290
+ "Exclude patterns (repeatable)",
3291
+ (val, prev) => [...prev, val],
3292
+ []
3293
+ ).option("-o, --output <format>", "Output format: text or json", "text").action(async (options) => {
3294
+ const { indexDirectory } = await import("uilint-duplicates");
3295
+ const projectRoot = process.cwd();
3296
+ const isJson = options.output === "json";
3297
+ let spinner;
3298
+ if (!isJson) {
3299
+ spinner = ora("Initializing indexer...").start();
3300
+ }
3301
+ try {
3302
+ const result = await indexDirectory(projectRoot, {
3303
+ force: options.force,
3304
+ model: options.model,
3305
+ exclude: options.exclude,
3306
+ onProgress: (message, current, total) => {
3307
+ if (spinner) {
3308
+ if (current && total) {
3309
+ spinner.text = `${message} (${current}/${total})`;
3310
+ } else {
3311
+ spinner.text = message;
3312
+ }
3313
+ }
3314
+ }
3315
+ });
3316
+ if (isJson) {
3317
+ console.log(JSON.stringify(result, null, 2));
3318
+ } else {
3319
+ spinner?.succeed(chalk2.green("Index complete"));
3320
+ console.log();
3321
+ console.log(chalk2.bold("Index Statistics:"));
3322
+ console.log(` Files added: ${result.added}`);
3323
+ console.log(` Files modified: ${result.modified}`);
3324
+ console.log(` Files deleted: ${result.deleted}`);
3325
+ console.log(` Total chunks: ${result.totalChunks}`);
3326
+ console.log(` Duration: ${(result.duration / 1e3).toFixed(2)}s`);
3327
+ }
3328
+ } catch (error) {
3329
+ if (spinner) {
3330
+ spinner.fail(chalk2.red("Index failed"));
3331
+ }
3332
+ const message = error instanceof Error ? error.message : String(error);
3333
+ if (isJson) {
3334
+ console.log(JSON.stringify({ error: message }, null, 2));
3335
+ } else {
3336
+ console.error(chalk2.red(`Error: ${message}`));
3337
+ }
3338
+ process.exit(1);
3339
+ }
3340
+ });
3341
+ }
3342
+
3343
+ // src/commands/duplicates/find.ts
3344
+ import { Command as Command2 } from "commander";
3345
+ import { relative as relative2 } from "path";
3346
+ import chalk3 from "chalk";
3347
+ function findCommand() {
3348
+ return new Command2("find").description("Find semantic duplicate groups in the codebase").option(
3349
+ "--threshold <n>",
3350
+ "Similarity threshold 0-1 (default: 0.85)",
3351
+ parseFloat
3352
+ ).option("--min-size <n>", "Minimum group size (default: 2)", parseInt).option("--kind <type>", "Filter: component, hook, function").option("-o, --output <format>", "Output format: text or json", "text").action(async (options) => {
3353
+ const { findDuplicates } = await import("uilint-duplicates");
3354
+ const projectRoot = process.cwd();
3355
+ const isJson = options.output === "json";
3356
+ try {
3357
+ const groups = await findDuplicates({
3358
+ path: projectRoot,
3359
+ threshold: options.threshold,
3360
+ minGroupSize: options.minSize,
3361
+ kind: options.kind
3362
+ });
3363
+ if (isJson) {
3364
+ console.log(JSON.stringify({ groups }, null, 2));
3365
+ return;
3366
+ }
3367
+ if (groups.length === 0) {
3368
+ console.log(chalk3.green("No semantic duplicates found."));
3369
+ return;
3370
+ }
3371
+ console.log(
3372
+ chalk3.bold(
3373
+ `Found ${groups.length} duplicate group${groups.length > 1 ? "s" : ""}:
3374
+ `
3375
+ )
3376
+ );
3377
+ groups.forEach((group, idx) => {
3378
+ const similarity = Math.round(group.avgSimilarity * 100);
3379
+ console.log(
3380
+ chalk3.yellow(
3381
+ `Duplicate Group ${idx + 1} (${similarity}% similar, ${group.members.length} occurrences):`
3382
+ )
3383
+ );
3384
+ group.members.forEach((member) => {
3385
+ const relPath = relative2(projectRoot, member.filePath);
3386
+ const location = `${relPath}:${member.startLine}-${member.endLine}`;
3387
+ const name = member.name || "(anonymous)";
3388
+ const score = member.score === 1 ? "" : chalk3.dim(` (${Math.round(member.score * 100)}%)`);
3389
+ console.log(` ${chalk3.cyan(location.padEnd(50))} ${name}${score}`);
3390
+ });
3391
+ console.log(
3392
+ chalk3.dim(
3393
+ ` Suggestion: Consider extracting shared logic into a reusable ${group.kind}
3394
+ `
3395
+ )
3396
+ );
3397
+ });
3398
+ } catch (error) {
3399
+ const message = error instanceof Error ? error.message : String(error);
3400
+ if (isJson) {
3401
+ console.log(JSON.stringify({ error: message }, null, 2));
3402
+ } else {
3403
+ console.error(chalk3.red(`Error: ${message}`));
3404
+ }
3405
+ process.exit(1);
3406
+ }
3407
+ });
3408
+ }
3409
+
3410
+ // src/commands/duplicates/search.ts
3411
+ import { Command as Command3 } from "commander";
3412
+ import { relative as relative3 } from "path";
3413
+ import chalk4 from "chalk";
3414
+ function searchCommand() {
3415
+ 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) => {
3416
+ const { searchSimilar } = await import("uilint-duplicates");
3417
+ const projectRoot = process.cwd();
3418
+ const isJson = options.output === "json";
3419
+ try {
3420
+ const results = await searchSimilar(query, {
3421
+ path: projectRoot,
3422
+ top: options.top,
3423
+ threshold: options.threshold
3424
+ });
3425
+ if (isJson) {
3426
+ console.log(JSON.stringify({ results }, null, 2));
3427
+ return;
3428
+ }
3429
+ if (results.length === 0) {
3430
+ console.log(chalk4.yellow("No matching code found."));
3431
+ return;
3432
+ }
3433
+ console.log(chalk4.bold(`Found ${results.length} matching results:
3434
+ `));
3435
+ results.forEach((result, idx) => {
3436
+ const relPath = relative3(projectRoot, result.filePath);
3437
+ const location = `${relPath}:${result.startLine}-${result.endLine}`;
3438
+ const name = result.name || "(anonymous)";
3439
+ const score = Math.round(result.score * 100);
3440
+ const kindLabel = result.kind.padEnd(10);
3441
+ console.log(
3442
+ `${chalk4.dim(`${idx + 1}.`)} ${chalk4.cyan(location)}`
3443
+ );
3444
+ console.log(
3445
+ ` ${chalk4.dim(kindLabel)} ${chalk4.bold(name)} ${chalk4.green(`(${score}% similar)`)}`
3446
+ );
3447
+ });
3448
+ } catch (error) {
3449
+ const message = error instanceof Error ? error.message : String(error);
3450
+ if (isJson) {
3451
+ console.log(JSON.stringify({ error: message }, null, 2));
3452
+ } else {
3453
+ console.error(chalk4.red(`Error: ${message}`));
3454
+ }
3455
+ process.exit(1);
3456
+ }
3457
+ });
3458
+ }
3459
+
3460
+ // src/commands/duplicates/similar.ts
3461
+ import { Command as Command4 } from "commander";
3462
+ import { relative as relative4, resolve as resolve7, isAbsolute as isAbsolute2 } from "path";
3463
+ import chalk5 from "chalk";
3464
+ function similarCommand() {
3465
+ 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) => {
3466
+ const { findSimilarAtLocation } = await import("uilint-duplicates");
3467
+ const projectRoot = process.cwd();
3468
+ const isJson = options.output === "json";
3469
+ const colonIdx = location.lastIndexOf(":");
3470
+ if (colonIdx === -1) {
3471
+ const message = "Invalid location format. Use file:line (e.g., src/Button.tsx:15)";
3472
+ if (isJson) {
3473
+ console.log(JSON.stringify({ error: message }, null, 2));
3474
+ } else {
3475
+ console.error(chalk5.red(`Error: ${message}`));
3476
+ }
3477
+ process.exit(1);
3478
+ }
3479
+ const filePart = location.slice(0, colonIdx);
3480
+ const linePart = location.slice(colonIdx + 1);
3481
+ const line = parseInt(linePart, 10);
3482
+ if (isNaN(line)) {
3483
+ const message = `Invalid line number: ${linePart}`;
3484
+ if (isJson) {
3485
+ console.log(JSON.stringify({ error: message }, null, 2));
3486
+ } else {
3487
+ console.error(chalk5.red(`Error: ${message}`));
3488
+ }
3489
+ process.exit(1);
3490
+ }
3491
+ const filePath = isAbsolute2(filePart) ? filePart : resolve7(projectRoot, filePart);
3492
+ try {
3493
+ const results = await findSimilarAtLocation({
3494
+ path: projectRoot,
3495
+ filePath,
3496
+ line,
3497
+ top: options.top,
3498
+ threshold: options.threshold ?? 0.7
3499
+ });
3500
+ if (isJson) {
3501
+ console.log(JSON.stringify({ results }, null, 2));
3502
+ return;
3503
+ }
3504
+ if (results.length === 0) {
3505
+ console.log(chalk5.yellow("No similar code found."));
3506
+ return;
3507
+ }
3508
+ console.log(
3509
+ chalk5.bold(
3510
+ `Found ${results.length} similar code locations to ${relative4(projectRoot, filePath)}:${line}:
3511
+ `
3512
+ )
3513
+ );
3514
+ results.forEach((result, idx) => {
3515
+ const relPath = relative4(projectRoot, result.filePath);
3516
+ const locationStr = `${relPath}:${result.startLine}-${result.endLine}`;
3517
+ const name = result.name || "(anonymous)";
3518
+ const score = Math.round(result.score * 100);
3519
+ const kindLabel = result.kind.padEnd(10);
3520
+ console.log(
3521
+ `${chalk5.dim(`${idx + 1}.`)} ${chalk5.cyan(locationStr)}`
3522
+ );
3523
+ console.log(
3524
+ ` ${chalk5.dim(kindLabel)} ${chalk5.bold(name)} ${chalk5.green(`(${score}% similar)`)}`
3525
+ );
3526
+ });
3527
+ } catch (error) {
3528
+ const message = error instanceof Error ? error.message : String(error);
3529
+ if (isJson) {
3530
+ console.log(JSON.stringify({ error: message }, null, 2));
3531
+ } else {
3532
+ console.error(chalk5.red(`Error: ${message}`));
3533
+ }
3534
+ process.exit(1);
3535
+ }
3536
+ });
3537
+ }
3538
+
3539
+ // src/commands/duplicates/index.ts
3540
+ function createDuplicatesCommand() {
3541
+ const duplicates = new Command5("duplicates").description("Semantic code duplicate detection");
3542
+ duplicates.addCommand(indexCommand());
3543
+ duplicates.addCommand(findCommand());
3544
+ duplicates.addCommand(searchCommand());
3545
+ duplicates.addCommand(similarCommand());
3546
+ return duplicates;
3547
+ }
3548
+
2889
3549
  // src/index.ts
2890
3550
  import { readFileSync as readFileSync3 } from "fs";
2891
3551
  import { dirname as dirname7, join as join5 } from "path";
@@ -2902,7 +3562,7 @@ function assertNodeVersion(minMajor) {
2902
3562
  }
2903
3563
  }
2904
3564
  assertNodeVersion(20);
2905
- var program = new Command();
3565
+ var program = new Command6();
2906
3566
  function getCLIVersion() {
2907
3567
  try {
2908
3568
  const __dirname = dirname7(fileURLToPath(import.meta.url));
@@ -2981,7 +3641,7 @@ program.command("update").description("Update existing style guide with new styl
2981
3641
  });
2982
3642
  });
2983
3643
  program.command("install").description("Install UILint integration").option("--force", "Overwrite existing configuration files").action(async (options) => {
2984
- const { installUI } = await import("./install-ui-HTVB5HDB.js");
3644
+ const { installUI } = await import("./install-ui-CCZ3XJDE.js");
2985
3645
  await installUI({ force: options.force });
2986
3646
  });
2987
3647
  program.command("serve").description("Start WebSocket server for real-time UI linting").option("-p, --port <number>", "Port to listen on", "9234").action(async (options) => {
@@ -3023,10 +3683,11 @@ program.command("vision").description("Analyze a screenshot with Ollama vision m
3023
3683
  debugDump: options.debugDump
3024
3684
  });
3025
3685
  });
3026
- program.command("config").description("Get or set UILint configuration options").argument("<action>", "Action: set or get").argument("<key>", "Config key (e.g., position)").argument("[value]", "Value to set (for set action)").option("-p, --port <number>", "WebSocket server port", "9234").action(async (action, key, value, options) => {
3027
- await config2(action, key, value, {
3686
+ program.command("config").description("Get or set UILint configuration options").argument("<action>", "Action: set or get").argument("<key>", "Config key (e.g., position, rule)").argument("[value]", "Value to set (for set action)").argument("[extraArg]", "Extra argument (e.g., options JSON for rule config)").option("-p, --port <number>", "WebSocket server port", "9234").action(async (action, key, value, extraArg, options) => {
3687
+ await config2(action, key, value, extraArg, {
3028
3688
  port: parseInt(options.port, 10)
3029
3689
  });
3030
3690
  });
3691
+ program.addCommand(createDuplicatesCommand());
3031
3692
  program.parse();
3032
3693
  //# sourceMappingURL=index.js.map