@darrenjcoxon/vibeoptimise 1.0.1 → 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.
@@ -174,12 +174,23 @@ export class CssEfficiencyScanner {
174
174
  }
175
175
  extractSelectors(content) {
176
176
  const selectors = [];
177
+ // Keyframe stops are NOT selectors — filter them out
178
+ const keyframeStops = new Set(['from', 'to', '0%', '100%', '50%', '25%', '75%']);
177
179
  const matches = content.matchAll(/^([^@{}/\n][^{]*)\{/gm);
178
180
  for (const match of matches) {
179
181
  const sel = match[1].trim();
180
- if (sel && !sel.startsWith('/*') && !sel.startsWith('//')) {
181
- selectors.push(sel);
182
- }
182
+ if (!sel)
183
+ continue;
184
+ if (sel.startsWith('/*') || sel.startsWith('//'))
185
+ continue;
186
+ // Skip keyframe percentage stops and named stops
187
+ if (keyframeStops.has(sel))
188
+ continue;
189
+ if (/^\d+%$/.test(sel))
190
+ continue; // Any percentage like 30%, 60%
191
+ if (/^\d+%\s*,\s*\d+%/.test(sel))
192
+ continue; // Combined like "0%, 100%"
193
+ selectors.push(sel);
183
194
  }
184
195
  return selectors;
185
196
  }
@@ -15,13 +15,7 @@ export class DepcheckScanner {
15
15
  description = 'Dependency analyzer — finds unused and missing npm packages';
16
16
  category = 'dead-code';
17
17
  async isAvailable() {
18
- try {
19
- execSync('npx --yes depcheck --version', { stdio: 'pipe', timeout: 30000 });
20
- return true;
21
- }
22
- catch {
23
- return false;
24
- }
18
+ return true;
25
19
  }
26
20
  async scan(targetDir, options) {
27
21
  const start = Date.now();
@@ -78,19 +72,30 @@ export class DepcheckScanner {
78
72
  'tailwindcss', 'postcss', 'autoprefixer',
79
73
  'husky', 'lint-staged', 'commitlint',
80
74
  '@playwright/test', 'vitest',
75
+ 'dotenv', 'dotenv-cli', 'cross-env', // Used by scripts, not detectable
76
+ 'prisma', '@prisma/client', // Used via generate/migrate, not direct imports
77
+ 'supabase', // CLI tool, not a direct import
81
78
  ]);
82
79
  // Unused dependencies
83
80
  if (report.dependencies && Array.isArray(report.dependencies)) {
84
81
  for (const dep of report.dependencies) {
82
+ // In monorepos, "unused" deps may be consumed via workspace packages
83
+ // that depcheck can't trace — lower severity and note this
84
+ const severity = isMonorepo ? 'medium' : 'high';
85
+ const monorepoNote = isMonorepo
86
+ ? ' Note: in a monorepo, this package may be used by a workspace dependency that depcheck cannot trace.'
87
+ : '';
85
88
  findings.push({
86
89
  id: `depcheck-unused-${dep}`,
87
90
  scanner: this.name,
88
91
  category: 'dead-code',
89
- severity: 'high',
92
+ severity: severity,
90
93
  title: `Unused dependency: ${dep}`,
91
- description: `Package "${dep}" is in dependencies but no usage was detected in the source code.`,
94
+ description: `Package "${dep}" is in dependencies but no usage was detected in the source code.${monorepoNote}`,
92
95
  file: 'package.json',
93
- suggestion: `Run \`npm uninstall ${dep}\` to remove it. This reduces install size and potential supply chain risk.`,
96
+ suggestion: isMonorepo
97
+ ? `Verify "${dep}" isn't used via workspace resolution before removing. Run \`grep -r "${dep}" --include="*.ts" --include="*.tsx" .\` to double-check.`
98
+ : `Run \`npm uninstall ${dep}\` to remove it. This reduces install size and potential supply chain risk.`,
94
99
  estimatedImpact: 'Faster installs, smaller node_modules, reduced attack surface',
95
100
  });
96
101
  }
@@ -18,13 +18,7 @@ export class EslintPerfScanner {
18
18
  description = 'Performance-focused linting — import cycles, unused imports, inefficient patterns';
19
19
  category = 'code-quality';
20
20
  async isAvailable() {
21
- try {
22
- execSync('npx --yes eslint --version', { stdio: 'pipe', timeout: 30000 });
23
- return true;
24
- }
25
- catch {
26
- return false;
27
- }
21
+ return true;
28
22
  }
29
23
  async scan(targetDir, options) {
30
24
  const start = Date.now();
@@ -15,13 +15,7 @@ export class JscpdScanner {
15
15
  description = 'Copy/paste detector — finds duplicated code blocks across the codebase';
16
16
  category = 'duplication';
17
17
  async isAvailable() {
18
- try {
19
- execSync('npx --yes jscpd --version', { stdio: 'pipe', timeout: 30000 });
20
- return true;
21
- }
22
- catch {
23
- return false;
24
- }
18
+ return true;
25
19
  }
26
20
  async scan(targetDir, options) {
27
21
  const start = Date.now();
@@ -15,13 +15,9 @@ export class KnipScanner {
15
15
  description = 'Dead code detector — unused files, exports, dependencies, types';
16
16
  category = 'dead-code';
17
17
  async isAvailable() {
18
- try {
19
- execSync('npx --yes knip --version', { stdio: 'pipe', timeout: 30000 });
20
- return true;
21
- }
22
- catch {
23
- return false;
24
- }
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;
25
21
  }
26
22
  async scan(targetDir, options) {
27
23
  const start = Date.now();
@@ -38,6 +34,17 @@ export class KnipScanner {
38
34
  }
39
35
  let output;
40
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
+ }
41
48
  const excludeArgs = options.includeTests ? '' : '';
42
49
  const cmd = `npx --yes knip --reporter json --no-exit-code ${excludeArgs}`;
43
50
  output = execSync(cmd, {
@@ -15,13 +15,7 @@ export class MadgeScanner {
15
15
  description = 'Circular dependency detector — finds import cycles and orphaned modules';
16
16
  category = 'performance';
17
17
  async isAvailable() {
18
- try {
19
- execSync('npx --yes madge --version', { stdio: 'pipe', timeout: 30000 });
20
- return true;
21
- }
22
- catch {
23
- return false;
24
- }
18
+ return true;
25
19
  }
26
20
  async scan(targetDir, options) {
27
21
  const start = Date.now();
@@ -25,18 +25,60 @@ export declare class RegexSafetyScanner implements Scanner {
25
25
  private extractRegexLiterals;
26
26
  private extractRegExpConstructors;
27
27
  /**
28
- * Only flag genuinely dangerous patterns.
29
- *
30
- * We ONLY report:
31
- * - Critical: Nested quantifiers (guaranteed exponential backtracking)
32
- * - High: Overlapping alternation + quantifier (likely quadratic+)
33
- * - Medium: 3+ greedy .* in same pattern (quadratic risk)
34
- *
35
- * We deliberately DO NOT flag:
36
- * - Unanchored patterns (normal and expected)
37
- * - Long regexes (complexity != vulnerability)
38
- * - Single quantifiers (normal regex usage)
39
- * - Two .* wildcards (common and usually fine)
28
+ * Only flag genuinely dangerous patterns — patterns where the regex engine
29
+ * can enter exponential or polynomial backtracking.
30
+ *
31
+ * KEY INSIGHT: A "nested quantifier" is only dangerous when the inner
32
+ * quantified expression can match the SAME characters as what follows.
33
+ *
34
+ * SAFE examples (we do NOT flag these):
35
+ * - ([^*\n]+?)[ \t]+ → [^*\n] is bounded, can't overlap with [ \t]
36
+ * - (\d+(?:\.\d+)?) → \d is bounded, ? is not a loop quantifier
37
+ * - [\d+(?:\s*,\s*\d+)+] requires , delimiter, can't backtrack
38
+ * - ([\s\S]*?) → lazy quantifier with bounded context
39
+ *
40
+ * DANGEROUS examples (we DO flag these):
41
+ * - (a+)+ → unbounded a+ inside quantified group
42
+ * - (.*)* → .* inside quantified group (classic ReDoS)
43
+ * - (\w+\s+)+ → \w+ and \s+ can alternate indefinitely
40
44
  */
41
45
  private analyzeRegex;
46
+ /**
47
+ * Check for genuinely dangerous nested quantifiers.
48
+ *
49
+ * Only flags when:
50
+ * 1. There's a group with a quantifier: (...)+ or (...)* or (...){n,}
51
+ * 2. Inside that group, there's a quantified UNBOUNDED matcher
52
+ * - . (dot), \w, \s, \d followed by + or *
53
+ * - NOT [^specific]+ which is bounded by the exclusion
54
+ * - NOT (?:...) non-capturing groups with ? (optional, not loop)
55
+ */
56
+ private hasDangerousNestedQuantifier;
57
+ /**
58
+ * Check if a group's content contains an inner quantifier that creates
59
+ * backtracking risk when the group itself is quantified.
60
+ *
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
+ *
66
+ * DANGEROUS: (a+)+, (.+)+, (\w+)+, (\s+\w+)+
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
70
+ * SAFE: (\d+(?:\.\d+)?) — ? is not a repeating quantifier
71
+ * SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class
72
+ */
73
+ private hasUnboundedInnerQuantifier;
74
+ /**
75
+ * Check for overlapping alternation with quantifier.
76
+ * Only flags when alternation branches use unbounded matchers that
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
82
+ */
83
+ private hasDangerousAlternation;
42
84
  }
@@ -186,57 +186,43 @@ export class RegexSafetyScanner {
186
186
  return regexes;
187
187
  }
188
188
  /**
189
- * Only flag genuinely dangerous patterns.
189
+ * Only flag genuinely dangerous patterns — patterns where the regex engine
190
+ * can enter exponential or polynomial backtracking.
190
191
  *
191
- * We ONLY report:
192
- * - Critical: Nested quantifiers (guaranteed exponential backtracking)
193
- * - High: Overlapping alternation + quantifier (likely quadratic+)
194
- * - Medium: 3+ greedy .* in same pattern (quadratic risk)
192
+ * KEY INSIGHT: A "nested quantifier" is only dangerous when the inner
193
+ * quantified expression can match the SAME characters as what follows.
195
194
  *
196
- * We deliberately DO NOT flag:
197
- * - Unanchored patterns (normal and expected)
198
- * - Long regexes (complexity != vulnerability)
199
- * - Single quantifiers (normal regex usage)
200
- * - Two .* wildcards (common and usually fine)
195
+ * SAFE examples (we do NOT flag these):
196
+ * - ([^*\n]+?)[ \t]+ → [^*\n] is bounded, can't overlap with [ \t]
197
+ * - (\d+(?:\.\d+)?) → \d is bounded, ? is not a loop quantifier
198
+ * - [\d+(?:\s*,\s*\d+)+] requires , delimiter, can't backtrack
199
+ * - ([\s\S]*?) → lazy quantifier with bounded context
200
+ *
201
+ * DANGEROUS examples (we DO flag these):
202
+ * - (a+)+ → unbounded a+ inside quantified group
203
+ * - (.*)* → .* inside quantified group (classic ReDoS)
204
+ * - (\w+\s+)+ → \w+ and \s+ can alternate indefinitely
201
205
  */
202
206
  analyzeRegex(pattern) {
203
207
  const issues = [];
204
- // Critical: Nested quantifiers — (a+)+, (.*)*
205
- if (/\([^)]*[+*][^)]*\)\s*[+*{]/.test(pattern) ||
206
- /\([^)]*[+*][^)]*[+*][^)]*\)/.test(pattern)) {
208
+ // Critical: TRUE nested quantifiers only
209
+ // Pattern: (X+)+ or (X*)+ where X is an UNBOUNDED matcher (. or \w or \s)
210
+ // NOT: ([^specific]+)+ which is bounded and safe
211
+ if (this.hasDangerousNestedQuantifier(pattern)) {
207
212
  issues.push({
208
213
  severity: 'critical',
209
214
  description: 'Nested quantifiers — exponential backtracking risk (ReDoS)',
210
215
  suggestion: 'Rewrite to avoid nested quantifiers. Instead of `(a+)+`, use `a+`. For complex patterns, consider the RE2 engine (npm re2) which guarantees linear time.',
211
216
  });
212
217
  }
213
- // High: Overlapping alternation with quantifier (a|ab)+
214
- if (/\([^)]*\|[^)]*\)[+*{]/.test(pattern)) {
215
- const groupMatch = pattern.match(/\(([^)]*)\)\s*[+*{]/);
216
- if (groupMatch) {
217
- const altParts = groupMatch[1].split('|');
218
- if (altParts.length > 1) {
219
- const hasOverlap = altParts.some((a, i) => altParts.some((b, j) => {
220
- if (i === j)
221
- return false;
222
- // Both contain wildcards — definite overlap
223
- if ((a.includes('.') || a.includes('\\w') || a.includes('\\d')) &&
224
- (b.includes('.') || b.includes('\\w') || b.includes('\\d')))
225
- return true;
226
- // Same starting char — potential overlap
227
- const aFirst = a.replace(/^\\/, '')[0];
228
- const bFirst = b.replace(/^\\/, '')[0];
229
- return aFirst && bFirst && aFirst === bFirst;
230
- }));
231
- if (hasOverlap) {
232
- issues.push({
233
- severity: 'high',
234
- description: 'Overlapping alternation with quantifier — potential backtracking',
235
- suggestion: 'Ensure alternation branches are mutually exclusive, or rewrite to avoid the quantifier on the group.',
236
- });
237
- }
238
- }
239
- }
218
+ // High: Overlapping alternation with quantifier where branches can match same input
219
+ // Only flag when alternatives use unbounded matchers (., \w, \s)
220
+ if (this.hasDangerousAlternation(pattern)) {
221
+ issues.push({
222
+ severity: 'high',
223
+ description: 'Overlapping alternation with quantifier — potential backtracking',
224
+ suggestion: 'Ensure alternation branches are mutually exclusive, or rewrite to avoid the quantifier on the group.',
225
+ });
240
226
  }
241
227
  // Medium: 3+ greedy wildcards (2 is normal, 3+ is risky)
242
228
  const wildcardCount = (pattern.match(/\.\*/g) || []).length;
@@ -249,4 +235,113 @@ export class RegexSafetyScanner {
249
235
  }
250
236
  return issues;
251
237
  }
238
+ /**
239
+ * Check for genuinely dangerous nested quantifiers.
240
+ *
241
+ * Only flags when:
242
+ * 1. There's a group with a quantifier: (...)+ or (...)* or (...){n,}
243
+ * 2. Inside that group, there's a quantified UNBOUNDED matcher
244
+ * - . (dot), \w, \s, \d followed by + or *
245
+ * - NOT [^specific]+ which is bounded by the exclusion
246
+ * - NOT (?:...) non-capturing groups with ? (optional, not loop)
247
+ */
248
+ hasDangerousNestedQuantifier(pattern) {
249
+ // Match groups followed by quantifiers: (...)+ or (...)* or (...){2,}
250
+ const groupWithQuantifier = /\(([^)]+)\)[+*]|\(([^)]+)\)\{[\d,]+\}/g;
251
+ let match;
252
+ while ((match = groupWithQuantifier.exec(pattern)) !== null) {
253
+ const groupContent = match[1] || match[2];
254
+ if (!groupContent)
255
+ continue;
256
+ // Check if group contains an unbounded quantified expression
257
+ // Dangerous: .+, .*, \w+, \w*, \s+, \s*
258
+ // Safe: [^x]+, [specific]+, \d+ (bounded), lazy .*? in limited context
259
+ if (this.hasUnboundedInnerQuantifier(groupContent)) {
260
+ return true;
261
+ }
262
+ }
263
+ return false;
264
+ }
265
+ /**
266
+ * Check if a group's content contains an inner quantifier that creates
267
+ * backtracking risk when the group itself is quantified.
268
+ *
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
+ *
274
+ * DANGEROUS: (a+)+, (.+)+, (\w+)+, (\s+\w+)+
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
278
+ * SAFE: (\d+(?:\.\d+)?) — ? is not a repeating quantifier
279
+ * SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class
280
+ */
281
+ hasUnboundedInnerQuantifier(content) {
282
+ // Check for any atom followed by + or * (greedy) inside the group
283
+ const hasInnerQuantifier = /[^?][+*]/.test(content) || /^.[+*]/.test(content);
284
+ if (!hasInnerQuantifier)
285
+ return false;
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
+ }
300
+ // Safe: negated char class followed by a delimiter
301
+ // e.g., [^,]+, or [^"]+\" — the delimiter prevents overlap
302
+ if (/\[\^[^\]]+\][+*]/.test(content)) {
303
+ return false;
304
+ }
305
+ // Safe: lazy quantifier (+? or *?) — prevents catastrophic backtracking
306
+ if (/[+*]\?/.test(content) && !/[+*][^?].*[+*]/.test(content)) {
307
+ return false;
308
+ }
309
+ // Safe: \d+ is bounded (only digits, 0-9)
310
+ // Exception: (\d+\s*)+ IS dangerous because \s* can match empty
311
+ if (/\\d[+*]/.test(content) && !/\\[sw.][+*]/.test(content)) {
312
+ return false;
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
+ }
319
+ return true;
320
+ }
321
+ /**
322
+ * Check for overlapping alternation with quantifier.
323
+ * Only flags when alternation branches use unbounded matchers that
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
329
+ */
330
+ hasDangerousAlternation(pattern) {
331
+ const match = pattern.match(/\(([^)]*\|[^)]*)\)\s*[+*{]/);
332
+ if (!match)
333
+ return false;
334
+ const altParts = match[1].split('|');
335
+ if (altParts.length < 2)
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;
342
+ // Only flag if BOTH branches contain unbounded matchers
343
+ const hasUnbounded = (part) => /\.[+*]/.test(part) || /\\[ws][+*]/.test(part);
344
+ const unboundedCount = altParts.filter(hasUnbounded).length;
345
+ return unboundedCount >= 2;
346
+ }
252
347
  }
@@ -15,13 +15,7 @@ export class SourceMapExplorerScanner {
15
15
  description = 'Bundle composition analyzer — shows what code is in your production bundles';
16
16
  category = 'bundle-size';
17
17
  async isAvailable() {
18
- try {
19
- execSync('npx --yes source-map-explorer --version', { stdio: 'pipe', timeout: 30000 });
20
- return true;
21
- }
22
- catch {
23
- return false;
24
- }
18
+ return true;
25
19
  }
26
20
  async scan(targetDir, options) {
27
21
  const start = Date.now();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Tool Installer Utility
3
+ *
4
+ * Robust availability checking for npm-based scanning tools.
5
+ * Tries local npx → npx --yes → global install as fallback.
6
+ */
7
+ /**
8
+ * Check if an npm tool is available, installing it if needed.
9
+ *
10
+ * Strategy:
11
+ * 1. Try npx (uses local or cached version — fast)
12
+ * 2. Try npx --yes (auto-installs if not cached — slower)
13
+ * 3. Try global install as last resort
14
+ *
15
+ * @param packageName - npm package name (e.g., 'knip', 'jscpd', 'madge')
16
+ * @param versionFlag - flag to check version (default: '--version')
17
+ * @param cwd - optional working directory (needed for tools with peer deps)
18
+ * @returns true if tool is available
19
+ */
20
+ export declare function isToolAvailable(packageName: string, versionFlag?: string, cwd?: string): boolean;
21
+ /**
22
+ * Build a command that runs a tool via npx --yes.
23
+ * Ensures the tool will be auto-installed if not present.
24
+ */
25
+ export declare function npxCmd(packageName: string, args: string): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Tool Installer Utility
3
+ *
4
+ * Robust availability checking for npm-based scanning tools.
5
+ * Tries local npx → npx --yes → global install as fallback.
6
+ */
7
+ import { execSync } from 'child_process';
8
+ /**
9
+ * Check if an npm tool is available, installing it if needed.
10
+ *
11
+ * Strategy:
12
+ * 1. Try npx (uses local or cached version — fast)
13
+ * 2. Try npx --yes (auto-installs if not cached — slower)
14
+ * 3. Try global install as last resort
15
+ *
16
+ * @param packageName - npm package name (e.g., 'knip', 'jscpd', 'madge')
17
+ * @param versionFlag - flag to check version (default: '--version')
18
+ * @param cwd - optional working directory (needed for tools with peer deps)
19
+ * @returns true if tool is available
20
+ */
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 } : {}) };
24
+ // Try 1: Local/cached npx (fast path)
25
+ try {
26
+ execSync(`npx ${packageName} ${versionFlag}`, opts);
27
+ return true;
28
+ }
29
+ catch { }
30
+ // Try 2: npx --yes to auto-install (first-time download)
31
+ try {
32
+ execSync(`npx --yes ${packageName} ${versionFlag}`, optsLong);
33
+ return true;
34
+ }
35
+ catch { }
36
+ // Try 3: Global install as fallback
37
+ try {
38
+ execSync(`npm install -g ${packageName} 2>/dev/null && npx ${packageName} ${versionFlag}`, optsLong);
39
+ return true;
40
+ }
41
+ catch { }
42
+ return false;
43
+ }
44
+ /**
45
+ * Build a command that runs a tool via npx --yes.
46
+ * Ensures the tool will be auto-installed if not present.
47
+ */
48
+ export function npxCmd(packageName, args) {
49
+ return `npx --yes ${packageName} ${args}`;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darrenjcoxon/vibeoptimise",
3
- "version": "1.0.1",
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",