uilint 0.2.23 → 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,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSpinner,
4
+ detectCoverageSetup,
4
5
  detectNextAppRouter,
5
6
  findEslintConfigFile,
6
7
  findNextAppRouterProjects,
@@ -9,17 +10,20 @@ import {
9
10
  logInfo,
10
11
  logSuccess,
11
12
  logWarning,
13
+ needsCoveragePreparation,
12
14
  note,
13
15
  outro,
14
16
  pc,
17
+ prepareCoverage,
15
18
  readRuleConfigsFromConfig,
16
19
  updateRuleConfigInConfig,
17
20
  updateRuleSeverityInConfig,
18
21
  withSpinner
19
- } from "./chunk-PB5DLLVC.js";
22
+ } from "./chunk-VNANPKR2.js";
23
+ import "./chunk-P4I4RKBY.js";
20
24
 
21
25
  // src/index.ts
22
- import { Command } from "commander";
26
+ import { Command as Command6 } from "commander";
23
27
 
24
28
  // src/commands/scan.ts
25
29
  import { dirname, resolve as resolve2 } from "path";
@@ -255,8 +259,8 @@ async function initializeLangfuseIfEnabled() {
255
259
  },
256
260
  { asType: "generation" }
257
261
  );
258
- await new Promise((resolve7) => {
259
- resolveTrace = resolve7;
262
+ await new Promise((resolve8) => {
263
+ resolveTrace = resolve8;
260
264
  });
261
265
  if (endData && generationRef) {
262
266
  const usageDetails = endData.usage ? Object.fromEntries(
@@ -1172,14 +1176,14 @@ import {
1172
1176
  } from "uilint-core";
1173
1177
  import { ensureOllamaReady as ensureOllamaReady3 } from "uilint-core/node";
1174
1178
  async function readStdin2() {
1175
- return new Promise((resolve7) => {
1179
+ return new Promise((resolve8) => {
1176
1180
  let data = "";
1177
1181
  const rl = createInterface({ input: process.stdin });
1178
1182
  rl.on("line", (line) => {
1179
1183
  data += line;
1180
1184
  });
1181
1185
  rl.on("close", () => {
1182
- resolve7(data);
1186
+ resolve8(data);
1183
1187
  });
1184
1188
  });
1185
1189
  }
@@ -2163,6 +2167,37 @@ ${stack}` : ""
2163
2167
  handleRuleConfigSet(ws, ruleId, severity, options, requestId);
2164
2168
  break;
2165
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
+ }
2166
2201
  }
2167
2202
  }
2168
2203
  function handleDisconnect(ws) {
@@ -2188,6 +2223,24 @@ function handleFileChange(filePath) {
2188
2223
  sendMessage(ws, { type: "file:changed", filePath: clientFilePath });
2189
2224
  }
2190
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
+ }
2191
2244
  var configStore = /* @__PURE__ */ new Map();
2192
2245
  var connectedClientsSet = /* @__PURE__ */ new Set();
2193
2246
  function broadcastConfigUpdate(key, value) {
@@ -2212,6 +2265,127 @@ function broadcastRuleConfigChange(ruleId, severity, options) {
2212
2265
  sendMessage(ws, message);
2213
2266
  }
2214
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
+ }
2215
2389
  function handleRuleConfigSet(ws, ruleId, severity, options, requestId) {
2216
2390
  logInfo(
2217
2391
  `${pc.dim("[ws]")} rule:config:set ${pc.bold(ruleId)} -> ${pc.dim(severity)}${options ? ` with options` : ""}`
@@ -2279,9 +2453,26 @@ async function serve(options) {
2279
2453
  ignoreInitial: true
2280
2454
  });
2281
2455
  fileWatcher.on("change", (path) => {
2282
- 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);
2283
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
+ }
2284
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
+ });
2285
2476
  wss.on("connection", (ws) => {
2286
2477
  connectedClients += 1;
2287
2478
  connectedClientsSet.add(ws);
@@ -2296,7 +2487,7 @@ async function serve(options) {
2296
2487
  const currentRuleConfigs = eslintConfigPath ? readRuleConfigsFromConfig(eslintConfigPath) : /* @__PURE__ */ new Map();
2297
2488
  sendMessage(ws, {
2298
2489
  type: "rules:metadata",
2299
- rules: ruleRegistry.map((rule) => {
2490
+ rules: ruleRegistry.filter((rule) => currentRuleConfigs.has(rule.id)).map((rule) => {
2300
2491
  const currentConfig = currentRuleConfigs.get(rule.id);
2301
2492
  return {
2302
2493
  id: rule.id,
@@ -2335,12 +2526,12 @@ async function serve(options) {
2335
2526
  `UILint WebSocket server running on ${pc.cyan(`ws://localhost:${port}`)}`
2336
2527
  );
2337
2528
  logInfo("Press Ctrl+C to stop");
2338
- await new Promise((resolve7) => {
2529
+ await new Promise((resolve8) => {
2339
2530
  process.on("SIGINT", () => {
2340
2531
  logInfo("Shutting down...");
2341
2532
  wss.close();
2342
2533
  fileWatcher?.close();
2343
- resolve7();
2534
+ resolve8();
2344
2535
  });
2345
2536
  });
2346
2537
  }
@@ -2884,7 +3075,7 @@ function parseOptionsJson(optionsStr) {
2884
3075
  }
2885
3076
  }
2886
3077
  async function sendConfigMessage(port, key, value) {
2887
- return new Promise((resolve7) => {
3078
+ return new Promise((resolve8) => {
2888
3079
  const url = `ws://localhost:${port}`;
2889
3080
  const ws = new WebSocket2(url);
2890
3081
  let resolved = false;
@@ -2892,7 +3083,7 @@ async function sendConfigMessage(port, key, value) {
2892
3083
  if (!resolved) {
2893
3084
  resolved = true;
2894
3085
  ws.close();
2895
- resolve7(false);
3086
+ resolve8(false);
2896
3087
  }
2897
3088
  }, 5e3);
2898
3089
  ws.on("open", () => {
@@ -2903,7 +3094,7 @@ async function sendConfigMessage(port, key, value) {
2903
3094
  resolved = true;
2904
3095
  clearTimeout(timeout);
2905
3096
  ws.close();
2906
- resolve7(true);
3097
+ resolve8(true);
2907
3098
  }
2908
3099
  }, 100);
2909
3100
  });
@@ -2911,13 +3102,13 @@ async function sendConfigMessage(port, key, value) {
2911
3102
  if (!resolved) {
2912
3103
  resolved = true;
2913
3104
  clearTimeout(timeout);
2914
- resolve7(false);
3105
+ resolve8(false);
2915
3106
  }
2916
3107
  });
2917
3108
  });
2918
3109
  }
2919
3110
  async function sendRuleConfigMessage(port, ruleId, severity, options) {
2920
- return new Promise((resolve7) => {
3111
+ return new Promise((resolve8) => {
2921
3112
  const url = `ws://localhost:${port}`;
2922
3113
  const ws = new WebSocket2(url);
2923
3114
  let resolved = false;
@@ -2926,7 +3117,7 @@ async function sendRuleConfigMessage(port, ruleId, severity, options) {
2926
3117
  if (!resolved) {
2927
3118
  resolved = true;
2928
3119
  ws.close();
2929
- resolve7({ success: false, error: "Request timed out" });
3120
+ resolve8({ success: false, error: "Request timed out" });
2930
3121
  }
2931
3122
  }, 1e4);
2932
3123
  ws.on("open", () => {
@@ -2947,7 +3138,7 @@ async function sendRuleConfigMessage(port, ruleId, severity, options) {
2947
3138
  resolved = true;
2948
3139
  clearTimeout(timeout);
2949
3140
  ws.close();
2950
- resolve7({
3141
+ resolve8({
2951
3142
  success: msg.success,
2952
3143
  error: msg.error
2953
3144
  });
@@ -2960,7 +3151,7 @@ async function sendRuleConfigMessage(port, ruleId, severity, options) {
2960
3151
  if (!resolved) {
2961
3152
  resolved = true;
2962
3153
  clearTimeout(timeout);
2963
- resolve7({ success: false, error: "Connection error" });
3154
+ resolve8({ success: false, error: "Connection error" });
2964
3155
  }
2965
3156
  });
2966
3157
  });
@@ -3086,6 +3277,275 @@ async function config2(action, key, value, extraArg, options = {}) {
3086
3277
  }
3087
3278
  }
3088
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
+
3089
3549
  // src/index.ts
3090
3550
  import { readFileSync as readFileSync3 } from "fs";
3091
3551
  import { dirname as dirname7, join as join5 } from "path";
@@ -3102,7 +3562,7 @@ function assertNodeVersion(minMajor) {
3102
3562
  }
3103
3563
  }
3104
3564
  assertNodeVersion(20);
3105
- var program = new Command();
3565
+ var program = new Command6();
3106
3566
  function getCLIVersion() {
3107
3567
  try {
3108
3568
  const __dirname = dirname7(fileURLToPath(import.meta.url));
@@ -3181,7 +3641,7 @@ program.command("update").description("Update existing style guide with new styl
3181
3641
  });
3182
3642
  });
3183
3643
  program.command("install").description("Install UILint integration").option("--force", "Overwrite existing configuration files").action(async (options) => {
3184
- const { installUI } = await import("./install-ui-TXV7A34M.js");
3644
+ const { installUI } = await import("./install-ui-CCZ3XJDE.js");
3185
3645
  await installUI({ force: options.force });
3186
3646
  });
3187
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) => {
@@ -3228,5 +3688,6 @@ program.command("config").description("Get or set UILint configuration options")
3228
3688
  port: parseInt(options.port, 10)
3229
3689
  });
3230
3690
  });
3691
+ program.addCommand(createDuplicatesCommand());
3231
3692
  program.parse();
3232
3693
  //# sourceMappingURL=index.js.map