@goshenkata/dryscan-cli 1.0.0
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/README.md +56 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/dupes.d.ts.map +1 -0
- package/dist/dupes.js +85 -0
- package/dist/dupes.js.map +1 -0
- package/dist/reports.d.ts +17 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +75 -0
- package/dist/reports.js.map +1 -0
- package/dist/templates/report.hbs +447 -0
- package/dist/uiServer.d.ts +25 -0
- package/dist/uiServer.d.ts.map +1 -0
- package/dist/uiServer.js +188 -0
- package/dist/uiServer.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @goshenkata/dryscan-cli
|
|
2
|
+
|
|
3
|
+
CLI for DryScan - semantic code duplication analyzer.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @goshenkata/dryscan-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Initialize repository
|
|
15
|
+
dryscan init
|
|
16
|
+
|
|
17
|
+
# Find duplicates
|
|
18
|
+
dryscan dupes # Text report
|
|
19
|
+
dryscan dupes --json # JSON output
|
|
20
|
+
dryscan dupes --ui # Web UI at http://localhost:3000
|
|
21
|
+
|
|
22
|
+
# Update index after changes
|
|
23
|
+
dryscan update
|
|
24
|
+
|
|
25
|
+
# Exclude duplicate by short ID
|
|
26
|
+
dryscan dupes exclude abc123
|
|
27
|
+
|
|
28
|
+
# Clean stale exclusions
|
|
29
|
+
dryscan clean
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Create `dryconfig.json` in your repository root:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"threshold": 0.88,
|
|
39
|
+
"minLines": 5,
|
|
40
|
+
"embeddingModel": "embeddinggemma",
|
|
41
|
+
"embeddingSource": "http://localhost:11434",
|
|
42
|
+
"excludedPaths": ["**/test/**"]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Embedding Providers:**
|
|
47
|
+
- Ollama (default): `"embeddingSource": "http://localhost:11434"`
|
|
48
|
+
- Google Gemini: `"embeddingSource": "google"` (requires `GOOGLE_API_KEY` env var set)
|
|
49
|
+
|
|
50
|
+
## Supported languages**
|
|
51
|
+
|
|
52
|
+
Just java for now
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { DryScan, configStore, } from '@goshenkata/dryscan-core';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { handleDupesCommand } from './dupes.js';
|
|
6
|
+
import { applyExclusionFromLatestReport } from './reports.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('dryscan')
|
|
10
|
+
.description('Semantic code duplication analyzer')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Initialize DryScan in the current repository')
|
|
15
|
+
.argument('[path]', 'Repository path', '.')
|
|
16
|
+
.action(async (path) => {
|
|
17
|
+
const repoPath = resolve(path);
|
|
18
|
+
await configStore.init(repoPath);
|
|
19
|
+
const scanner = new DryScan(repoPath);
|
|
20
|
+
await scanner.init();
|
|
21
|
+
console.log('DryScan initialized successfully');
|
|
22
|
+
});
|
|
23
|
+
program
|
|
24
|
+
.command('update')
|
|
25
|
+
.description('Update the DryScan index (incremental scan for changes)')
|
|
26
|
+
.argument('[path]', 'Repository path', '.')
|
|
27
|
+
.action(async (path) => {
|
|
28
|
+
const repoPath = resolve(path);
|
|
29
|
+
await configStore.init(repoPath);
|
|
30
|
+
const scanner = new DryScan(repoPath);
|
|
31
|
+
await scanner.updateIndex();
|
|
32
|
+
console.log('DryScan index updated successfully');
|
|
33
|
+
});
|
|
34
|
+
const dupesCommand = program
|
|
35
|
+
.command('dupes')
|
|
36
|
+
.description('Find duplicate code blocks');
|
|
37
|
+
dupesCommand
|
|
38
|
+
.argument('[path]', 'Repository path', '.')
|
|
39
|
+
.option('--json', 'Output results as JSON')
|
|
40
|
+
.option('--ui', 'Serve interactive report at http://localhost:3000')
|
|
41
|
+
.action(handleDupesCommand);
|
|
42
|
+
dupesCommand
|
|
43
|
+
.command('exclude')
|
|
44
|
+
.description('Add the duplicate pair identified by short id to dryconfig.json from the latest report')
|
|
45
|
+
.argument('<id>', 'Short id from the latest dryscan report')
|
|
46
|
+
.argument('[path]', 'Repository path', '.')
|
|
47
|
+
.action(async (id, path) => {
|
|
48
|
+
const repoPath = resolve(path);
|
|
49
|
+
const { exclusion, added } = await applyExclusionFromLatestReport(repoPath, id);
|
|
50
|
+
if (added) {
|
|
51
|
+
console.log(`Added exclusion: ${exclusion}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(`Exclusion already present: ${exclusion}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
program
|
|
58
|
+
.command('clean')
|
|
59
|
+
.description('Remove excludedPairs entries that no longer match indexed code')
|
|
60
|
+
.argument('[path]', 'Repository path', '.')
|
|
61
|
+
.action(async (path) => {
|
|
62
|
+
const repoPath = resolve(path);
|
|
63
|
+
await configStore.init(repoPath);
|
|
64
|
+
const scanner = new DryScan(repoPath);
|
|
65
|
+
const { kept, removed } = await scanner.cleanExclusions();
|
|
66
|
+
console.log(`Clean complete. Kept ${kept} exclusions, removed ${removed}.`);
|
|
67
|
+
});
|
|
68
|
+
program.parse();
|
|
69
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dupes.d.ts","sourceRoot":"","sources":["../src/dupes.ts"],"names":[],"mappings":"AAQA,KAAK,YAAY,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAsErD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC3F"}
|
package/dist/dupes.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { DryScan, configStore } from '@goshenkata/dryscan-core';
|
|
3
|
+
import { writeDuplicateReport } from './reports.js';
|
|
4
|
+
import { DuplicateReportServer } from './uiServer.js';
|
|
5
|
+
const UI_PORT = 3000;
|
|
6
|
+
function formatCodeSnippet(code, maxLines = 15) {
|
|
7
|
+
const lines = code.split('\n');
|
|
8
|
+
const displayLines = lines.slice(0, maxLines);
|
|
9
|
+
const truncated = lines.length > maxLines;
|
|
10
|
+
const formatted = displayLines
|
|
11
|
+
.map((line, i) => {
|
|
12
|
+
const lineNum = (i + 1).toString().padStart(3, ' ');
|
|
13
|
+
return ` ${lineNum} │ ${line}`;
|
|
14
|
+
})
|
|
15
|
+
.join('\n');
|
|
16
|
+
return formatted + (truncated ? `\n ... │ (${lines.length - maxLines} more lines)` : '');
|
|
17
|
+
}
|
|
18
|
+
function formatDuplicates(report, reportPath) {
|
|
19
|
+
const { duplicates, score, threshold } = report;
|
|
20
|
+
console.log('\n' + '═'.repeat(80));
|
|
21
|
+
console.log(`\n📊 DUPLICATION SCORE: ${score.score.toFixed(2)}% - ${score.grade}`);
|
|
22
|
+
console.log(` Total Lines: ${score.totalLines.toLocaleString()}`);
|
|
23
|
+
console.log(` Duplicate Lines (weighted): ${score.duplicateLines.toLocaleString()}`);
|
|
24
|
+
console.log(` Duplicate Groups: ${score.duplicateGroups}`);
|
|
25
|
+
console.log('\n' + '═'.repeat(80));
|
|
26
|
+
if (reportPath) {
|
|
27
|
+
console.log(`\n🗂 Report saved to ${reportPath}`);
|
|
28
|
+
}
|
|
29
|
+
if (duplicates.length === 0) {
|
|
30
|
+
console.log(`\n✓ No duplicates found (threshold: ${(threshold * 100).toFixed(0)}%)\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(`\n🔍 Found ${duplicates.length} duplicate group(s) (threshold: ${(threshold * 100).toFixed(0)}%)\n`);
|
|
34
|
+
console.log('═'.repeat(80));
|
|
35
|
+
duplicates.forEach((group, index) => {
|
|
36
|
+
const similarityPercent = (group.similarity * 100).toFixed(1);
|
|
37
|
+
const exclusionString = group.exclusionString;
|
|
38
|
+
const shortId = group.shortId;
|
|
39
|
+
console.log(`\n[${index + 1}] Similarity: ${similarityPercent}%`);
|
|
40
|
+
console.log('─'.repeat(80));
|
|
41
|
+
if (shortId) {
|
|
42
|
+
console.log(`Exclusion ID: ${shortId}`);
|
|
43
|
+
}
|
|
44
|
+
if (exclusionString) {
|
|
45
|
+
console.log(`Exclusion key: ${exclusionString}`);
|
|
46
|
+
}
|
|
47
|
+
console.log(`\n📄 ${group.left.filePath}:${group.left.startLine}-${group.left.endLine}`);
|
|
48
|
+
console.log(formatCodeSnippet(group.left.code));
|
|
49
|
+
console.log('\n' + '~'.repeat(40) + ' VS ' + '~'.repeat(40) + '\n');
|
|
50
|
+
console.log(`📄 ${group.right.filePath}:${group.right.startLine}-${group.right.endLine}`);
|
|
51
|
+
console.log(formatCodeSnippet(group.right.code));
|
|
52
|
+
if (index < duplicates.length - 1) {
|
|
53
|
+
console.log('\n' + '═'.repeat(80));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
console.log('\n' + '═'.repeat(80) + '\n');
|
|
57
|
+
}
|
|
58
|
+
export async function handleDupesCommand(path, options) {
|
|
59
|
+
const repoPath = resolve(path);
|
|
60
|
+
await configStore.init(repoPath);
|
|
61
|
+
const scanner = new DryScan(repoPath);
|
|
62
|
+
const report = await scanner.buildDuplicateReport();
|
|
63
|
+
const reportPath = await writeDuplicateReport(repoPath, report);
|
|
64
|
+
if (options.ui) {
|
|
65
|
+
const server = new DuplicateReportServer({
|
|
66
|
+
repoPath,
|
|
67
|
+
threshold: report.threshold,
|
|
68
|
+
duplicates: report.duplicates,
|
|
69
|
+
score: report.score,
|
|
70
|
+
port: UI_PORT,
|
|
71
|
+
});
|
|
72
|
+
await server.start();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (options.json) {
|
|
76
|
+
console.log(JSON.stringify({
|
|
77
|
+
...report,
|
|
78
|
+
reportPath,
|
|
79
|
+
}, null, 2));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
formatDuplicates(report, reportPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=dupes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dupes.js","sourceRoot":"","sources":["../src/dupes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DuplicateReport } from "@goshenkata/dryscan-core";
|
|
2
|
+
/**
|
|
3
|
+
* Writes a timestamped report file under .dry/reports and returns its path.
|
|
4
|
+
*/
|
|
5
|
+
export declare function writeDuplicateReport(repoPath: string, report: DuplicateReport): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Loads the most recently modified report file, returning null when none exist.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadLatestReport(repoPath: string): Promise<DuplicateReport | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Adds the exclusion for a duplicate group referenced by short id from the latest report.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyExclusionFromLatestReport(repoPath: string, shortId: string): Promise<{
|
|
14
|
+
exclusion: string;
|
|
15
|
+
added: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=reports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reports.d.ts","sourceRoot":"","sources":["../src/reports.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAa,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAO3E;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CASrG;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA0BxF;AAED;;GAEG;AACH,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CA6BhD"}
|
package/dist/reports.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { configStore } from "@goshenkata/dryscan-core";
|
|
4
|
+
const REPORT_FILE_PREFIX = "dupes-";
|
|
5
|
+
const DRYSCAN_DIR = ".dry";
|
|
6
|
+
const REPORTS_DIR = "reports";
|
|
7
|
+
/**
|
|
8
|
+
* Writes a timestamped report file under .dry/reports and returns its path.
|
|
9
|
+
*/
|
|
10
|
+
export async function writeDuplicateReport(repoPath, report) {
|
|
11
|
+
const reportDir = join(repoPath, DRYSCAN_DIR, REPORTS_DIR);
|
|
12
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
13
|
+
const safeTimestamp = report.generatedAt.replace(/[:.]/g, "-");
|
|
14
|
+
const fileName = `${REPORT_FILE_PREFIX}${safeTimestamp}.json`;
|
|
15
|
+
const filePath = join(reportDir, fileName);
|
|
16
|
+
await fs.writeFile(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Loads the most recently modified report file, returning null when none exist.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadLatestReport(repoPath) {
|
|
23
|
+
const reportDir = join(repoPath, DRYSCAN_DIR, REPORTS_DIR);
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await fs.readdir(reportDir);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err?.code === "ENOENT")
|
|
30
|
+
return null;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
const reportFiles = await Promise.all(entries
|
|
34
|
+
.filter((name) => name.endsWith(".json"))
|
|
35
|
+
.map(async (name) => {
|
|
36
|
+
const fullPath = join(reportDir, name);
|
|
37
|
+
const stat = await fs.stat(fullPath);
|
|
38
|
+
return { name, fullPath, mtimeMs: stat.mtimeMs };
|
|
39
|
+
}));
|
|
40
|
+
if (reportFiles.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
reportFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
43
|
+
const latest = reportFiles[0];
|
|
44
|
+
const content = await fs.readFile(latest.fullPath, "utf8");
|
|
45
|
+
return JSON.parse(content);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Adds the exclusion for a duplicate group referenced by short id from the latest report.
|
|
49
|
+
*/
|
|
50
|
+
export async function applyExclusionFromLatestReport(repoPath, shortId) {
|
|
51
|
+
const report = await loadLatestReport(repoPath);
|
|
52
|
+
if (!report) {
|
|
53
|
+
throw new Error("No duplicate reports found. Run `dryscan dupes` first.");
|
|
54
|
+
}
|
|
55
|
+
const group = report.duplicates.find((d) => d.shortId === shortId);
|
|
56
|
+
if (!group) {
|
|
57
|
+
throw new Error(`No duplicate group found for id ${shortId}.`);
|
|
58
|
+
}
|
|
59
|
+
if (!group.exclusionString) {
|
|
60
|
+
throw new Error("Duplicate group cannot be excluded because it lacks a pair key.");
|
|
61
|
+
}
|
|
62
|
+
await configStore.init(repoPath);
|
|
63
|
+
const config = await configStore.get(repoPath);
|
|
64
|
+
const alreadyPresent = config.excludedPairs.includes(group.exclusionString);
|
|
65
|
+
if (alreadyPresent) {
|
|
66
|
+
return { exclusion: group.exclusionString, added: false };
|
|
67
|
+
}
|
|
68
|
+
const nextConfig = {
|
|
69
|
+
...config,
|
|
70
|
+
excludedPairs: [...config.excludedPairs, group.exclusionString],
|
|
71
|
+
};
|
|
72
|
+
await configStore.save(repoPath, nextConfig);
|
|
73
|
+
return { exclusion: group.exclusionString, added: true };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reports.js","sourceRoot":"","sources":["../src/reports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>DryScan Duplicates</title>
|
|
7
|
+
<link id="hljs-theme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/styles/line-numbers.min.css" />
|
|
9
|
+
<style>
|
|
10
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0b1021;
|
|
13
|
+
--panel: #0f162f;
|
|
14
|
+
--card: #121b38;
|
|
15
|
+
--border: #1c284a;
|
|
16
|
+
--muted: #9aa6c0;
|
|
17
|
+
--accent: #5eead4;
|
|
18
|
+
--accent-2: #60a5fa;
|
|
19
|
+
--accent-3: #f472b6;
|
|
20
|
+
--shadow: 0 24px 60px rgba(0,0,0,0.35);
|
|
21
|
+
--class: #f59e0b;
|
|
22
|
+
--function: #22d3ee;
|
|
23
|
+
--block: #a855f7;
|
|
24
|
+
--text: #e6edf7;
|
|
25
|
+
--code-bg: #0a0f1e;
|
|
26
|
+
--excellent: #10b981;
|
|
27
|
+
--good: #22d3ee;
|
|
28
|
+
--fair: #f59e0b;
|
|
29
|
+
--poor: #f97316;
|
|
30
|
+
--critical: #ef4444;
|
|
31
|
+
}
|
|
32
|
+
[data-theme='light'] {
|
|
33
|
+
--bg: #f7f9fc;
|
|
34
|
+
--panel: #ffffff;
|
|
35
|
+
--card: #ffffff;
|
|
36
|
+
--border: #d9e2ec;
|
|
37
|
+
--muted: #667799;
|
|
38
|
+
--accent: #2563eb;
|
|
39
|
+
--accent-2: #0ea5e9;
|
|
40
|
+
--accent-3: #ec4899;
|
|
41
|
+
--shadow: 0 16px 40px rgba(15,23,42,0.15);
|
|
42
|
+
--class: #d97706;
|
|
43
|
+
--function: #0ea5e9;
|
|
44
|
+
--block: #8b5cf6;
|
|
45
|
+
--text: #0f172a;
|
|
46
|
+
--code-bg: #f5f7fb;
|
|
47
|
+
--excellent: #059669;
|
|
48
|
+
--good: #0891b2;
|
|
49
|
+
--fair: #d97706;
|
|
50
|
+
--poor: #ea580c;
|
|
51
|
+
--critical: #dc2626;
|
|
52
|
+
}
|
|
53
|
+
* { box-sizing: border-box; }
|
|
54
|
+
body {
|
|
55
|
+
margin: 0;
|
|
56
|
+
background: radial-gradient(circle at 20% 20%, rgba(96,165,250,0.12), transparent 30%),
|
|
57
|
+
radial-gradient(circle at 75% 10%, rgba(244,114,182,0.12), transparent 30%),
|
|
58
|
+
var(--bg);
|
|
59
|
+
color: var(--text);
|
|
60
|
+
font-family: 'Inter', 'IBM Plex Sans', sans-serif;
|
|
61
|
+
min-height: 100vh;
|
|
62
|
+
padding: 28px;
|
|
63
|
+
transition: background 0.3s ease, color 0.3s ease;
|
|
64
|
+
}
|
|
65
|
+
h1 { margin: 0; font-size: 28px; letter-spacing: -0.02em; }
|
|
66
|
+
.sub { color: var(--muted); margin-top: 6px; font-size: 14px; }
|
|
67
|
+
.layout { max-width: 1280px; margin: 0 auto 40px auto; }
|
|
68
|
+
.card {
|
|
69
|
+
background: var(--card);
|
|
70
|
+
border: 1px solid var(--border);
|
|
71
|
+
border-radius: 16px;
|
|
72
|
+
padding: 18px;
|
|
73
|
+
box-shadow: var(--shadow);
|
|
74
|
+
margin-top: 18px;
|
|
75
|
+
transition: background 0.3s ease, border 0.3s ease;
|
|
76
|
+
}
|
|
77
|
+
.header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: space-between; }
|
|
78
|
+
.header-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
79
|
+
.header-right { display: flex; align-items: center; gap: 8px; }
|
|
80
|
+
.pill {
|
|
81
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
82
|
+
padding: 7px 12px; border-radius: 999px;
|
|
83
|
+
background: rgba(255,255,255,0.04);
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
color: var(--text); font-size: 12px; letter-spacing: 0.02em;
|
|
86
|
+
transition: background 0.3s ease, border 0.3s ease, color 0.3s ease;
|
|
87
|
+
}
|
|
88
|
+
.pill .pill-emoji { font-size: 14px; }
|
|
89
|
+
.pill.accent { color: var(--accent); border-color: rgba(94,234,212,0.3); }
|
|
90
|
+
.pill.type { font-weight: 700; text-transform: uppercase; }
|
|
91
|
+
.pill.class { color: var(--class); border-color: rgba(245,158,11,0.35); }
|
|
92
|
+
.pill.function { color: var(--function); border-color: rgba(34,211,238,0.35); }
|
|
93
|
+
.pill.excellent { color: var(--excellent); border-color: rgba(16,185,129,0.35); font-weight: 700; }
|
|
94
|
+
.pill.good { color: var(--good); border-color: rgba(34,211,238,0.35); font-weight: 700; }
|
|
95
|
+
.pill.fair { color: var(--fair); border-color: rgba(245,158,11,0.35); font-weight: 700; }
|
|
96
|
+
.pill.poor { color: var(--poor); border-color: rgba(249,115,22,0.35); font-weight: 700; }
|
|
97
|
+
.pill.critical { color: var(--critical); border-color: rgba(239,68,68,0.35); font-weight: 700; }
|
|
98
|
+
.score-card {
|
|
99
|
+
margin-top: 28px;
|
|
100
|
+
padding: 28px;
|
|
101
|
+
border-radius: 20px;
|
|
102
|
+
border: 1px solid rgba(255,255,255,0.04);
|
|
103
|
+
background: linear-gradient(135deg, rgba(96,165,250,0.08), rgba(13,23,45,0.9));
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
gap: 28px;
|
|
107
|
+
}
|
|
108
|
+
[data-theme='light'] .score-card {
|
|
109
|
+
background: linear-gradient(135deg, rgba(37,99,235,0.08), rgba(255,255,255,0.95));
|
|
110
|
+
border-color: rgba(15,23,42,0.08);
|
|
111
|
+
}
|
|
112
|
+
.score-main { flex: 1 1 240px; }
|
|
113
|
+
.score-label { text-transform: uppercase; font-size: 12px; letter-spacing: 0.2em; color: var(--muted); }
|
|
114
|
+
.score-value { display: flex; align-items: baseline; gap: 18px; margin-top: 12px; }
|
|
115
|
+
.score-number { font-size: 56px; font-weight: 700; letter-spacing: -0.04em; }
|
|
116
|
+
.score-grade { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; text-transform: uppercase; }
|
|
117
|
+
.score-grade.excellent { color: var(--excellent); }
|
|
118
|
+
.score-grade.good { color: var(--good); }
|
|
119
|
+
.score-grade.fair { color: var(--fair); }
|
|
120
|
+
.score-grade.poor { color: var(--poor); }
|
|
121
|
+
.score-grade.critical { color: var(--critical); }
|
|
122
|
+
.score-emoji { font-size: 20px; }
|
|
123
|
+
.score-description { margin-top: 8px; color: var(--muted); font-size: 14px; max-width: 420px; }
|
|
124
|
+
.score-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; flex: 2 1 320px; }
|
|
125
|
+
.metric {
|
|
126
|
+
background: rgba(255,255,255,0.02);
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: 14px;
|
|
129
|
+
padding: 14px 16px;
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
gap: 6px;
|
|
133
|
+
}
|
|
134
|
+
[data-theme='light'] .metric { background: rgba(15,23,42,0.02); }
|
|
135
|
+
.metric-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.16em; color: var(--muted); }
|
|
136
|
+
.metric-value { font-size: 20px; font-weight: 600; }
|
|
137
|
+
.overview-text {
|
|
138
|
+
margin-top: 18px;
|
|
139
|
+
padding: 14px 18px;
|
|
140
|
+
border-radius: 14px;
|
|
141
|
+
background: rgba(255,255,255,0.03);
|
|
142
|
+
border: 1px dashed var(--border);
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
font-size: 15px;
|
|
145
|
+
}
|
|
146
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 14px; margin-top: 14px; }
|
|
147
|
+
.side { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 12px; transition: background 0.3s ease, border 0.3s ease; }
|
|
148
|
+
.path { font-family: 'IBM Plex Mono', monospace; color: var(--muted); font-size: 12px; margin-bottom: 6px; word-break: break-all; }
|
|
149
|
+
.code { background: var(--code-bg); border: 1px solid var(--border); border-radius: 12px; padding: 12px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: var(--text); overflow: auto; max-height: 260px; }
|
|
150
|
+
[data-theme='light'] .code { color: #111827; }
|
|
151
|
+
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
152
|
+
.btn { background: linear-gradient(120deg, var(--accent), var(--accent-2)); color: #0b1021; border: none; padding: 9px 12px; border-radius: 10px; cursor: pointer; font-weight: 700; box-shadow: 0 12px 28px rgba(96,165,250,0.35); transition: transform 0.1s ease; }
|
|
153
|
+
.btn.secondary { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
|
154
|
+
.toggle { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 999px; background: rgba(255,255,255,0.05); border: 1px solid var(--border); color: var(--text); cursor: pointer; font-weight: 600; }
|
|
155
|
+
.toggle-icon { font-size: 14px; }
|
|
156
|
+
.btn:active { transform: translateY(1px); }
|
|
157
|
+
.muted { color: var(--muted); font-size: 12px; }
|
|
158
|
+
.modal { position: fixed; inset: 0; background: rgba(5,7,15,0.7); display: none; align-items: center; justify-content: center; padding: 28px; z-index: 20; }
|
|
159
|
+
.modal.active { display: flex; }
|
|
160
|
+
.modal-content { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; max-width: 1100px; width: 100%; max-height: 92vh; overflow: auto; padding: 18px; box-shadow: var(--shadow); transition: background 0.3s ease, border 0.3s ease; }
|
|
161
|
+
.modal h3 { margin: 0 0 10px 0; }
|
|
162
|
+
.close { float: right; cursor: pointer; color: var(--muted); font-size: 18px; }
|
|
163
|
+
</style>
|
|
164
|
+
</head>
|
|
165
|
+
<body>
|
|
166
|
+
<div class="layout">
|
|
167
|
+
<div class="header">
|
|
168
|
+
<div class="header-left">
|
|
169
|
+
<h1>DryScan Duplicate Report</h1>
|
|
170
|
+
<span class="pill accent">Threshold {{thresholdPct}}%</span>
|
|
171
|
+
<span class="pill {{score.gradeClass}}"><span class="pill-emoji">{{score.emoji}}</span>{{score.scoreRounded}}% · {{score.grade}}</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="header-right">
|
|
174
|
+
<button class="toggle" id="theme-toggle"><span class="toggle-icon">🌙</span><span id="theme-label">Dark</span></button>
|
|
175
|
+
<button class="btn" id="regenerate-btn">Regenerate report</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<section class="score-card">
|
|
179
|
+
<div class="score-main">
|
|
180
|
+
<div class="score-label">Duplication Score</div>
|
|
181
|
+
<div class="score-value">
|
|
182
|
+
<span class="score-number">{{score.scoreRounded}}%</span>
|
|
183
|
+
<span class="score-grade {{score.gradeClass}}">
|
|
184
|
+
<span class="score-emoji">{{score.emoji}}</span>
|
|
185
|
+
{{score.grade}}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
<p class="score-description">Weighted duplicate lines across the entire codebase. Lower numbers indicate healthier reuse.</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="score-metrics">
|
|
191
|
+
<div class="metric">
|
|
192
|
+
<span class="metric-label">Total Lines</span>
|
|
193
|
+
<span class="metric-value">{{score.totalLinesFormatted}}</span>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="metric">
|
|
196
|
+
<span class="metric-label">Duplicate Lines</span>
|
|
197
|
+
<span class="metric-value">{{score.duplicateLinesFormatted}}</span>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="metric">
|
|
200
|
+
<span class="metric-label">Duplicate Groups</span>
|
|
201
|
+
<span class="metric-value">{{score.duplicateGroupsFormatted}}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="metric">
|
|
204
|
+
<span class="metric-label">Similarity Threshold</span>
|
|
205
|
+
<span class="metric-value">{{thresholdPct}}%</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
|
|
210
|
+
<div id="groups"></div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div class="modal" id="modal">
|
|
214
|
+
<div class="modal-content">
|
|
215
|
+
<span class="close" id="modal-close">✕</span>
|
|
216
|
+
<h3 id="modal-title"></h3>
|
|
217
|
+
<pre class="code"><code id="modal-code" class="language-plaintext hljs line-numbers"></code></pre>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<script id="dup-data" type="application/json">{{{duplicatesJson}}}</script>
|
|
222
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
223
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/java.min.js"></script>
|
|
224
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
|
|
225
|
+
<script type="module">
|
|
226
|
+
const dataEl = document.getElementById('dup-data');
|
|
227
|
+
const modal = document.getElementById('modal');
|
|
228
|
+
const modalTitle = document.getElementById('modal-title');
|
|
229
|
+
const modalCode = document.getElementById('modal-code');
|
|
230
|
+
const themeToggle = document.getElementById('theme-toggle');
|
|
231
|
+
const themeLabel = document.getElementById('theme-label');
|
|
232
|
+
const themeLink = document.getElementById('hljs-theme');
|
|
233
|
+
const groups = JSON.parse(dataEl.textContent || '[]');
|
|
234
|
+
const toast = (msg) => alert(msg);
|
|
235
|
+
|
|
236
|
+
const applyTheme = (next) => {
|
|
237
|
+
document.documentElement.setAttribute('data-theme', next);
|
|
238
|
+
localStorage.setItem('dryscan-theme', next);
|
|
239
|
+
themeLabel.textContent = next === 'light' ? 'Light' : 'Dark';
|
|
240
|
+
themeToggle.querySelector('.toggle-icon').textContent = next === 'light' ? '☀️' : '🌙';
|
|
241
|
+
themeLink.setAttribute('href', next === 'light'
|
|
242
|
+
? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css'
|
|
243
|
+
: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css');
|
|
244
|
+
};
|
|
245
|
+
const savedTheme = localStorage.getItem('dryscan-theme');
|
|
246
|
+
if (savedTheme === 'light' || savedTheme === 'dark') applyTheme(savedTheme);
|
|
247
|
+
|
|
248
|
+
themeToggle.addEventListener('click', () => {
|
|
249
|
+
const current = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
250
|
+
applyTheme(current === 'light' ? 'dark' : 'light');
|
|
251
|
+
hljs.highlightAll();
|
|
252
|
+
document.querySelectorAll('code.hljs').forEach(el => hljs.lineNumbersBlock(el));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
document.getElementById('modal-close').addEventListener('click', () => modal.classList.remove('active'));
|
|
256
|
+
modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('active'); });
|
|
257
|
+
|
|
258
|
+
const typeClass = (unitType) => {
|
|
259
|
+
if (unitType === 'class') return 'class';
|
|
260
|
+
if (unitType === 'block') return 'block';
|
|
261
|
+
return 'function';
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const languageClass = (filePath) => {
|
|
265
|
+
const ext = (filePath.split('.').pop() || '').toLowerCase();
|
|
266
|
+
if (ext === 'java') return 'language-java';
|
|
267
|
+
return 'language-plaintext';
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const container = document.getElementById('groups');
|
|
271
|
+
|
|
272
|
+
const render = () => {
|
|
273
|
+
container.innerHTML = '';
|
|
274
|
+
groups.forEach((group, idx) => {
|
|
275
|
+
container.appendChild(renderGroup(group, idx));
|
|
276
|
+
});
|
|
277
|
+
hljs.highlightAll();
|
|
278
|
+
document.querySelectorAll('code.hljs').forEach(el => hljs.lineNumbersBlock(el));
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const regenerateReport = async () => {
|
|
282
|
+
const res = await fetch('/api/regenerate', { method: 'POST' });
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const msg = (await res.json().catch(() => ({}))).error || 'Failed to regenerate report';
|
|
285
|
+
toast(msg);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Refresh the page to pick up the new report
|
|
289
|
+
window.location.reload();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const renderGroup = (group, idx) => {
|
|
293
|
+
const card = document.createElement('div');
|
|
294
|
+
card.className = 'card';
|
|
295
|
+
|
|
296
|
+
const header = document.createElement('div');
|
|
297
|
+
header.className = 'row';
|
|
298
|
+
const title = document.createElement('div');
|
|
299
|
+
title.innerHTML = '<strong>Group ' + (idx + 1) + '</strong> · Similarity ' + (group.similarity * 100).toFixed(1) + '%';
|
|
300
|
+
header.appendChild(title);
|
|
301
|
+
header.appendChild(makePill(group.left.unitType));
|
|
302
|
+
if (group.shortId) {
|
|
303
|
+
const idTag = document.createElement('div');
|
|
304
|
+
idTag.className = 'muted';
|
|
305
|
+
idTag.textContent = 'ID ' + group.shortId;
|
|
306
|
+
header.appendChild(idTag);
|
|
307
|
+
}
|
|
308
|
+
card.appendChild(header);
|
|
309
|
+
|
|
310
|
+
const grid = document.createElement('div');
|
|
311
|
+
grid.className = 'grid';
|
|
312
|
+
grid.appendChild(renderSide(group.left, 'Left', group.similarity));
|
|
313
|
+
grid.appendChild(renderSide(group.right, 'Right', group.similarity));
|
|
314
|
+
card.appendChild(grid);
|
|
315
|
+
|
|
316
|
+
const footer = document.createElement('div');
|
|
317
|
+
footer.className = 'row';
|
|
318
|
+
const excludeBtn = document.createElement('button');
|
|
319
|
+
excludeBtn.className = 'btn secondary';
|
|
320
|
+
excludeBtn.textContent = 'Exclude this pair';
|
|
321
|
+
excludeBtn.dataset.action = 'exclude-pair';
|
|
322
|
+
excludeBtn.dataset.id = group.shortId || '';
|
|
323
|
+
excludeBtn.disabled = !group.shortId;
|
|
324
|
+
footer.appendChild(excludeBtn);
|
|
325
|
+
if (group.exclusionString) {
|
|
326
|
+
const exclusionMeta = document.createElement('div');
|
|
327
|
+
exclusionMeta.className = 'muted';
|
|
328
|
+
exclusionMeta.textContent = 'Exclusion key: ' + group.exclusionString;
|
|
329
|
+
footer.appendChild(exclusionMeta);
|
|
330
|
+
}
|
|
331
|
+
card.appendChild(footer);
|
|
332
|
+
return card;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const renderSide = (side, label, similarity) => {
|
|
336
|
+
const wrap = document.createElement('div');
|
|
337
|
+
wrap.className = 'side';
|
|
338
|
+
|
|
339
|
+
const row = document.createElement('div');
|
|
340
|
+
row.className = 'row';
|
|
341
|
+
const title = document.createElement('div');
|
|
342
|
+
title.innerHTML = '<strong>' + label + '</strong> · ' + side.name;
|
|
343
|
+
row.appendChild(title);
|
|
344
|
+
// row.appendChild(makePill(side.unitType));
|
|
345
|
+
wrap.appendChild(row);
|
|
346
|
+
|
|
347
|
+
const path = document.createElement('div');
|
|
348
|
+
path.className = 'path';
|
|
349
|
+
path.textContent = side.filePath + ':' + side.startLine + '-' + side.endLine;
|
|
350
|
+
wrap.appendChild(path);
|
|
351
|
+
|
|
352
|
+
const codeWrap = document.createElement('pre');
|
|
353
|
+
codeWrap.className = 'code';
|
|
354
|
+
const codeEl = document.createElement('code');
|
|
355
|
+
codeEl.className = languageClass(side.filePath) + ' hljs line-numbers';
|
|
356
|
+
codeEl.textContent = side.code;
|
|
357
|
+
codeWrap.appendChild(codeEl);
|
|
358
|
+
wrap.appendChild(codeWrap);
|
|
359
|
+
|
|
360
|
+
const actions = document.createElement('div');
|
|
361
|
+
actions.className = 'row';
|
|
362
|
+
const btn = document.createElement('button');
|
|
363
|
+
btn.className = 'btn';
|
|
364
|
+
btn.textContent = 'View full file';
|
|
365
|
+
btn.dataset.action = 'view-file';
|
|
366
|
+
btn.dataset.path = side.filePath;
|
|
367
|
+
actions.appendChild(btn);
|
|
368
|
+
const meta = document.createElement('div');
|
|
369
|
+
meta.className = 'muted';
|
|
370
|
+
meta.textContent = 'Lines ' + side.startLine + '-' + side.endLine + ' · Similarity ' + (similarity * 100).toFixed(1) + '%';
|
|
371
|
+
actions.appendChild(meta);
|
|
372
|
+
wrap.appendChild(actions);
|
|
373
|
+
|
|
374
|
+
return wrap;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const makePill = (unitType) => {
|
|
378
|
+
const el = document.createElement('span');
|
|
379
|
+
el.className = 'pill type ' + typeClass(unitType);
|
|
380
|
+
el.textContent = unitType;
|
|
381
|
+
return el;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
document.addEventListener('click', async (event) => {
|
|
385
|
+
const target = event.target;
|
|
386
|
+
if (target && target.dataset && target.dataset.action === 'view-file') {
|
|
387
|
+
const path = target.dataset.path;
|
|
388
|
+
try {
|
|
389
|
+
const res = await fetch('/api/file?path=' + encodeURIComponent(path));
|
|
390
|
+
if (!res.ok) {
|
|
391
|
+
modalTitle.textContent = 'Unable to load file';
|
|
392
|
+
modalCode.textContent = await res.text();
|
|
393
|
+
modalCode.className = 'language-plaintext hljs';
|
|
394
|
+
modal.classList.add('active');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const data = await res.json();
|
|
398
|
+
modalTitle.textContent = data.path;
|
|
399
|
+
modalCode.textContent = data.content;
|
|
400
|
+
modalCode.className = languageClass(data.path) + ' hljs line-numbers';
|
|
401
|
+
modal.classList.add('active');
|
|
402
|
+
hljs.highlightElement(modalCode);
|
|
403
|
+
hljs.lineNumbersBlock(modalCode);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
modalTitle.textContent = 'Network error';
|
|
406
|
+
modalCode.textContent = err?.message || 'Unable to fetch file content.';
|
|
407
|
+
modalCode.className = 'language-plaintext hljs';
|
|
408
|
+
modal.classList.add('active');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (target && target.dataset && target.dataset.action === 'exclude-pair') {
|
|
413
|
+
const id = target.dataset.id;
|
|
414
|
+
if (!id) {
|
|
415
|
+
toast('Missing duplicate id for exclusion.');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
target.disabled = true;
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch('/api/exclusions', {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
headers: { 'content-type': 'application/json' },
|
|
423
|
+
body: JSON.stringify({ id }),
|
|
424
|
+
});
|
|
425
|
+
const body = await res.json();
|
|
426
|
+
if (!res.ok || body.error) {
|
|
427
|
+
throw new Error(body.error || 'Unable to add exclusion');
|
|
428
|
+
}
|
|
429
|
+
toast(body.status === 'already-present'
|
|
430
|
+
? 'Exclusion already present: ' + body.exclusion
|
|
431
|
+
: 'Added exclusion: ' + body.exclusion);
|
|
432
|
+
await regenerateReport();
|
|
433
|
+
} catch (err) {
|
|
434
|
+
toast(err?.message || 'Unable to add exclusion');
|
|
435
|
+
target.disabled = false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
document.getElementById('regenerate-btn')?.addEventListener('click', async () => {
|
|
441
|
+
await regenerateReport();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
render();
|
|
445
|
+
</script>
|
|
446
|
+
</body>
|
|
447
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DuplicateGroup, DuplicationScore } from "@goshenkata/dryscan-core";
|
|
2
|
+
export interface UiServerOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
threshold: number;
|
|
5
|
+
repoPath: string;
|
|
6
|
+
duplicates: DuplicateGroup[];
|
|
7
|
+
score: DuplicationScore;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Responsible for serving the interactive duplicates UI.
|
|
11
|
+
*/
|
|
12
|
+
export declare class DuplicateReportServer {
|
|
13
|
+
private readonly options;
|
|
14
|
+
private readonly port;
|
|
15
|
+
private server?;
|
|
16
|
+
private readonly templatePromise;
|
|
17
|
+
private readonly repoRoot;
|
|
18
|
+
private state;
|
|
19
|
+
private regenerating?;
|
|
20
|
+
private readonly configReady;
|
|
21
|
+
constructor(options: UiServerOptions);
|
|
22
|
+
start(): Promise<void>;
|
|
23
|
+
private regenerateReport;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=uiServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uiServer.d.ts","sourceRoot":"","sources":["../src/uiServer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAIjF,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAYD;;GAEG;AACH,qBAAa,qBAAqB;IASpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuC;IACvE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,KAAK,CAA+E;IAC5F,OAAO,CAAC,YAAY,CAAC,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;gBAEd,OAAO,EAAE,eAAe;IAY/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAiGd,gBAAgB;CAwB/B"}
|
package/dist/uiServer.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolve, join } from "path";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import Handlebars from "handlebars";
|
|
7
|
+
import { DryScan, configStore } from "@goshenkata/dryscan-core";
|
|
8
|
+
import { applyExclusionFromLatestReport, writeDuplicateReport } from "./reports.js";
|
|
9
|
+
const defaultPort = 3000;
|
|
10
|
+
const gradeMeta = {
|
|
11
|
+
Excellent: { emoji: "🌟", className: "excellent" },
|
|
12
|
+
Good: { emoji: "👍", className: "good" },
|
|
13
|
+
Fair: { emoji: "⚠️", className: "fair" },
|
|
14
|
+
Poor: { emoji: "🚨", className: "poor" },
|
|
15
|
+
Critical: { emoji: "🔥", className: "critical" },
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Responsible for serving the interactive duplicates UI.
|
|
19
|
+
*/
|
|
20
|
+
export class DuplicateReportServer {
|
|
21
|
+
options;
|
|
22
|
+
port;
|
|
23
|
+
server;
|
|
24
|
+
templatePromise;
|
|
25
|
+
repoRoot;
|
|
26
|
+
state;
|
|
27
|
+
regenerating;
|
|
28
|
+
configReady;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.port = options.port ?? defaultPort;
|
|
32
|
+
this.templatePromise = loadTemplate();
|
|
33
|
+
this.repoRoot = resolve(options.repoPath);
|
|
34
|
+
this.configReady = configStore.init(this.repoRoot);
|
|
35
|
+
this.state = {
|
|
36
|
+
duplicates: options.duplicates,
|
|
37
|
+
score: options.score,
|
|
38
|
+
threshold: options.threshold,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async start() {
|
|
42
|
+
const template = await this.templatePromise;
|
|
43
|
+
this.server = createServer(async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
46
|
+
if (url.pathname === "/api/duplicates") {
|
|
47
|
+
res.setHeader("content-type", "application/json");
|
|
48
|
+
res.end(JSON.stringify(this.state.duplicates));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (url.pathname === "/api/exclusions" && req.method === "POST") {
|
|
52
|
+
try {
|
|
53
|
+
const payload = await readJsonBody(req);
|
|
54
|
+
const id = payload?.id;
|
|
55
|
+
if (!id || typeof id !== "string") {
|
|
56
|
+
res.statusCode = 400;
|
|
57
|
+
res.end(JSON.stringify({ error: "Missing or invalid id" }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const result = await applyExclusionFromLatestReport(this.repoRoot, id);
|
|
61
|
+
await this.regenerateReport();
|
|
62
|
+
res.setHeader("content-type", "application/json");
|
|
63
|
+
res.end(JSON.stringify({
|
|
64
|
+
exclusion: result.exclusion,
|
|
65
|
+
status: result.added ? "added" : "already-present",
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
res.statusCode = 400;
|
|
70
|
+
res.end(JSON.stringify({ error: err?.message || "Unable to apply exclusion" }));
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (url.pathname === "/api/regenerate" && req.method === "POST") {
|
|
75
|
+
try {
|
|
76
|
+
await this.regenerateReport();
|
|
77
|
+
res.setHeader("content-type", "application/json");
|
|
78
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
res.statusCode = 500;
|
|
82
|
+
res.end(JSON.stringify({ error: err?.message || "Unable to regenerate report" }));
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (url.pathname === "/api/file") {
|
|
87
|
+
const relPathParam = url.searchParams.get("path");
|
|
88
|
+
if (!relPathParam) {
|
|
89
|
+
res.statusCode = 400;
|
|
90
|
+
res.end(JSON.stringify({ error: "Missing path" }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const sanitizedPath = relPathParam.replace(/^[/\\]+/, "");
|
|
94
|
+
try {
|
|
95
|
+
const fullPath = resolve(this.repoRoot, sanitizedPath);
|
|
96
|
+
// Prevent escaping the repo folder
|
|
97
|
+
if (!fullPath.startsWith(this.repoRoot + path.sep) && fullPath !== this.repoRoot) {
|
|
98
|
+
res.statusCode = 400;
|
|
99
|
+
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const content = await readFile(fullPath, "utf8");
|
|
103
|
+
res.setHeader("content-type", "application/json");
|
|
104
|
+
res.end(JSON.stringify({ path: sanitizedPath, content }));
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
res.statusCode = 404;
|
|
108
|
+
res.end(JSON.stringify({ error: "Not found", message: err?.message }));
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
113
|
+
const html = template({
|
|
114
|
+
thresholdPct: Math.round(this.state.threshold * 100),
|
|
115
|
+
duplicatesJson: JSON.stringify(this.state.duplicates),
|
|
116
|
+
score: buildScoreView(this.state.score),
|
|
117
|
+
});
|
|
118
|
+
res.end(html);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
res.statusCode = 500;
|
|
122
|
+
res.end(JSON.stringify({ error: "Internal server error", message: err?.message }));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
126
|
+
this.server.on("error", rejectPromise);
|
|
127
|
+
this.server.on("listening", () => resolvePromise());
|
|
128
|
+
this.server.listen(this.port, () => {
|
|
129
|
+
console.log(`\nUI available at http://localhost:${this.port}\n`);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async regenerateReport() {
|
|
134
|
+
if (this.regenerating) {
|
|
135
|
+
return this.regenerating;
|
|
136
|
+
}
|
|
137
|
+
const run = async () => {
|
|
138
|
+
await this.configReady;
|
|
139
|
+
const scanner = new DryScan(this.repoRoot);
|
|
140
|
+
const report = await scanner.buildDuplicateReport();
|
|
141
|
+
await writeDuplicateReport(this.repoRoot, report);
|
|
142
|
+
this.state = {
|
|
143
|
+
duplicates: report.duplicates,
|
|
144
|
+
score: report.score,
|
|
145
|
+
threshold: report.threshold,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
this.regenerating = run();
|
|
149
|
+
try {
|
|
150
|
+
await this.regenerating;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
this.regenerating = undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function loadTemplate() {
|
|
158
|
+
const templatePath = join(fileURLToPath(new URL(".", import.meta.url)), "templates", "report.hbs");
|
|
159
|
+
const source = await readFile(templatePath, "utf8");
|
|
160
|
+
return Handlebars.compile(source, { noEscape: true });
|
|
161
|
+
}
|
|
162
|
+
async function readJsonBody(req) {
|
|
163
|
+
const chunks = [];
|
|
164
|
+
for await (const chunk of req) {
|
|
165
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
166
|
+
}
|
|
167
|
+
if (chunks.length === 0)
|
|
168
|
+
return {};
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
171
|
+
}
|
|
172
|
+
catch (_err) {
|
|
173
|
+
throw new Error("Invalid JSON body");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function buildScoreView(score) {
|
|
177
|
+
const meta = gradeMeta[score.grade] ?? gradeMeta.Fair;
|
|
178
|
+
return {
|
|
179
|
+
...score,
|
|
180
|
+
gradeClass: meta.className,
|
|
181
|
+
emoji: meta.emoji,
|
|
182
|
+
scoreRounded: score.score.toFixed(1),
|
|
183
|
+
totalLinesFormatted: score.totalLines.toLocaleString(),
|
|
184
|
+
duplicateLinesFormatted: score.duplicateLines.toLocaleString(),
|
|
185
|
+
duplicateGroupsFormatted: score.duplicateGroups.toLocaleString(),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=uiServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uiServer.js","sourceRoot":"","sources":["../src/uiServer.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goshenkata/dryscan-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for DryScan - semantic code duplication analyzer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dryscan": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && chmod +x dist/cli.js && mkdir -p dist/templates && cp src/templates/report.hbs dist/templates/report.hbs",
|
|
12
|
+
"clean": "rm -rf dist",
|
|
13
|
+
"test": "npm run test:unit && npm run test:bats",
|
|
14
|
+
"test:unit": "tsx ../node_modules/mocha/bin/mocha \"test/**/*.test.mjs\"",
|
|
15
|
+
"test:bats": "npm run build && bats test/dryscan-cli.bats",
|
|
16
|
+
"coverage": "c8 tsx ../node_modules/mocha/bin/mocha \"test/**/*.test.mjs\""
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cli",
|
|
23
|
+
"code-analysis",
|
|
24
|
+
"duplication-detection"
|
|
25
|
+
],
|
|
26
|
+
"author": "Goshenkata",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@goshenkata/dryscan-core": "^1.0.0",
|
|
30
|
+
"commander": "^14.0.2",
|
|
31
|
+
"handlebars": "^4.7.8"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/handlebars": "^4.1.0",
|
|
35
|
+
"@types/node": "^25.0.3",
|
|
36
|
+
"bats": "^1.13.0",
|
|
37
|
+
"chai": "^6.2.2",
|
|
38
|
+
"mocha": "^11.7.5",
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|