@darrenjcoxon/vibeoptimise 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scanners/depcheck.js +1 -2
- package/dist/scanners/eslint-perf.js +1 -2
- package/dist/scanners/jscpd.js +1 -2
- package/dist/scanners/knip.js +14 -2
- package/dist/scanners/madge.js +1 -2
- package/dist/scanners/regex-safety.d.ts +12 -6
- package/dist/scanners/regex-safety.js +38 -15
- package/dist/scanners/source-map-explorer.js +1 -2
- package/dist/utils/tool-installer.d.ts +2 -1
- package/dist/utils/tool-installer.js +7 -13
- package/package.json +1 -1
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
-
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
14
13
|
export class DepcheckScanner {
|
|
15
14
|
name = 'Depcheck';
|
|
16
15
|
description = 'Dependency analyzer — finds unused and missing npm packages';
|
|
17
16
|
category = 'dead-code';
|
|
18
17
|
async isAvailable() {
|
|
19
|
-
return
|
|
18
|
+
return true;
|
|
20
19
|
}
|
|
21
20
|
async scan(targetDir, options) {
|
|
22
21
|
const start = Date.now();
|
|
@@ -13,13 +13,12 @@
|
|
|
13
13
|
import { execSync } from 'child_process';
|
|
14
14
|
import { existsSync } from 'fs';
|
|
15
15
|
import { join } from 'path';
|
|
16
|
-
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
17
16
|
export class EslintPerfScanner {
|
|
18
17
|
name = 'ESLint Perf';
|
|
19
18
|
description = 'Performance-focused linting — import cycles, unused imports, inefficient patterns';
|
|
20
19
|
category = 'code-quality';
|
|
21
20
|
async isAvailable() {
|
|
22
|
-
return
|
|
21
|
+
return true;
|
|
23
22
|
}
|
|
24
23
|
async scan(targetDir, options) {
|
|
25
24
|
const start = Date.now();
|
package/dist/scanners/jscpd.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
-
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
14
13
|
export class JscpdScanner {
|
|
15
14
|
name = 'JSCPD';
|
|
16
15
|
description = 'Copy/paste detector — finds duplicated code blocks across the codebase';
|
|
17
16
|
category = 'duplication';
|
|
18
17
|
async isAvailable() {
|
|
19
|
-
return
|
|
18
|
+
return true;
|
|
20
19
|
}
|
|
21
20
|
async scan(targetDir, options) {
|
|
22
21
|
const start = Date.now();
|
package/dist/scanners/knip.js
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
-
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
14
13
|
export class KnipScanner {
|
|
15
14
|
name = 'Knip';
|
|
16
15
|
description = 'Dead code detector — unused files, exports, dependencies, types';
|
|
17
16
|
category = 'dead-code';
|
|
18
17
|
async isAvailable() {
|
|
19
|
-
|
|
18
|
+
// Always attempt to run — knip will be auto-installed via npx --yes during scan.
|
|
19
|
+
// Errors are handled gracefully in scan().
|
|
20
|
+
return true;
|
|
20
21
|
}
|
|
21
22
|
async scan(targetDir, options) {
|
|
22
23
|
const start = Date.now();
|
|
@@ -33,6 +34,17 @@ export class KnipScanner {
|
|
|
33
34
|
}
|
|
34
35
|
let output;
|
|
35
36
|
try {
|
|
37
|
+
// Ensure knip is available in the project context
|
|
38
|
+
// (knip needs typescript as a peer dep, resolved from project node_modules)
|
|
39
|
+
const hasTypescript = existsSync(join(targetDir, 'node_modules', 'typescript'));
|
|
40
|
+
if (!hasTypescript) {
|
|
41
|
+
return {
|
|
42
|
+
scanner: this.name,
|
|
43
|
+
findings: [],
|
|
44
|
+
summary: 'Skipped — typescript not installed in project (required by Knip)',
|
|
45
|
+
duration: Date.now() - start,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
36
48
|
const excludeArgs = options.includeTests ? '' : '';
|
|
37
49
|
const cmd = `npx --yes knip --reporter json --no-exit-code ${excludeArgs}`;
|
|
38
50
|
output = execSync(cmd, {
|
package/dist/scanners/madge.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
-
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
14
13
|
export class MadgeScanner {
|
|
15
14
|
name = 'Madge';
|
|
16
15
|
description = 'Circular dependency detector — finds import cycles and orphaned modules';
|
|
17
16
|
category = 'performance';
|
|
18
17
|
async isAvailable() {
|
|
19
|
-
return
|
|
18
|
+
return true;
|
|
20
19
|
}
|
|
21
20
|
async scan(targetDir, options) {
|
|
22
21
|
const start = Date.now();
|
|
@@ -58,21 +58,27 @@ export declare class RegexSafetyScanner implements Scanner {
|
|
|
58
58
|
* Check if a group's content contains an inner quantifier that creates
|
|
59
59
|
* backtracking risk when the group itself is quantified.
|
|
60
60
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* KEY RULE: (X+)+ is only dangerous when the regex engine can partition
|
|
62
|
+
* the input between inner/outer repetitions in many ways. A literal
|
|
63
|
+
* delimiter inside the group PREVENTS this because the engine has no
|
|
64
|
+
* ambiguity about where one repetition ends and the next begins.
|
|
65
65
|
*
|
|
66
66
|
* DANGEROUS: (a+)+, (.+)+, (\w+)+, (\s+\w+)+
|
|
67
|
-
* SAFE: (
|
|
67
|
+
* SAFE: (\s*,\s*\d+)+ — comma is a required literal delimiter
|
|
68
|
+
* SAFE: (\s+[A-Z][a-z]+)+ — [A-Z] after \s+ creates unambiguous boundary
|
|
69
|
+
* SAFE: ([^,]+,)+ — negated class + delimiter
|
|
68
70
|
* SAFE: (\d+(?:\.\d+)?) — ? is not a repeating quantifier
|
|
69
|
-
* SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class
|
|
71
|
+
* SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class
|
|
70
72
|
*/
|
|
71
73
|
private hasUnboundedInnerQuantifier;
|
|
72
74
|
/**
|
|
73
75
|
* Check for overlapping alternation with quantifier.
|
|
74
76
|
* Only flags when alternation branches use unbounded matchers that
|
|
75
77
|
* can match the same characters.
|
|
78
|
+
*
|
|
79
|
+
* SAFE: (?:Mr|Mrs|Ms|Dr)\.? — fixed literals, no backtracking risk
|
|
80
|
+
* SAFE: (?:foo|bar)+ — fixed literals, no overlap
|
|
81
|
+
* DANGEROUS: (?:.*|.+)+ — unbounded matchers overlap
|
|
76
82
|
*/
|
|
77
83
|
private hasDangerousAlternation;
|
|
78
84
|
}
|
|
@@ -266,48 +266,66 @@ export class RegexSafetyScanner {
|
|
|
266
266
|
* Check if a group's content contains an inner quantifier that creates
|
|
267
267
|
* backtracking risk when the group itself is quantified.
|
|
268
268
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
269
|
+
* KEY RULE: (X+)+ is only dangerous when the regex engine can partition
|
|
270
|
+
* the input between inner/outer repetitions in many ways. A literal
|
|
271
|
+
* delimiter inside the group PREVENTS this because the engine has no
|
|
272
|
+
* ambiguity about where one repetition ends and the next begins.
|
|
273
273
|
*
|
|
274
274
|
* DANGEROUS: (a+)+, (.+)+, (\w+)+, (\s+\w+)+
|
|
275
|
-
* SAFE: (
|
|
275
|
+
* SAFE: (\s*,\s*\d+)+ — comma is a required literal delimiter
|
|
276
|
+
* SAFE: (\s+[A-Z][a-z]+)+ — [A-Z] after \s+ creates unambiguous boundary
|
|
277
|
+
* SAFE: ([^,]+,)+ — negated class + delimiter
|
|
276
278
|
* SAFE: (\d+(?:\.\d+)?) — ? is not a repeating quantifier
|
|
277
|
-
* SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class
|
|
279
|
+
* SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class
|
|
278
280
|
*/
|
|
279
281
|
hasUnboundedInnerQuantifier(content) {
|
|
280
282
|
// Check for any atom followed by + or * (greedy) inside the group
|
|
281
|
-
// This catches: a+, .+, \w+, \d+, [abc]+, etc.
|
|
282
283
|
const hasInnerQuantifier = /[^?][+*]/.test(content) || /^.[+*]/.test(content);
|
|
283
284
|
if (!hasInnerQuantifier)
|
|
284
285
|
return false;
|
|
285
|
-
//
|
|
286
|
+
// ── SAFE EXCEPTIONS ──
|
|
287
|
+
// Safe: Contains a required literal character (not a metachar) between quantified parts.
|
|
288
|
+
// e.g., \s*,\s*\d+ — the comma is required, preventing ambiguous partitioning.
|
|
289
|
+
// e.g., \s+[A-Z] — the [A-Z] after \s+ creates an unambiguous boundary.
|
|
290
|
+
// Look for: quantifier followed by a literal char or specific char class [A-Z], [a-z], etc.
|
|
291
|
+
if (/[+*]\s*[,;:=|&!@#'".\-\/]/.test(content)) {
|
|
292
|
+
// Has a required literal delimiter after a quantifier — safe
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
if (/[+*]\s*\[(?![\^])(?:[A-Z]|[a-z]|[0-9])/.test(content)) {
|
|
296
|
+
// Has a specific character class (not negated) after quantifier — safe
|
|
297
|
+
// e.g., \s+[A-Z] — the uppercase letter creates unambiguous boundary
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
286
300
|
// Safe: negated char class followed by a delimiter
|
|
287
301
|
// e.g., [^,]+, or [^"]+\" — the delimiter prevents overlap
|
|
288
|
-
if (/\[\^[^\]]+\][+*]
|
|
289
|
-
// Has a negated class + a literal delimiter after — likely safe
|
|
290
|
-
// But only if the delimiter is in the negated class
|
|
302
|
+
if (/\[\^[^\]]+\][+*]/.test(content)) {
|
|
291
303
|
return false;
|
|
292
304
|
}
|
|
293
305
|
// Safe: lazy quantifier (+? or *?) — prevents catastrophic backtracking
|
|
294
|
-
// e.g., ([^*\n]+?)[ \t]+ — the lazy ? means it yields to what follows
|
|
295
306
|
if (/[+*]\?/.test(content) && !/[+*][^?].*[+*]/.test(content)) {
|
|
296
|
-
// Has a lazy quantifier and no other greedy quantifier — safe
|
|
297
307
|
return false;
|
|
298
308
|
}
|
|
299
|
-
// Safe: \d+ is bounded (only digits
|
|
300
|
-
// vulnerable but in practice \d is so bounded it rarely causes issues
|
|
309
|
+
// Safe: \d+ is bounded (only digits, 0-9)
|
|
301
310
|
// Exception: (\d+\s*)+ IS dangerous because \s* can match empty
|
|
302
311
|
if (/\\d[+*]/.test(content) && !/\\[sw.][+*]/.test(content)) {
|
|
303
312
|
return false;
|
|
304
313
|
}
|
|
314
|
+
// Safe: only \s quantifiers with required non-\s between them
|
|
315
|
+
// e.g., \s*,\s* — the comma breaks the chain
|
|
316
|
+
if (/\\s[+*]/.test(content) && /\\s[+*][^\\+*]*[,;:=\-\/.]/.test(content)) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
305
319
|
return true;
|
|
306
320
|
}
|
|
307
321
|
/**
|
|
308
322
|
* Check for overlapping alternation with quantifier.
|
|
309
323
|
* Only flags when alternation branches use unbounded matchers that
|
|
310
324
|
* can match the same characters.
|
|
325
|
+
*
|
|
326
|
+
* SAFE: (?:Mr|Mrs|Ms|Dr)\.? — fixed literals, no backtracking risk
|
|
327
|
+
* SAFE: (?:foo|bar)+ — fixed literals, no overlap
|
|
328
|
+
* DANGEROUS: (?:.*|.+)+ — unbounded matchers overlap
|
|
311
329
|
*/
|
|
312
330
|
hasDangerousAlternation(pattern) {
|
|
313
331
|
const match = pattern.match(/\(([^)]*\|[^)]*)\)\s*[+*{]/);
|
|
@@ -316,6 +334,11 @@ export class RegexSafetyScanner {
|
|
|
316
334
|
const altParts = match[1].split('|');
|
|
317
335
|
if (altParts.length < 2)
|
|
318
336
|
return false;
|
|
337
|
+
// Safe: All branches are fixed literals (no quantifiers inside)
|
|
338
|
+
// e.g., (Mr|Mrs|Ms|Dr) — these can't cause backtracking
|
|
339
|
+
const allLiteral = altParts.every(part => !/[+*{]/.test(part) && !/\\[wsd.]/.test(part));
|
|
340
|
+
if (allLiteral)
|
|
341
|
+
return false;
|
|
319
342
|
// Only flag if BOTH branches contain unbounded matchers
|
|
320
343
|
const hasUnbounded = (part) => /\.[+*]/.test(part) || /\\[ws][+*]/.test(part);
|
|
321
344
|
const unboundedCount = altParts.filter(hasUnbounded).length;
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
-
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
14
13
|
export class SourceMapExplorerScanner {
|
|
15
14
|
name = 'Source Map Explorer';
|
|
16
15
|
description = 'Bundle composition analyzer — shows what code is in your production bundles';
|
|
17
16
|
category = 'bundle-size';
|
|
18
17
|
async isAvailable() {
|
|
19
|
-
return
|
|
18
|
+
return true;
|
|
20
19
|
}
|
|
21
20
|
async scan(targetDir, options) {
|
|
22
21
|
const start = Date.now();
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @param packageName - npm package name (e.g., 'knip', 'jscpd', 'madge')
|
|
16
16
|
* @param versionFlag - flag to check version (default: '--version')
|
|
17
|
+
* @param cwd - optional working directory (needed for tools with peer deps)
|
|
17
18
|
* @returns true if tool is available
|
|
18
19
|
*/
|
|
19
|
-
export declare function isToolAvailable(packageName: string, versionFlag?: string): boolean;
|
|
20
|
+
export declare function isToolAvailable(packageName: string, versionFlag?: string, cwd?: string): boolean;
|
|
20
21
|
/**
|
|
21
22
|
* Build a command that runs a tool via npx --yes.
|
|
22
23
|
* Ensures the tool will be auto-installed if not present.
|
|
@@ -15,33 +15,27 @@ import { execSync } from 'child_process';
|
|
|
15
15
|
*
|
|
16
16
|
* @param packageName - npm package name (e.g., 'knip', 'jscpd', 'madge')
|
|
17
17
|
* @param versionFlag - flag to check version (default: '--version')
|
|
18
|
+
* @param cwd - optional working directory (needed for tools with peer deps)
|
|
18
19
|
* @returns true if tool is available
|
|
19
20
|
*/
|
|
20
|
-
export function isToolAvailable(packageName, versionFlag = '--version') {
|
|
21
|
+
export function isToolAvailable(packageName, versionFlag = '--version', cwd) {
|
|
22
|
+
const opts = { stdio: 'pipe', timeout: 15000, ...(cwd ? { cwd } : {}) };
|
|
23
|
+
const optsLong = { stdio: 'pipe', timeout: 90000, ...(cwd ? { cwd } : {}) };
|
|
21
24
|
// Try 1: Local/cached npx (fast path)
|
|
22
25
|
try {
|
|
23
|
-
execSync(`npx ${packageName} ${versionFlag}`,
|
|
24
|
-
stdio: 'pipe',
|
|
25
|
-
timeout: 15000,
|
|
26
|
-
});
|
|
26
|
+
execSync(`npx ${packageName} ${versionFlag}`, opts);
|
|
27
27
|
return true;
|
|
28
28
|
}
|
|
29
29
|
catch { }
|
|
30
30
|
// Try 2: npx --yes to auto-install (first-time download)
|
|
31
31
|
try {
|
|
32
|
-
execSync(`npx --yes ${packageName} ${versionFlag}`,
|
|
33
|
-
stdio: 'pipe',
|
|
34
|
-
timeout: 90000, // 90s for first download
|
|
35
|
-
});
|
|
32
|
+
execSync(`npx --yes ${packageName} ${versionFlag}`, optsLong);
|
|
36
33
|
return true;
|
|
37
34
|
}
|
|
38
35
|
catch { }
|
|
39
36
|
// Try 3: Global install as fallback
|
|
40
37
|
try {
|
|
41
|
-
execSync(`npm install -g ${packageName} 2>/dev/null && npx ${packageName} ${versionFlag}`,
|
|
42
|
-
stdio: 'pipe',
|
|
43
|
-
timeout: 90000,
|
|
44
|
-
});
|
|
38
|
+
execSync(`npm install -g ${packageName} 2>/dev/null && npx ${packageName} ${versionFlag}`, optsLong);
|
|
45
39
|
return true;
|
|
46
40
|
}
|
|
47
41
|
catch { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darrenjcoxon/vibeoptimise",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "🚀 VibeOptimise — The ultimate codebase optimisation scanner. Find dead code, duplication, circular deps, bundle bloat & more. Get OPTIMISE.md for AI agents to fix everything.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|