@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.
@@ -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 isToolAvailable('depcheck');
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 isToolAvailable('eslint');
21
+ return true;
23
22
  }
24
23
  async scan(targetDir, options) {
25
24
  const start = Date.now();
@@ -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 isToolAvailable('jscpd');
18
+ return true;
20
19
  }
21
20
  async scan(targetDir, options) {
22
21
  const start = Date.now();
@@ -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
- return isToolAvailable('knip');
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, {
@@ -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 isToolAvailable('madge');
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
- * Key insight: (X+)+ is ALWAYS vulnerable for any X, because the regex
62
- * engine can partition the input into the inner vs outer repetition in
63
- * exponentially many ways. The exception is negated character classes
64
- * with a following delimiter that prevents overlap.
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: ([^,]+,)+ — the comma delimiter prevents backtracking
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 after
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
- * Key insight: (X+)+ is ALWAYS vulnerable for any X, because the regex
270
- * engine can partition the input into the inner vs outer repetition in
271
- * exponentially many ways. The exception is negated character classes
272
- * with a following delimiter that prevents overlap.
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: ([^,]+,)+ — the comma delimiter prevents backtracking
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 after
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
- // Now check for SAFE exceptions:
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 (/\[\^[^\]]+\][+*].*[^\\][\w,;:"'|&]/.test(content)) {
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) — ((\d+)\.?)+ is technically
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 isToolAvailable('source-map-explorer');
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",
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",