@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 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
@@ -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"}
@@ -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":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,MAAM,kBAAkB,GAAG,QAAQ,CAAC;AACpC,MAAM,WAAW,GAAG,MAAM,CAAC;AAC3B,MAAM,WAAW,GAAG,SAAS,CAAC;AAE9B;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,MAAuB;IAClF,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IAC3D,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,MAAM,aAAa,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAG,GAAG,kBAAkB,GAAG,aAAa,OAAO,CAAC;IAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC3C,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACtE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IACrD,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IAC3D,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,EAAE,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACxC,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC,OAAO;SACJ,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SACxC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IACnD,CAAC,CAAC,CACL,CAAC;IAEF,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE1C,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3D,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAoB,CAAC;AAChD,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,QAAgB,EAChB,OAAe;IAEf,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC;IACnE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,mCAAmC,OAAO,GAAG,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,cAAc,GAAG,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAC5E,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC5D,CAAC;IAED,MAAM,UAAU,GAAc;QAC5B,GAAG,MAAM;QACT,aAAa,EAAE,CAAC,GAAG,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,eAAe,CAAC;KAChE,CAAC;IAEF,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC7C,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC3D,CAAC"}
@@ -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"}
@@ -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
+ }