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/chunk-P4I4RKBY.js +126 -0
- package/dist/chunk-P4I4RKBY.js.map +1 -0
- package/dist/{chunk-PBEKMDUH.js → chunk-TWUDB36F.js} +62 -54
- package/dist/chunk-TWUDB36F.js.map +1 -0
- package/dist/{chunk-PB5DLLVC.js → chunk-VNANPKR2.js} +256 -30
- package/dist/chunk-VNANPKR2.js.map +1 -0
- package/dist/index.js +481 -20
- package/dist/index.js.map +1 -1
- package/dist/{install-ui-TXV7A34M.js → install-ui-CCZ3XJDE.js} +263 -41
- package/dist/install-ui-CCZ3XJDE.js.map +1 -0
- package/dist/{plan-SIXVCXCK.js → plan-5WHKVACB.js} +95 -40
- package/dist/plan-5WHKVACB.js.map +1 -0
- package/package.json +9 -4
- package/dist/chunk-PB5DLLVC.js.map +0 -1
- package/dist/chunk-PBEKMDUH.js.map +0 -1
- package/dist/install-ui-TXV7A34M.js.map +0 -1
- package/dist/plan-SIXVCXCK.js.map +0 -1
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-
|
|
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((
|
|
259
|
-
resolveTrace =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
2529
|
+
await new Promise((resolve8) => {
|
|
2339
2530
|
process.on("SIGINT", () => {
|
|
2340
2531
|
logInfo("Shutting down...");
|
|
2341
2532
|
wss.close();
|
|
2342
2533
|
fileWatcher?.close();
|
|
2343
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3105
|
+
resolve8(false);
|
|
2915
3106
|
}
|
|
2916
3107
|
});
|
|
2917
3108
|
});
|
|
2918
3109
|
}
|
|
2919
3110
|
async function sendRuleConfigMessage(port, ruleId, severity, options) {
|
|
2920
|
-
return new Promise((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|