-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathformat-code.js
More file actions
114 lines (95 loc) 路 3.6 KB
/
Copy pathformat-code.js
File metadata and controls
114 lines (95 loc) 路 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env node
/**
* Format Code - PostToolUse Hook for Write|Edit
* Auto-formats files after Claude Code modifies them.
* Supports Python (ruff) and Markdown/YAML/JSON (prettier).
* Logs to: ~/.claude/hooks-logs/
*
* Setup in .claude/settings.json:
* {
* "hooks": {
* "PostToolUse": [{
* "matcher": "Write|Edit",
* "hooks": [{ "type": "command", "command": "node /path/to/format-code.js" }]
* }]
* }
* }
*/
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const LOG_DIR = path.join(process.env.HOME, '.claude', 'hooks-logs');
const PRETTIER_EXTS = new Set(['.js', '.ts', '.json', '.md', '.yaml', '.yml', '.html']);
const FORMATTERS = {
'.py': (fp) => [
['uv', 'run', 'ruff', 'check', '--fix', '--exit-zero', '--quiet', fp],
['uv', 'run', 'ruff', 'format', '--quiet', fp],
],
};
for (const ext of PRETTIER_EXTS) {
FORMATTERS[ext] = (fp) => [['npx', '--yes', 'prettier', '--write', fp]];
}
function log(data) {
try {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
const file = path.join(LOG_DIR, `${new Date().toISOString().slice(0, 10)}.jsonl`);
fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), hook: 'format-code', ...data }) + '\n');
} catch {}
}
function getFormatter(filePath) {
const ext = path.extname(filePath).toLowerCase();
return FORMATTERS[ext] || null;
}
function formatFile(filePath, cwd, sessionId) {
const absPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
if (!fs.existsSync(absPath)) {
log({ level: 'SKIP', reason: 'file not found', file: absPath, session_id: sessionId });
return { success: false, error: 'file not found' };
}
const ext = path.extname(absPath).toLowerCase();
const getCommands = FORMATTERS[ext];
if (!getCommands) {
log({ level: 'SKIP', reason: 'unsupported file type', file: absPath, session_id: sessionId });
return { success: false, error: 'unsupported file type' };
}
const formatterName = PRETTIER_EXTS.has(ext) ? 'prettier' : 'ruff';
const dir = path.dirname(absPath);
for (const args of getCommands(absPath)) {
try {
const result = spawnSync(args[0], args.slice(1), { cwd: dir, stdio: 'pipe' });
if (result.status !== 0) {
const msg = result.stderr?.toString().trim() || `Process exited with code ${result.status}`;
log({ level: 'ERROR', formatter: formatterName, file: absPath, error: msg, session_id: sessionId });
return { success: false, error: msg, formatter: formatterName };
}
} catch (e) {
log({ level: 'ERROR', formatter: formatterName, file: absPath, error: e.message, session_id: sessionId });
return { success: false, error: e.message, formatter: formatterName };
}
}
log({ level: 'FORMATTED', formatter: formatterName, file: absPath, session_id: sessionId });
return { success: true, formatter: formatterName };
}
async function main() {
let input = '';
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch (e) {
log({ level: 'ERROR', error: e.message });
return console.log('{}');
}
if (data === null || typeof data !== 'object') data = {};
const { tool_name, tool_input, session_id, cwd } = data;
if (!['Write', 'Edit'].includes(tool_name) || !tool_input?.file_path) {
return console.log('{}');
}
formatFile(tool_input.file_path, cwd || process.cwd(), session_id);
console.log('{}');
}
if (require.main === module) {
main();
} else {
module.exports = { getFormatter, formatFile, log, FORMATTERS };
}