Skip to content

Commit b4dcb2f

Browse files
author
Umutcan ÖNER
committed
feat: add Cascade CLI integration for auto-fix and performance refactoring in pre-commit hook
1 parent 7eeec9b commit b4dcb2f

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

.husky/pre-commit

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,22 @@ if [ $? -ne 0 ]; then
4949
exit 1
5050
fi
5151

52+
# Run Cascade auto-fix and refactor
53+
echo "🔧 Running Cascade auto-fix and refactor..."
54+
55+
# Check if Cascade CLI is available
56+
if ! command -v cascade &> /dev/null; then
57+
echo "${YELLOW}⚠️ Cascade CLI not found. Skipping auto-fix and refactor.${NC}"
58+
else
59+
# Run biome check first if available and create output
60+
if command -v biome &> /dev/null; then
61+
biome check . --json > biome.json 2>/dev/null || true
62+
node ./scripts/cascade-fix.js --from=biome.json --apply
63+
rm biome.json 2>/dev/null || true
64+
fi
65+
66+
# Run refactor script for performance optimizations
67+
node ./scripts/cascade-refactor.js --strategy=performance --apply
68+
fi
69+
5270
echo "${GREEN}✅ Pre-commit checks passed!${NC}"

scripts/cascade-fix.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env node
2+
/**
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* Auto refactor script for Windsurf Cascade integration.
6+
* Supports input from Biome, TypeScript, or custom static analyzers.
7+
*/
8+
9+
const fs = require('node:fs');
10+
const path = require('node:path');
11+
const { spawnSync } = require('node:child_process');
12+
13+
const args = process.argv.slice(2);
14+
const options = {
15+
apply: args.includes('--apply'),
16+
dry: args.includes('--dry'),
17+
input: getArgValue('--from') || 'typecheck.json', // default
18+
};
19+
20+
// Helper to get CLI argument value
21+
function getArgValue(flag) {
22+
const index = args.indexOf(flag);
23+
return index >= 0 && args[index + 1] ? args[index + 1] : null;
24+
}
25+
26+
// Helper: check if cascade CLI is available
27+
function checkCascade() {
28+
try {
29+
const result = spawnSync('cascade', ['--version'], { encoding: 'utf-8' });
30+
return result.status === 0;
31+
} catch {
32+
return false;
33+
}
34+
}
35+
36+
// Helper: run Cascade prompt on code block
37+
function runCascadePrompt({ filePath, line, message }) {
38+
const prompt = `Refactor the following code to fix this issue:\n\n// ${message}\n`;
39+
40+
const fileContent = fs.readFileSync(filePath, 'utf-8').split('\n');
41+
const context = fileContent.slice(Math.max(0, line - 5), line + 5).join('\n');
42+
43+
const fullPrompt = `${prompt}\n\`\`\`ts\n${context}\n\`\`\``;
44+
45+
const result = spawnSync(
46+
'cascade',
47+
['--prompt', fullPrompt, '--file', filePath, '--line', String(line)],
48+
{ encoding: 'utf-8' }
49+
);
50+
51+
return result.stdout;
52+
}
53+
54+
// Main logic
55+
function run() {
56+
if (!fs.existsSync(options.input)) {
57+
console.error(`❌ Cannot find file: ${options.input}`);
58+
process.exit(1);
59+
}
60+
61+
const raw = fs.readFileSync(options.input, 'utf-8');
62+
let issues = [];
63+
64+
try {
65+
issues = JSON.parse(raw);
66+
} catch {
67+
console.error('❌ Invalid JSON input file.');
68+
process.exit(1);
69+
}
70+
71+
const applicable = issues.filter((i) => i.file && i.message && i.line);
72+
if (applicable.length === 0) {
73+
console.log('✅ No fixable issues found.');
74+
return;
75+
}
76+
77+
const cascadeAvailable = checkCascade();
78+
if (!cascadeAvailable) {
79+
console.warn('⚠️ Cascade CLI not found. Skipping auto fix.');
80+
}
81+
82+
for (const issue of applicable) {
83+
const filePath = path.resolve(issue.file);
84+
const message = issue.message;
85+
const line = issue.line;
86+
87+
console.log(`🔧 Refactoring ${filePath}:${line} - ${message}`);
88+
89+
if (options.dry || !options.apply || !cascadeAvailable) {
90+
console.log(`📝 Suggested fix: [${message}]`);
91+
continue;
92+
}
93+
94+
const result = runCascadePrompt({ filePath, line, message });
95+
console.log(result.trim());
96+
}
97+
98+
console.log(`✨ Done. ${options.apply ? 'Fixes applied.' : 'Dry run.'}`);
99+
}
100+
101+
run();

scripts/cascade-refactor.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env node
2+
/**
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* Proactive performance-based refactor tool using Cascade CLI.
6+
* Scans entire repo by default, or limited via --include=path
7+
*/
8+
9+
const fs = require('node:fs');
10+
const path = require('node:path');
11+
const glob = require('fast-glob');
12+
const { spawnSync } = require('node:child_process');
13+
14+
// ----------------------------
15+
// CLI Options
16+
// ----------------------------
17+
const args = process.argv.slice(2);
18+
const targetDir = getArgValue('--include') || '.'; // scan entire repo by default
19+
const strategy = getArgValue('--strategy') || 'performance';
20+
const shouldApply = args.includes('--apply');
21+
const shouldDry = args.includes('--dry');
22+
23+
function getArgValue(flag) {
24+
const i = args.indexOf(flag);
25+
return i >= 0 && args[i + 1] ? args[i + 1] : null;
26+
}
27+
28+
function checkCascade() {
29+
try {
30+
const result = spawnSync('cascade', ['--version'], { encoding: 'utf-8' });
31+
return result.status === 0;
32+
} catch {
33+
return false;
34+
}
35+
}
36+
37+
// ----------------------------
38+
// Pattern matchers (basic)
39+
// ----------------------------
40+
function findSuspiciousLines(content) {
41+
const lines = content.split('\n');
42+
const matches = [];
43+
44+
lines.forEach((line, i) => {
45+
if (line.includes('.map(') && (line.includes('.filter(') || line.includes('.reduce('))) {
46+
matches.push({ line: i + 1, content: line.trim() });
47+
}
48+
49+
if (line.match(/for\s*\(\s*let\s+[a-z]+\s*=\s*0;/)) {
50+
matches.push({ line: i + 1, content: line.trim() });
51+
}
52+
});
53+
54+
return matches;
55+
}
56+
57+
function runCascade({ filePath, line, codeBlock }) {
58+
const prompt = `Refactor the following code to optimize runtime performance.
59+
Avoid redundant array operations or nested loops.
60+
Ensure the logic remains the same and behavior is preserved.
61+
62+
\`\`\`ts
63+
${codeBlock}
64+
\`\`\`
65+
`;
66+
67+
const result = spawnSync(
68+
'cascade',
69+
['--prompt', prompt, '--file', filePath, '--line', String(line)],
70+
{ encoding: 'utf-8' }
71+
);
72+
73+
return result.stdout;
74+
}
75+
76+
// ----------------------------
77+
// Main
78+
// ----------------------------
79+
async function run() {
80+
const cascadeAvailable = checkCascade();
81+
if (!cascadeAvailable) {
82+
console.error('❌ Cascade CLI not found.');
83+
process.exit(1);
84+
}
85+
86+
const files = await glob([`${targetDir}/**/*.ts`, `${targetDir}/**/*.tsx`], {
87+
ignore: [
88+
'**/node_modules/**',
89+
'**/.next/**',
90+
'**/dist/**',
91+
'**/.turbo/**',
92+
'**/.git/**',
93+
'**/build/**',
94+
'**/out/**',
95+
'**/coverage/**',
96+
],
97+
});
98+
99+
for (const file of files) {
100+
const fullPath = path.resolve(file);
101+
const content = fs.readFileSync(fullPath, 'utf-8');
102+
const suspicious = findSuspiciousLines(content);
103+
104+
if (suspicious.length === 0) continue;
105+
106+
console.log(`🔍 Found ${suspicious.length} performance candidate(s) in ${file}`);
107+
108+
for (const match of suspicious) {
109+
const { line, content: codeLine } = match;
110+
console.log(`⚡ Refactoring ${file}:${line}`);
111+
112+
if (shouldDry) {
113+
console.log(`📝 Suspicious code: ${codeLine}`);
114+
continue;
115+
}
116+
117+
if (shouldApply) {
118+
const result = runCascade({
119+
filePath: fullPath,
120+
line,
121+
codeBlock: codeLine,
122+
});
123+
124+
console.log(result.trim());
125+
}
126+
}
127+
}
128+
129+
console.log('✅ Cascade performance refactor complete.');
130+
}
131+
132+
run();

0 commit comments

Comments
 (0)