devkit-quality 0.1.0 → 0.1.1
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/CHANGELOG.md +93 -0
- package/README.md +38 -15
- package/dist/cli.js +71 -49
- package/dist/cliLogic.js +39 -4
- package/dist/discovery.js +175 -17
- package/dist/glob.js +5 -1
- package/dist/ignore.js +14 -5
- package/dist/moduleGraph.js +4 -1
- package/dist/reporters.js +180 -92
- package/dist/rules/deadCode.js +4 -4
- package/dist/rules/dependencies.js +126 -53
- package/dist/rules/duplication.js +24 -9
- package/dist/rules/errorHandling.js +3 -1
- package/dist/rules/hygiene.js +11 -2
- package/dist/rules/security.js +20 -6
- package/dist/scanner.js +90 -27
- package/dist/scoring.js +53 -15
- package/dist/suppressions.js +28 -4
- package/dist/terminal.js +151 -37
- package/package.json +11 -3
package/dist/terminal.js
CHANGED
|
@@ -3,24 +3,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.ProgressRenderer = exports.color = exports.glyph = void 0;
|
|
7
|
+
exports.terminalWidth = terminalWidth;
|
|
7
8
|
exports.severityColor = severityColor;
|
|
9
|
+
exports.severityBadge = severityBadge;
|
|
8
10
|
exports.scoreColor = scoreColor;
|
|
9
11
|
exports.scoreLabel = scoreLabel;
|
|
12
|
+
exports.scoreGrade = scoreGrade;
|
|
10
13
|
exports.renderScoreBar = renderScoreBar;
|
|
14
|
+
exports.renderStackedBar = renderStackedBar;
|
|
11
15
|
exports.pad = pad;
|
|
12
16
|
exports.padStart = padStart;
|
|
17
|
+
exports.truncateMiddle = truncateMiddle;
|
|
18
|
+
exports.renderBox = renderBox;
|
|
19
|
+
exports.sectionHeading = sectionHeading;
|
|
20
|
+
exports.formatDuration = formatDuration;
|
|
13
21
|
exports.renderCodeFrame = renderCodeFrame;
|
|
14
|
-
exports.waitForNextTick = waitForNextTick;
|
|
15
22
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
23
|
const node_path_1 = __importDefault(require("node:path"));
|
|
17
24
|
const colorEnabled = Boolean(process.env.FORCE_COLOR) || (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
|
|
25
|
+
/** Box-drawing and block glyphs, with a plain-ASCII fallback for dumb terminals. */
|
|
26
|
+
const ascii = process.env.TERM === 'dumb' || Boolean(process.env.DEVKIT_ASCII);
|
|
27
|
+
exports.glyph = ascii
|
|
28
|
+
? { full: '#', empty: '.', tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|', dot: '*', bullet: '-', arrow: '->', check: 'ok', warn: '!', rail: '|', sep: '-' }
|
|
29
|
+
: { full: '█', empty: '░', tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', dot: '●', bullet: '•', arrow: '→', check: '✔', warn: '▲', rail: '┃', sep: '─' };
|
|
18
30
|
function wrap(open, close) {
|
|
19
31
|
return (text) => (colorEnabled ? `\x1b[${open}m${text}\x1b[${close}m` : text);
|
|
20
32
|
}
|
|
21
33
|
exports.color = {
|
|
22
34
|
bold: wrap('1', '22'),
|
|
23
35
|
dim: wrap('2', '22'),
|
|
36
|
+
italic: wrap('3', '23'),
|
|
37
|
+
inverse: wrap('7', '27'),
|
|
24
38
|
red: wrap('31', '39'),
|
|
25
39
|
green: wrap('32', '39'),
|
|
26
40
|
yellow: wrap('33', '39'),
|
|
@@ -28,11 +42,27 @@ exports.color = {
|
|
|
28
42
|
magenta: wrap('35', '39'),
|
|
29
43
|
cyan: wrap('36', '39'),
|
|
30
44
|
gray: wrap('90', '39'),
|
|
31
|
-
white: wrap('37', '39')
|
|
45
|
+
white: wrap('37', '39'),
|
|
46
|
+
brightRed: wrap('91', '39'),
|
|
47
|
+
bgRed: wrap('41;97', '49;39'),
|
|
48
|
+
bgYellow: wrap('43;30', '49;39'),
|
|
49
|
+
bgGreen: wrap('42;30', '49;39'),
|
|
50
|
+
bgCyan: wrap('46;30', '49;39'),
|
|
51
|
+
bgGray: wrap('100;97', '49;39')
|
|
32
52
|
};
|
|
53
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
54
|
+
/** Printable width of a string, ignoring ANSI escape codes. */
|
|
55
|
+
function visibleLength(text) {
|
|
56
|
+
return [...text.replace(ANSI_PATTERN, '')].length;
|
|
57
|
+
}
|
|
58
|
+
function terminalWidth() {
|
|
59
|
+
const columns = process.stdout.columns || 100;
|
|
60
|
+
return Math.max(60, Math.min(columns, 100));
|
|
61
|
+
}
|
|
33
62
|
function severityColor(severity, text) {
|
|
34
63
|
switch (severity) {
|
|
35
64
|
case 'CRITICAL':
|
|
65
|
+
return exports.color.brightRed(exports.color.bold(text));
|
|
36
66
|
case 'HIGH':
|
|
37
67
|
return exports.color.red(text);
|
|
38
68
|
case 'MEDIUM':
|
|
@@ -43,6 +73,22 @@ function severityColor(severity, text) {
|
|
|
43
73
|
return exports.color.gray(text);
|
|
44
74
|
}
|
|
45
75
|
}
|
|
76
|
+
function severityBadge(severity) {
|
|
77
|
+
const label = ` ${severity.padEnd(8)}`;
|
|
78
|
+
if (!colorEnabled)
|
|
79
|
+
return `[${severity}]`.padEnd(10);
|
|
80
|
+
switch (severity) {
|
|
81
|
+
case 'CRITICAL':
|
|
82
|
+
case 'HIGH':
|
|
83
|
+
return exports.color.bgRed(label);
|
|
84
|
+
case 'MEDIUM':
|
|
85
|
+
return exports.color.bgYellow(label);
|
|
86
|
+
case 'LOW':
|
|
87
|
+
return exports.color.bgCyan(label);
|
|
88
|
+
default:
|
|
89
|
+
return exports.color.bgGray(label);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
46
92
|
function scoreColor(score, text) {
|
|
47
93
|
if (score >= 8)
|
|
48
94
|
return exports.color.green(text);
|
|
@@ -57,18 +103,74 @@ function scoreLabel(score) {
|
|
|
57
103
|
return 'Good';
|
|
58
104
|
if (score >= 5)
|
|
59
105
|
return 'Needs work';
|
|
60
|
-
|
|
106
|
+
if (score >= 3)
|
|
107
|
+
return 'Poor';
|
|
108
|
+
return 'Critical';
|
|
109
|
+
}
|
|
110
|
+
function scoreGrade(score) {
|
|
111
|
+
if (score >= 9)
|
|
112
|
+
return 'A';
|
|
113
|
+
if (score >= 7)
|
|
114
|
+
return 'B';
|
|
115
|
+
if (score >= 5)
|
|
116
|
+
return 'C';
|
|
117
|
+
if (score >= 3)
|
|
118
|
+
return 'D';
|
|
119
|
+
return 'F';
|
|
61
120
|
}
|
|
62
121
|
function renderScoreBar(score, width = 28) {
|
|
63
122
|
const filled = Math.round((Math.max(0, Math.min(10, score)) / 10) * width);
|
|
64
|
-
|
|
65
|
-
|
|
123
|
+
return scoreColor(score, exports.glyph.full.repeat(filled)) + exports.color.dim(exports.glyph.empty.repeat(width - filled));
|
|
124
|
+
}
|
|
125
|
+
/** A single horizontal bar split into colored segments, e.g. a severity distribution. */
|
|
126
|
+
function renderStackedBar(segments, width) {
|
|
127
|
+
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
128
|
+
if (total === 0)
|
|
129
|
+
return exports.color.dim(exports.glyph.empty.repeat(width));
|
|
130
|
+
let used = 0;
|
|
131
|
+
return segments
|
|
132
|
+
.map((segment, index) => {
|
|
133
|
+
const isLast = index === segments.length - 1;
|
|
134
|
+
const cells = isLast ? width - used : Math.round((segment.value / total) * width);
|
|
135
|
+
const size = segment.value > 0 ? Math.max(1, Math.min(cells, width - used)) : 0;
|
|
136
|
+
used += size;
|
|
137
|
+
return segment.paint(exports.glyph.full.repeat(size));
|
|
138
|
+
})
|
|
139
|
+
.join('');
|
|
66
140
|
}
|
|
67
141
|
function pad(text, width) {
|
|
68
|
-
|
|
142
|
+
const length = visibleLength(text);
|
|
143
|
+
return length >= width ? text : text + ' '.repeat(width - length);
|
|
69
144
|
}
|
|
70
145
|
function padStart(text, width) {
|
|
71
|
-
|
|
146
|
+
const length = visibleLength(text);
|
|
147
|
+
return length >= width ? text : ' '.repeat(width - length) + text;
|
|
148
|
+
}
|
|
149
|
+
function truncateMiddle(text, width) {
|
|
150
|
+
if (text.length <= width)
|
|
151
|
+
return text;
|
|
152
|
+
const keep = width - 1;
|
|
153
|
+
return `${text.slice(0, Math.ceil(keep / 3))}…${text.slice(text.length - Math.floor((keep * 2) / 3))}`;
|
|
154
|
+
}
|
|
155
|
+
/** A rounded box around pre-rendered lines. */
|
|
156
|
+
function renderBox(content, width) {
|
|
157
|
+
const inner = width - 4;
|
|
158
|
+
return [
|
|
159
|
+
exports.color.dim(`${exports.glyph.tl}${exports.glyph.h.repeat(width - 2)}${exports.glyph.tr}`),
|
|
160
|
+
...content.map((line) => `${exports.color.dim(exports.glyph.v)} ${pad(line, inner)} ${exports.color.dim(exports.glyph.v)}`),
|
|
161
|
+
exports.color.dim(`${exports.glyph.bl}${exports.glyph.h.repeat(width - 2)}${exports.glyph.br}`)
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
function sectionHeading(title, width, detail = '') {
|
|
165
|
+
const label = ` ${exports.color.bold(title.toUpperCase())}${detail ? ` ${exports.color.dim(detail)}` : ''} `;
|
|
166
|
+
return `${exports.color.dim(exports.glyph.h.repeat(2))}${label}${exports.color.dim(exports.glyph.h.repeat(Math.max(0, width - visibleLength(label) - 2)))}`;
|
|
167
|
+
}
|
|
168
|
+
function formatDuration(ms) {
|
|
169
|
+
if (ms < 1000)
|
|
170
|
+
return `${ms}ms`;
|
|
171
|
+
if (ms < 60_000)
|
|
172
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
173
|
+
return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`;
|
|
72
174
|
}
|
|
73
175
|
function renderCodeFrame(repository, relativeFile, line, column, severity) {
|
|
74
176
|
const absolutePath = node_path_1.default.join(repository, relativeFile);
|
|
@@ -83,53 +185,65 @@ function renderCodeFrame(repository, relativeFile, line, column, severity) {
|
|
|
83
185
|
const endLine = Math.min(lines.length, line + 1);
|
|
84
186
|
const gutterWidth = String(endLine).length;
|
|
85
187
|
const output = [];
|
|
86
|
-
const maxWidth =
|
|
188
|
+
const maxWidth = terminalWidth() - gutterWidth - 12;
|
|
87
189
|
const truncate = (text) => (text.length > maxWidth ? `${text.slice(0, maxWidth)}…` : text);
|
|
88
190
|
for (let current = startLine; current <= endLine; current += 1) {
|
|
89
|
-
const rawText = lines[current - 1] ?? '';
|
|
191
|
+
const rawText = (lines[current - 1] ?? '').replace(/\t/g, ' ');
|
|
90
192
|
const text = truncate(rawText);
|
|
91
193
|
const gutter = padStart(String(current), gutterWidth);
|
|
92
194
|
const marker = current === line ? severityColor(severity, '>') : ' ';
|
|
93
195
|
if (current === line) {
|
|
94
|
-
output.push(`
|
|
196
|
+
output.push(` ${marker} ${exports.color.bold(gutter)} ${exports.color.dim(exports.glyph.v)} ${text}`);
|
|
95
197
|
if (column > 0 && column - 1 < maxWidth) {
|
|
96
|
-
output.push(`
|
|
198
|
+
output.push(` ${' '.repeat(gutterWidth)} ${exports.color.dim(exports.glyph.v)} ${' '.repeat(Math.max(0, column - 1))}${severityColor(severity, '^')}`);
|
|
97
199
|
}
|
|
98
200
|
}
|
|
99
201
|
else {
|
|
100
|
-
output.push(`
|
|
202
|
+
output.push(` ${marker} ${exports.color.dim(gutter)} ${exports.color.dim(exports.glyph.v)} ${exports.color.dim(text)}`);
|
|
101
203
|
}
|
|
102
204
|
}
|
|
103
205
|
return output;
|
|
104
206
|
}
|
|
105
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
207
|
+
const SPINNER_FRAMES = ascii ? ['-', '\\', '|', '/'] : ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
208
|
+
/**
|
|
209
|
+
* Live scan progress on stderr. The scan itself is synchronous, so a timer-driven spinner would
|
|
210
|
+
* never get to draw; instead every progress update redraws the line directly (throttled).
|
|
211
|
+
* Writes nothing when stderr is not a TTY, so piped/CI output stays clean.
|
|
212
|
+
*/
|
|
213
|
+
class ProgressRenderer {
|
|
214
|
+
frame = 0;
|
|
215
|
+
lastDraw = 0;
|
|
216
|
+
lastStep = 0;
|
|
217
|
+
enabled = Boolean(process.stderr.isTTY);
|
|
218
|
+
startedAt = Date.now();
|
|
219
|
+
update(progress) {
|
|
220
|
+
if (!this.enabled)
|
|
221
|
+
return;
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const isNewStep = progress.step !== this.lastStep;
|
|
224
|
+
if (!isNewStep && now - this.lastDraw < 60)
|
|
225
|
+
return;
|
|
226
|
+
this.lastDraw = now;
|
|
227
|
+
this.lastStep = progress.step;
|
|
228
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
229
|
+
const width = 20;
|
|
230
|
+
const fraction = (progress.step - 1 + (progress.total ? (progress.current ?? 0) / progress.total : 0)) / progress.totalSteps;
|
|
231
|
+
const filled = Math.round(Math.max(0, Math.min(1, fraction)) * width);
|
|
232
|
+
const bar = exports.color.cyan(exports.glyph.full.repeat(filled)) + exports.color.dim(exports.glyph.empty.repeat(width - filled));
|
|
233
|
+
const counter = progress.total ? exports.color.dim(` ${progress.current}/${progress.total}`) : '';
|
|
234
|
+
const percent = exports.color.dim(`${String(Math.round(fraction * 100)).padStart(3)}%`);
|
|
235
|
+
const elapsed = exports.color.dim(formatDuration(now - this.startedAt));
|
|
236
|
+
process.stderr.write(`\r\x1b[K${exports.color.cyan(SPINNER_FRAMES[this.frame])} ${bar} ${percent} ${progress.label}${counter} ${elapsed}`);
|
|
112
237
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!process.stderr.isTTY) {
|
|
116
|
-
process.stderr.write(`${this.text}\n`);
|
|
238
|
+
finish(message) {
|
|
239
|
+
if (!this.enabled)
|
|
117
240
|
return;
|
|
118
|
-
}
|
|
119
|
-
this.timer = setInterval(() => {
|
|
120
|
-
process.stderr.write(`\r${exports.color.cyan(SPINNER_FRAMES[this.frameIndex])} ${this.text}`);
|
|
121
|
-
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
122
|
-
}, 80);
|
|
241
|
+
process.stderr.write(`\r\x1b[K${exports.color.green(exports.glyph.check)} ${message} ${exports.color.dim(`in ${formatDuration(Date.now() - this.startedAt)}`)}\n`);
|
|
123
242
|
}
|
|
124
|
-
|
|
125
|
-
if (!this.
|
|
243
|
+
fail() {
|
|
244
|
+
if (!this.enabled)
|
|
126
245
|
return;
|
|
127
|
-
clearInterval(this.timer);
|
|
128
|
-
this.timer = undefined;
|
|
129
246
|
process.stderr.write('\r\x1b[K');
|
|
130
247
|
}
|
|
131
248
|
}
|
|
132
|
-
exports.
|
|
133
|
-
function waitForNextTick() {
|
|
134
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
135
|
-
}
|
|
249
|
+
exports.ProgressRenderer = ProgressRenderer;
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devkit-quality",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Deterministic repository quality scanner for JavaScript and TypeScript projects",
|
|
5
5
|
"bin": {
|
|
6
|
-
"devkit
|
|
6
|
+
"devkit": "bin/devkit.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "dist/cli.js",
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"bin",
|
|
12
12
|
"README.md",
|
|
13
|
-
"LICENSE"
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"CHANGELOG.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "tsc -p tsconfig.json",
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
"type": "git",
|
|
25
26
|
"url": "git+https://github.com/Karan071/devkit-cli.git"
|
|
26
27
|
},
|
|
28
|
+
"homepage": "https://github.com/Karan071/devkit-cli#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/Karan071/devkit-cli/issues"
|
|
31
|
+
},
|
|
27
32
|
"keywords": [
|
|
28
33
|
"cli",
|
|
29
34
|
"typescript",
|
|
@@ -34,6 +39,9 @@
|
|
|
34
39
|
"license": "MIT",
|
|
35
40
|
"author": "Karan Chourasia",
|
|
36
41
|
"type": "commonjs",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
|
|
44
|
+
},
|
|
37
45
|
"dependencies": {
|
|
38
46
|
"commander": "^12.1.0",
|
|
39
47
|
"typescript": "^5.7.2"
|