@darrenjcoxon/vibeoptimise 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 +122 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +81 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/orchestrator.d.ts +16 -0
- package/dist/orchestrator.js +201 -0
- package/dist/reporter.d.ts +15 -0
- package/dist/reporter.js +140 -0
- package/dist/scanners/bundlephobia.d.ts +19 -0
- package/dist/scanners/bundlephobia.js +189 -0
- package/dist/scanners/css-efficiency.d.ts +15 -0
- package/dist/scanners/css-efficiency.js +210 -0
- package/dist/scanners/depcheck.d.ts +17 -0
- package/dist/scanners/depcheck.js +133 -0
- package/dist/scanners/eslint-perf.d.ts +23 -0
- package/dist/scanners/eslint-perf.js +209 -0
- package/dist/scanners/index.d.ts +26 -0
- package/dist/scanners/index.js +53 -0
- package/dist/scanners/jscpd.d.ts +17 -0
- package/dist/scanners/jscpd.js +150 -0
- package/dist/scanners/knip.d.ts +17 -0
- package/dist/scanners/knip.js +248 -0
- package/dist/scanners/madge.d.ts +19 -0
- package/dist/scanners/madge.js +162 -0
- package/dist/scanners/query-efficiency.d.ts +20 -0
- package/dist/scanners/query-efficiency.js +232 -0
- package/dist/scanners/regex-safety.d.ts +21 -0
- package/dist/scanners/regex-safety.js +221 -0
- package/dist/scanners/source-map-explorer.d.ts +21 -0
- package/dist/scanners/source-map-explorer.js +177 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +35 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# 🚀 VibeOptimise
|
|
2
|
+
|
|
3
|
+
**The ultimate codebase optimisation scanner for AI-assisted development.**
|
|
4
|
+
|
|
5
|
+
Scan your codebase for dead code, duplication, circular dependencies, bundle bloat, inefficient queries, and more — then get an AI-actionable `OPTIMISE.md` for Claude Code, Cursor, or any AI agent to fix everything.
|
|
6
|
+
|
|
7
|
+
> **By the creator of [Vibeguard](https://www.npmjs.com/package/@darrenjcoxon/vibeguard)** — the security scanner for vibe coders.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @darrenjcoxon/vibeoptimise
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it. Run it from the root of any JS/TS project.
|
|
16
|
+
|
|
17
|
+
## What It Scans
|
|
18
|
+
|
|
19
|
+
VibeOptimise runs **10 parallel scanners** that cover every dimension of codebase efficiency:
|
|
20
|
+
|
|
21
|
+
| # | Scanner | What It Finds | Tool |
|
|
22
|
+
|---|---------|---------------|------|
|
|
23
|
+
| 1 | **Knip** | Unused files, exports, dependencies, types | [knip](https://knip.dev) |
|
|
24
|
+
| 2 | **JSCPD** | Copy/pasted code blocks across the codebase | [jscpd](https://jscpd.dev) |
|
|
25
|
+
| 3 | **Madge** | Circular dependencies & orphaned modules | [madge](https://github.com/pahen/madge) |
|
|
26
|
+
| 4 | **Depcheck** | Unused & missing npm packages | [depcheck](https://github.com/depcheck/depcheck) |
|
|
27
|
+
| 5 | **BundlePhobia** | Heavy deps with lighter alternatives | [bundlephobia](https://bundlephobia.com) |
|
|
28
|
+
| 6 | **ESLint Perf** | Import cycles, barrel file bloat, unused imports | [eslint](https://eslint.org) |
|
|
29
|
+
| 7 | **Source Map Explorer** | Bundle composition analysis | [source-map-explorer](https://github.com/danvk/source-map-explorer) |
|
|
30
|
+
| 8 | **Regex Safety** | ReDoS-vulnerable patterns & catastrophic backtracking | Built-in |
|
|
31
|
+
| 9 | **CSS Efficiency** | Unused CSS, duplicate selectors, !important overuse | Built-in |
|
|
32
|
+
| 10 | **Query Efficiency** | N+1 patterns, unbounded queries, overfetching | Built-in |
|
|
33
|
+
|
|
34
|
+
## Output: OPTIMISE.md
|
|
35
|
+
|
|
36
|
+
The report is structured for AI agents to work through systematically:
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
# OPTIMISE.md — Codebase Optimisation Report
|
|
40
|
+
|
|
41
|
+
## 🔴 Critical — Immediate Performance Impact
|
|
42
|
+
- Circular dependencies causing bundle bloat
|
|
43
|
+
- ReDoS-vulnerable regex patterns
|
|
44
|
+
- N+1 query patterns
|
|
45
|
+
|
|
46
|
+
## 🟠 High — Significant Optimisation Opportunities
|
|
47
|
+
- Unused dependencies (12 packages, ~45MB savings)
|
|
48
|
+
- Unbounded database queries
|
|
49
|
+
|
|
50
|
+
## 🟡 Medium — Recommended Improvements
|
|
51
|
+
- Duplicated code blocks (with file pairs + line ranges)
|
|
52
|
+
- Heavy deps with lighter alternatives
|
|
53
|
+
|
|
54
|
+
## 🔵 Low — Nice to Have
|
|
55
|
+
- Unused exports, console.log statements
|
|
56
|
+
- CSS shorthand opportunities
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Scan current directory
|
|
63
|
+
npx @darrenjcoxon/vibeoptimise
|
|
64
|
+
|
|
65
|
+
# Scan a specific project
|
|
66
|
+
npx @darrenjcoxon/vibeoptimise /path/to/project
|
|
67
|
+
|
|
68
|
+
# Quiet mode — just generate the report
|
|
69
|
+
npx @darrenjcoxon/vibeoptimise --quiet
|
|
70
|
+
|
|
71
|
+
# JSON output (for CI/CD integration)
|
|
72
|
+
npx @darrenjcoxon/vibeoptimise --json
|
|
73
|
+
|
|
74
|
+
# Include test files in analysis
|
|
75
|
+
npx @darrenjcoxon/vibeoptimise --include-tests
|
|
76
|
+
|
|
77
|
+
# Exclude specific patterns
|
|
78
|
+
npx @darrenjcoxon/vibeoptimise --exclude "legacy/**" "vendor/**"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## AI Agent Workflow
|
|
82
|
+
|
|
83
|
+
1. Run `npx @darrenjcoxon/vibeoptimise`
|
|
84
|
+
2. Feed `OPTIMISE.md` to your AI agent
|
|
85
|
+
3. Tell it: *"Read OPTIMISE.md and implement all fixes, starting from Critical"*
|
|
86
|
+
4. Re-run VibeOptimise to verify improvements
|
|
87
|
+
|
|
88
|
+
Works with **Claude Code**, **Cursor**, **GitHub Copilot**, **Windsurf**, and any AI coding assistant.
|
|
89
|
+
|
|
90
|
+
## CI/CD Integration
|
|
91
|
+
|
|
92
|
+
VibeOptimise exits with code 1 when critical or high findings exist — perfect for CI quality gates:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
# GitHub Actions
|
|
96
|
+
- name: Check codebase optimisation
|
|
97
|
+
run: npx @darrenjcoxon/vibeoptimise --quiet
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## ORM Support
|
|
101
|
+
|
|
102
|
+
The Query Efficiency scanner understands:
|
|
103
|
+
- **Supabase** — `.from()` without `.select()` or `.limit()`
|
|
104
|
+
- **Prisma** — Unbounded `findMany()`, deeply nested includes
|
|
105
|
+
- **Drizzle** — Queries without `.limit()`
|
|
106
|
+
- **Mongoose** — `find({})` without limit, missing `.lean()`
|
|
107
|
+
- **Raw SQL** — `SELECT *`, missing `LIMIT`
|
|
108
|
+
|
|
109
|
+
## Companion Tool: Vibeguard
|
|
110
|
+
|
|
111
|
+
Use VibeOptimise alongside [Vibeguard](https://www.npmjs.com/package/@darrenjcoxon/vibeguard) for complete coverage:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx @darrenjcoxon/vibeguard # Security scanning → FIXES.md
|
|
115
|
+
npx @darrenjcoxon/vibeoptimise # Optimisation scanning → OPTIMISE.md
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT — Free for everyone. Built for the vibe coding community. 🤙
|
|
121
|
+
|
|
122
|
+
**Made by [Darren Coxon](https://github.com/darrenjcoxon)**
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VibeOptimise CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @darrenjcoxon/vibeoptimise # Scan current directory
|
|
7
|
+
* npx @darrenjcoxon/vibeoptimise /path # Scan specific directory
|
|
8
|
+
* npx @darrenjcoxon/vibeoptimise --help # Show help
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VibeOptimise CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @darrenjcoxon/vibeoptimise # Scan current directory
|
|
7
|
+
* npx @darrenjcoxon/vibeoptimise /path # Scan specific directory
|
|
8
|
+
* npx @darrenjcoxon/vibeoptimise --help # Show help
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
import { Orchestrator } from './orchestrator.js';
|
|
15
|
+
import { Reporter } from './reporter.js';
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name('vibeoptimise')
|
|
19
|
+
.description('🚀 The ultimate codebase optimisation scanner — find dead code, duplication, circular deps, bundle bloat & more')
|
|
20
|
+
.version('1.0.0')
|
|
21
|
+
.argument('[directory]', 'Directory to scan (default: current directory)', '.')
|
|
22
|
+
.option('-q, --quiet', 'Minimal output — just generate OPTIMISE.md')
|
|
23
|
+
.option('-v, --verbose', 'Show detailed scanner output')
|
|
24
|
+
.option('--include-tests', 'Include test files in analysis')
|
|
25
|
+
.option('--include-css', 'Force CSS scanning even if no CSS files detected')
|
|
26
|
+
.option('--include-bundle', 'Force bundle analysis (requires build output)')
|
|
27
|
+
.option('--json', 'Output results as JSON instead of OPTIMISE.md')
|
|
28
|
+
.option('--no-report', 'Skip generating OPTIMISE.md file')
|
|
29
|
+
.option('-e, --exclude <patterns...>', 'Additional glob patterns to exclude')
|
|
30
|
+
.action(async (directory, opts) => {
|
|
31
|
+
try {
|
|
32
|
+
const targetDir = resolve(directory);
|
|
33
|
+
if (!existsSync(targetDir)) {
|
|
34
|
+
console.error(chalk.red(`\n ✖ Directory not found: ${targetDir}\n`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const options = {
|
|
38
|
+
targetDir,
|
|
39
|
+
quiet: opts.quiet || false,
|
|
40
|
+
verbose: opts.verbose || false,
|
|
41
|
+
includeTests: opts.includeTests || false,
|
|
42
|
+
includeCss: opts.includeCss || false,
|
|
43
|
+
includeBundle: opts.includeBundle || false,
|
|
44
|
+
exclude: opts.exclude || [],
|
|
45
|
+
};
|
|
46
|
+
// Run the orchestrator
|
|
47
|
+
const orchestrator = new Orchestrator();
|
|
48
|
+
const report = await orchestrator.run(options);
|
|
49
|
+
// Output results
|
|
50
|
+
if (opts.json) {
|
|
51
|
+
console.log(JSON.stringify(report, null, 2));
|
|
52
|
+
}
|
|
53
|
+
else if (opts.report !== false) {
|
|
54
|
+
const reporter = new Reporter();
|
|
55
|
+
const outputPath = reporter.generateOptimiseMd(report, targetDir);
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(chalk.green.bold(` 📄 OPTIMISE.md generated → ${outputPath}`));
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.cyan(' Next steps:'));
|
|
60
|
+
console.log(chalk.gray(' 1. Feed OPTIMISE.md to your AI agent (Claude Code, Cursor, etc.)'));
|
|
61
|
+
console.log(chalk.gray(' 2. Ask it to "Read OPTIMISE.md and implement all fixes"'));
|
|
62
|
+
console.log(chalk.gray(' 3. Re-run vibeoptimise to verify the improvements'));
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
// Exit with error code if critical or high findings exist
|
|
66
|
+
const hasBlocking = report.summary.critical > 0 || report.summary.high > 0;
|
|
67
|
+
if (hasBlocking && !opts.quiet) {
|
|
68
|
+
console.log(chalk.yellow(' ⚠ Exiting with code 1 due to high/critical findings'));
|
|
69
|
+
console.log(chalk.gray(' Use this in CI to block merges with optimisation debt'));
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
process.exit(hasBlocking ? 1 : 0);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(chalk.red(`\n ✖ Fatal error: ${err.message}\n`));
|
|
76
|
+
if (opts.verbose)
|
|
77
|
+
console.error(err.stack);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
program.parse();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VibeOptimise — The Ultimate Codebase Optimisation Scanner
|
|
3
|
+
*
|
|
4
|
+
* 10 parallel scanners that find every inefficiency in your codebase
|
|
5
|
+
* and generate OPTIMISE.md for AI agents to fix everything.
|
|
6
|
+
*
|
|
7
|
+
* @module @darrenjcoxon/vibeoptimise
|
|
8
|
+
*/
|
|
9
|
+
export { Orchestrator } from './orchestrator.js';
|
|
10
|
+
export { Reporter } from './reporter.js';
|
|
11
|
+
export { getAllScanners } from './scanners/index.js';
|
|
12
|
+
export * from './types.js';
|
|
13
|
+
export { KnipScanner, JscpdScanner, MadgeScanner, DepcheckScanner, BundlePhobiaScanner, EslintPerfScanner, SourceMapExplorerScanner, RegexSafetyScanner, CssEfficiencyScanner, QueryEfficiencyScanner, } from './scanners/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VibeOptimise — The Ultimate Codebase Optimisation Scanner
|
|
3
|
+
*
|
|
4
|
+
* 10 parallel scanners that find every inefficiency in your codebase
|
|
5
|
+
* and generate OPTIMISE.md for AI agents to fix everything.
|
|
6
|
+
*
|
|
7
|
+
* @module @darrenjcoxon/vibeoptimise
|
|
8
|
+
*/
|
|
9
|
+
export { Orchestrator } from './orchestrator.js';
|
|
10
|
+
export { Reporter } from './reporter.js';
|
|
11
|
+
export { getAllScanners } from './scanners/index.js';
|
|
12
|
+
export * from './types.js';
|
|
13
|
+
// Export individual scanners for selective use
|
|
14
|
+
export { KnipScanner, JscpdScanner, MadgeScanner, DepcheckScanner, BundlePhobiaScanner, EslintPerfScanner, SourceMapExplorerScanner, RegexSafetyScanner, CssEfficiencyScanner, QueryEfficiencyScanner, } from './scanners/index.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VibeOptimise Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Manages parallel execution of all 10 scanners, aggregates results,
|
|
5
|
+
* and produces the final OptimiseReport.
|
|
6
|
+
*/
|
|
7
|
+
import { Scanner, ScanOptions, OptimiseReport } from './types.js';
|
|
8
|
+
export declare class Orchestrator {
|
|
9
|
+
private scanners;
|
|
10
|
+
constructor(scanners?: Scanner[]);
|
|
11
|
+
run(options: ScanOptions): Promise<OptimiseReport>;
|
|
12
|
+
private deduplicateFindings;
|
|
13
|
+
private buildReport;
|
|
14
|
+
private calculateSavings;
|
|
15
|
+
private printSummary;
|
|
16
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VibeOptimise Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Manages parallel execution of all 10 scanners, aggregates results,
|
|
5
|
+
* and produces the final OptimiseReport.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { SEVERITY_WEIGHTS } from './types.js';
|
|
10
|
+
import { getAllScanners } from './scanners/index.js';
|
|
11
|
+
export class Orchestrator {
|
|
12
|
+
scanners;
|
|
13
|
+
constructor(scanners) {
|
|
14
|
+
this.scanners = scanners || getAllScanners();
|
|
15
|
+
}
|
|
16
|
+
async run(options) {
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const results = [];
|
|
19
|
+
if (!options.quiet) {
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(chalk.cyan.bold(' 🚀 VibeOptimise — Codebase Optimisation Scanner'));
|
|
22
|
+
console.log(chalk.gray(` Scanning: ${options.targetDir}`));
|
|
23
|
+
console.log('');
|
|
24
|
+
}
|
|
25
|
+
// Phase 1: Check availability
|
|
26
|
+
const spinner = ora({ text: 'Checking scanner availability...', color: 'cyan' }).start();
|
|
27
|
+
const availability = [];
|
|
28
|
+
for (const scanner of this.scanners) {
|
|
29
|
+
const available = await scanner.isAvailable();
|
|
30
|
+
availability.push({ scanner, available });
|
|
31
|
+
}
|
|
32
|
+
spinner.stop();
|
|
33
|
+
if (!options.quiet) {
|
|
34
|
+
for (const { scanner, available } of availability) {
|
|
35
|
+
const icon = available ? chalk.green('✔') : chalk.yellow('○');
|
|
36
|
+
const status = available ? '' : chalk.gray(' (skipped)');
|
|
37
|
+
console.log(` ${icon} ${scanner.name}${status}`);
|
|
38
|
+
}
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
// Phase 2: Run available scanners in parallel
|
|
42
|
+
const availableScanners = availability
|
|
43
|
+
.filter(a => a.available)
|
|
44
|
+
.map(a => a.scanner);
|
|
45
|
+
if (availableScanners.length === 0) {
|
|
46
|
+
console.log(chalk.red(' ✖ No scanners available. Install dependencies and try again.'));
|
|
47
|
+
return this.buildReport(options, results, Date.now() - start, availability);
|
|
48
|
+
}
|
|
49
|
+
const scanSpinner = ora({
|
|
50
|
+
text: `Running ${availableScanners.length} scanners in parallel...`,
|
|
51
|
+
color: 'cyan',
|
|
52
|
+
}).start();
|
|
53
|
+
const scanPromises = availableScanners.map(async (scanner) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await scanner.scan(options.targetDir, options);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
scanner: scanner.name,
|
|
61
|
+
findings: [],
|
|
62
|
+
summary: `Error: ${err.message}`,
|
|
63
|
+
duration: 0,
|
|
64
|
+
error: err.message,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const scanResults = await Promise.allSettled(scanPromises);
|
|
69
|
+
for (const result of scanResults) {
|
|
70
|
+
if (result.status === 'fulfilled') {
|
|
71
|
+
results.push(result.value);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
results.push({
|
|
75
|
+
scanner: 'Unknown',
|
|
76
|
+
findings: [],
|
|
77
|
+
summary: `Failed: ${result.reason}`,
|
|
78
|
+
duration: 0,
|
|
79
|
+
error: String(result.reason),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
scanSpinner.stop();
|
|
84
|
+
// Phase 3: Deduplicate findings across scanners
|
|
85
|
+
const deduped = this.deduplicateFindings(results);
|
|
86
|
+
// Phase 4: Sort findings by severity
|
|
87
|
+
for (const result of deduped) {
|
|
88
|
+
result.findings.sort((a, b) => {
|
|
89
|
+
const weightDiff = SEVERITY_WEIGHTS[b.severity] - SEVERITY_WEIGHTS[a.severity];
|
|
90
|
+
if (weightDiff !== 0)
|
|
91
|
+
return weightDiff;
|
|
92
|
+
return (a.file || '').localeCompare(b.file || '');
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const report = this.buildReport(options, deduped, Date.now() - start, availability);
|
|
96
|
+
// Phase 5: Print summary
|
|
97
|
+
if (!options.quiet) {
|
|
98
|
+
this.printSummary(report);
|
|
99
|
+
}
|
|
100
|
+
return report;
|
|
101
|
+
}
|
|
102
|
+
deduplicateFindings(results) {
|
|
103
|
+
// Knip and Depcheck both find unused dependencies — deduplicate
|
|
104
|
+
const seenDeps = new Set();
|
|
105
|
+
for (const result of results) {
|
|
106
|
+
if (result.scanner === 'Knip') {
|
|
107
|
+
for (const finding of result.findings) {
|
|
108
|
+
if (finding.title.startsWith('Unused dependency:') || finding.title.startsWith('Unused devDependency:')) {
|
|
109
|
+
const depName = finding.title.split(': ')[1];
|
|
110
|
+
seenDeps.add(depName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Remove Depcheck findings that Knip already caught
|
|
116
|
+
for (const result of results) {
|
|
117
|
+
if (result.scanner === 'Depcheck') {
|
|
118
|
+
result.findings = result.findings.filter(finding => {
|
|
119
|
+
if (finding.title.startsWith('Unused dependency:') || finding.title.startsWith('Unused devDependency:')) {
|
|
120
|
+
const depName = finding.title.split(': ')[1];
|
|
121
|
+
return !seenDeps.has(depName);
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
buildReport(options, results, duration, availability) {
|
|
130
|
+
const allFindings = results.flatMap(r => r.findings);
|
|
131
|
+
const summary = {
|
|
132
|
+
totalFindings: allFindings.length,
|
|
133
|
+
critical: allFindings.filter(f => f.severity === 'critical').length,
|
|
134
|
+
high: allFindings.filter(f => f.severity === 'high').length,
|
|
135
|
+
medium: allFindings.filter(f => f.severity === 'medium').length,
|
|
136
|
+
low: allFindings.filter(f => f.severity === 'low').length,
|
|
137
|
+
info: allFindings.filter(f => f.severity === 'info').length,
|
|
138
|
+
estimatedSavings: this.calculateSavings(allFindings),
|
|
139
|
+
scannersPassed: availability.filter(a => a.available).length,
|
|
140
|
+
scannersSkipped: availability.filter(a => !a.available).length,
|
|
141
|
+
scannersFailed: results.filter(r => r.error).length,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
version: '1.0.0',
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
targetDir: options.targetDir,
|
|
147
|
+
duration,
|
|
148
|
+
scanners: results,
|
|
149
|
+
summary,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
calculateSavings(findings) {
|
|
153
|
+
const parts = [];
|
|
154
|
+
const unusedFiles = findings.filter(f => f.title?.startsWith('Unused file')).length;
|
|
155
|
+
if (unusedFiles > 0)
|
|
156
|
+
parts.push(`${unusedFiles} dead files`);
|
|
157
|
+
const unusedDeps = findings.filter(f => f.title?.startsWith('Unused dependency')).length;
|
|
158
|
+
if (unusedDeps > 0)
|
|
159
|
+
parts.push(`${unusedDeps} unused packages`);
|
|
160
|
+
const duplication = findings.filter(f => f.category === 'duplication').length;
|
|
161
|
+
if (duplication > 0)
|
|
162
|
+
parts.push(`${duplication} duplication blocks`);
|
|
163
|
+
const circular = findings.filter(f => f.title?.startsWith('Circular')).length;
|
|
164
|
+
if (circular > 0)
|
|
165
|
+
parts.push(`${circular} circular deps`);
|
|
166
|
+
return parts.length > 0 ? parts.join(', ') : 'Codebase is clean';
|
|
167
|
+
}
|
|
168
|
+
printSummary(report) {
|
|
169
|
+
const { summary } = report;
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(chalk.cyan.bold(' ━━━ Results ━━━'));
|
|
172
|
+
console.log('');
|
|
173
|
+
if (summary.totalFindings === 0) {
|
|
174
|
+
console.log(chalk.green.bold(' ✅ Codebase is optimised — no issues found!'));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (summary.critical > 0)
|
|
178
|
+
console.log(` 🔴 Critical: ${chalk.red.bold(String(summary.critical))}`);
|
|
179
|
+
if (summary.high > 0)
|
|
180
|
+
console.log(` 🟠 High: ${chalk.yellow.bold(String(summary.high))}`);
|
|
181
|
+
if (summary.medium > 0)
|
|
182
|
+
console.log(` 🟡 Medium: ${chalk.yellow(String(summary.medium))}`);
|
|
183
|
+
if (summary.low > 0)
|
|
184
|
+
console.log(` 🔵 Low: ${chalk.blue(String(summary.low))}`);
|
|
185
|
+
if (summary.info > 0)
|
|
186
|
+
console.log(` ⚪ Info: ${chalk.gray(String(summary.info))}`);
|
|
187
|
+
console.log(` ─────────────`);
|
|
188
|
+
console.log(` Total: ${chalk.bold(String(summary.totalFindings))}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log(chalk.gray(` Scanned in ${(report.duration / 1000).toFixed(1)}s with ${summary.scannersPassed}/10 scanners`));
|
|
192
|
+
// Per-scanner breakdown
|
|
193
|
+
console.log('');
|
|
194
|
+
for (const scanner of report.scanners) {
|
|
195
|
+
const count = scanner.findings.length;
|
|
196
|
+
const icon = scanner.error ? chalk.red('✖') : count === 0 ? chalk.green('✔') : chalk.yellow('⚠');
|
|
197
|
+
console.log(` ${icon} ${scanner.scanner}: ${scanner.summary}`);
|
|
198
|
+
}
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VibeOptimise Reporter
|
|
3
|
+
*
|
|
4
|
+
* Generates OPTIMISE.md — an AI-actionable report that Claude Code,
|
|
5
|
+
* Cursor, or any AI agent can use to automatically fix optimisation issues.
|
|
6
|
+
*
|
|
7
|
+
* Follows the Vibeguard FIXES.md pattern with clear severity separation.
|
|
8
|
+
*/
|
|
9
|
+
import { OptimiseReport } from './types.js';
|
|
10
|
+
export declare class Reporter {
|
|
11
|
+
generateOptimiseMd(report: OptimiseReport, outputDir: string): string;
|
|
12
|
+
private buildMarkdown;
|
|
13
|
+
private renderFinding;
|
|
14
|
+
private formatCategoryName;
|
|
15
|
+
}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VibeOptimise Reporter
|
|
3
|
+
*
|
|
4
|
+
* Generates OPTIMISE.md — an AI-actionable report that Claude Code,
|
|
5
|
+
* Cursor, or any AI agent can use to automatically fix optimisation issues.
|
|
6
|
+
*
|
|
7
|
+
* Follows the Vibeguard FIXES.md pattern with clear severity separation.
|
|
8
|
+
*/
|
|
9
|
+
import { writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { CATEGORY_ICONS } from './types.js';
|
|
12
|
+
export class Reporter {
|
|
13
|
+
generateOptimiseMd(report, outputDir) {
|
|
14
|
+
const outputPath = join(outputDir, 'OPTIMISE.md');
|
|
15
|
+
const content = this.buildMarkdown(report);
|
|
16
|
+
writeFileSync(outputPath, content, 'utf-8');
|
|
17
|
+
return outputPath;
|
|
18
|
+
}
|
|
19
|
+
buildMarkdown(report) {
|
|
20
|
+
const { summary, scanners } = report;
|
|
21
|
+
const allFindings = scanners.flatMap(s => s.findings);
|
|
22
|
+
const lines = [];
|
|
23
|
+
// Header
|
|
24
|
+
lines.push('# OPTIMISE.md — Codebase Optimisation Report');
|
|
25
|
+
lines.push('');
|
|
26
|
+
lines.push(`> Generated by [VibeOptimise](https://www.npmjs.com/package/@darrenjcoxon/vibeoptimise) v${report.version}`);
|
|
27
|
+
lines.push(`> Scanned: ${report.targetDir}`);
|
|
28
|
+
lines.push(`> Date: ${new Date(report.timestamp).toLocaleString()}`);
|
|
29
|
+
lines.push(`> Duration: ${(report.duration / 1000).toFixed(1)}s`);
|
|
30
|
+
lines.push('');
|
|
31
|
+
// AI Agent Instructions
|
|
32
|
+
lines.push('## Instructions for AI Agents');
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push('This file contains actionable optimisation findings for your codebase.');
|
|
35
|
+
lines.push('Work through each section in order of priority (Critical → High → Medium → Low).');
|
|
36
|
+
lines.push('Each finding includes the file, line number, and specific fix to apply.');
|
|
37
|
+
lines.push('After making changes, re-run `npx @darrenjcoxon/vibeoptimise` to verify fixes.');
|
|
38
|
+
lines.push('');
|
|
39
|
+
// Summary
|
|
40
|
+
lines.push('## Summary');
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(`| Severity | Count |`);
|
|
43
|
+
lines.push(`|----------|-------|`);
|
|
44
|
+
if (summary.critical > 0)
|
|
45
|
+
lines.push(`| 🔴 Critical | ${summary.critical} |`);
|
|
46
|
+
if (summary.high > 0)
|
|
47
|
+
lines.push(`| 🟠 High | ${summary.high} |`);
|
|
48
|
+
if (summary.medium > 0)
|
|
49
|
+
lines.push(`| 🟡 Medium | ${summary.medium} |`);
|
|
50
|
+
if (summary.low > 0)
|
|
51
|
+
lines.push(`| 🔵 Low | ${summary.low} |`);
|
|
52
|
+
if (summary.info > 0)
|
|
53
|
+
lines.push(`| ⚪ Info | ${summary.info} |`);
|
|
54
|
+
lines.push(`| **Total** | **${summary.totalFindings}** |`);
|
|
55
|
+
lines.push('');
|
|
56
|
+
if (summary.totalFindings === 0) {
|
|
57
|
+
lines.push('✅ **Codebase is optimised — no issues found!**');
|
|
58
|
+
lines.push('');
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
// Group findings by severity, then by category
|
|
62
|
+
const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
|
|
63
|
+
const severityLabels = {
|
|
64
|
+
critical: '🔴 Critical — Immediate Performance Impact',
|
|
65
|
+
high: '🟠 High — Significant Optimisation Opportunities',
|
|
66
|
+
medium: '🟡 Medium — Recommended Improvements',
|
|
67
|
+
low: '🔵 Low — Nice to Have',
|
|
68
|
+
info: '⚪ Info — Informational',
|
|
69
|
+
};
|
|
70
|
+
for (const severity of severityOrder) {
|
|
71
|
+
const findings = allFindings.filter(f => f.severity === severity);
|
|
72
|
+
if (findings.length === 0)
|
|
73
|
+
continue;
|
|
74
|
+
lines.push(`---`);
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push(`## ${severityLabels[severity]}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
// Group by category within severity
|
|
79
|
+
const categories = [...new Set(findings.map(f => f.category))];
|
|
80
|
+
for (const category of categories) {
|
|
81
|
+
const catFindings = findings.filter(f => f.category === category);
|
|
82
|
+
const icon = CATEGORY_ICONS[category] || '📋';
|
|
83
|
+
lines.push(`### ${icon} ${this.formatCategoryName(category)} (${catFindings.length})`);
|
|
84
|
+
lines.push('');
|
|
85
|
+
for (const finding of catFindings) {
|
|
86
|
+
this.renderFinding(finding, lines);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Scanner Results Summary
|
|
91
|
+
lines.push('---');
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('## Scanner Results');
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('| Scanner | Findings | Duration | Status |');
|
|
96
|
+
lines.push('|---------|----------|----------|--------|');
|
|
97
|
+
for (const scanner of scanners) {
|
|
98
|
+
const status = scanner.error ? '❌ Error' : scanner.findings.length === 0 ? '✅ Clean' : `⚠️ ${scanner.findings.length} issues`;
|
|
99
|
+
const duration = `${(scanner.duration / 1000).toFixed(1)}s`;
|
|
100
|
+
lines.push(`| ${scanner.scanner} | ${scanner.findings.length} | ${duration} | ${status} |`);
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
// Footer
|
|
104
|
+
lines.push('---');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('*Generated by VibeOptimise — The ultimate codebase optimisation scanner*');
|
|
107
|
+
lines.push('*Run `npx @darrenjcoxon/vibeoptimise` to re-scan after fixes*');
|
|
108
|
+
lines.push('');
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
renderFinding(finding, lines) {
|
|
112
|
+
const location = finding.file
|
|
113
|
+
? finding.line
|
|
114
|
+
? `\`${finding.file}:${finding.line}\``
|
|
115
|
+
: `\`${finding.file}\``
|
|
116
|
+
: '';
|
|
117
|
+
lines.push(`#### ${finding.title}`);
|
|
118
|
+
lines.push('');
|
|
119
|
+
if (location)
|
|
120
|
+
lines.push(`**Location:** ${location}`);
|
|
121
|
+
if (finding.relatedFiles && finding.relatedFiles.length > 0) {
|
|
122
|
+
lines.push(`**Related files:** ${finding.relatedFiles.map(f => `\`${f}\``).join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`**Issue:** ${finding.description}`);
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push(`**Fix:** ${finding.suggestion}`);
|
|
128
|
+
lines.push('');
|
|
129
|
+
if (finding.estimatedImpact) {
|
|
130
|
+
lines.push(`**Impact:** ${finding.estimatedImpact}`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
formatCategoryName(category) {
|
|
135
|
+
return category
|
|
136
|
+
.split('-')
|
|
137
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
138
|
+
.join(' ');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BundlePhobia Scanner — Dependency Weight Analysis
|
|
3
|
+
*
|
|
4
|
+
* Checks every dependency's bundle size impact and suggests lighter alternatives.
|
|
5
|
+
* Uses the BundlePhobia API to get minified + gzipped sizes.
|
|
6
|
+
*
|
|
7
|
+
* Tool: bundlephobia API (https://bundlephobia.com/api)
|
|
8
|
+
* Output: JSON API responses
|
|
9
|
+
*/
|
|
10
|
+
import { Scanner, ScannerResult, ScanOptions } from '../types.js';
|
|
11
|
+
export declare class BundlePhobiaScanner implements Scanner {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
category: "bundle-size";
|
|
15
|
+
isAvailable(): Promise<boolean>;
|
|
16
|
+
scan(targetDir: string, options: ScanOptions): Promise<ScannerResult>;
|
|
17
|
+
private checkBundleSizes;
|
|
18
|
+
private findDuplicatePackages;
|
|
19
|
+
}
|