@mthanhlm/autodev 0.4.1 → 0.4.2
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/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/autodev/bin/autodev-tools.cjs +1 -0
- package/autodev/templates/config.json +1 -0
- package/autodev/workflows/help.md +1 -0
- package/autodev/workflows/new-project.md +1 -0
- package/bin/install.js +5 -0
- package/hooks/autodev-auto-format.js +210 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- Resolves `.autodev/` state from the repo root even when Claude is started in a nested subdirectory
|
|
12
12
|
- Maps brownfield repos with foreground delegated agents when the environment supports them
|
|
13
13
|
- Runs a multi-lens review pass, using foreground review agents when the environment supports them
|
|
14
|
+
- Can auto-format edited code after Claude writes files when a local formatter is available
|
|
14
15
|
- Ships manual commands when you want direct control:
|
|
15
16
|
- `/autodev-help`
|
|
16
17
|
- `/autodev-new-project`
|
|
@@ -116,6 +117,7 @@ project -> track -> phase -> tasks
|
|
|
116
117
|
- No automatic parallel execution
|
|
117
118
|
- If specialized agents are unavailable in the current Claude Code environment, the workflow falls back cleanly to current-session execution instead of stopping on platform wording
|
|
118
119
|
- If you want Claude Code itself to hard-disable background task functionality, install with `--disable-background-tasks`, which writes `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` into Claude Code `settings.json`
|
|
120
|
+
- The post-edit formatter hook uses only locally available tools such as `prettier`, `ruff`, `black`, `gofmt`, `rustfmt`, or `shfmt`, and skips `.autodev/` files
|
|
119
121
|
|
|
120
122
|
## Git Policy
|
|
121
123
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lean Claude Code workflow. No automatic commits. No branches. No worktrees. Git is read-only.
|
|
4
4
|
It resolves `.autodev/` state from the repo root even when you start Claude in a nested subdirectory.
|
|
5
|
+
It can auto-format edited code after writes when the repo already has a local formatter available.
|
|
5
6
|
|
|
6
7
|
## Main Entry
|
|
7
8
|
|
|
@@ -32,6 +32,7 @@ node "$AUTODEV_ROOT/autodev/bin/autodev-tools.cjs" init new-project
|
|
|
32
32
|
- `workflow.research: false`
|
|
33
33
|
- `workflow.review_after_execute: true`
|
|
34
34
|
- `git.mode: "read-only"`
|
|
35
|
+
- `hooks.auto_format: true`
|
|
35
36
|
|
|
36
37
|
6. Write `.autodev/PROJECT.md` with:
|
|
37
38
|
- one-line summary
|
package/bin/install.js
CHANGED
|
@@ -14,6 +14,7 @@ const MANAGED_PREFIX = 'autodev-';
|
|
|
14
14
|
const ROOT_COMMAND = 'autodev';
|
|
15
15
|
const HOOK_FILES = [
|
|
16
16
|
'hooks.json',
|
|
17
|
+
'autodev-auto-format.js',
|
|
17
18
|
'autodev-paths.js',
|
|
18
19
|
'autodev-context-monitor.js',
|
|
19
20
|
'autodev-git-guard.js',
|
|
@@ -375,6 +376,9 @@ function configureSettings(targetDir, isGlobal, options = {}) {
|
|
|
375
376
|
const contextMonitorCommand = isGlobal
|
|
376
377
|
? buildGlobalCommand(targetDir, 'autodev-context-monitor.js')
|
|
377
378
|
: buildLocalCommand('autodev-context-monitor.js');
|
|
379
|
+
const autoFormatCommand = isGlobal
|
|
380
|
+
? buildGlobalCommand(targetDir, 'autodev-auto-format.js')
|
|
381
|
+
: buildLocalCommand('autodev-auto-format.js');
|
|
378
382
|
const promptGuardCommand = isGlobal
|
|
379
383
|
? buildGlobalCommand(targetDir, 'autodev-prompt-guard.js')
|
|
380
384
|
: buildLocalCommand('autodev-prompt-guard.js');
|
|
@@ -402,6 +406,7 @@ function configureSettings(targetDir, isGlobal, options = {}) {
|
|
|
402
406
|
ensureHook(settings, preToolEvent, 'Write|Edit', readGuardCommand, 5);
|
|
403
407
|
ensureHook(settings, preToolEvent, 'Write|Edit', workflowGuardCommand, 5);
|
|
404
408
|
ensureHook(settings, preToolEvent, 'Bash', gitGuardCommand, 5);
|
|
409
|
+
ensureHook(settings, postToolEvent, 'Edit|Write|MultiEdit', autoFormatCommand, 10);
|
|
405
410
|
ensureHook(settings, postToolEvent, 'Bash|Edit|Write|MultiEdit|Agent|Task', contextMonitorCommand, 10);
|
|
406
411
|
ensureHook(settings, postToolEvent, 'Write|Edit', phaseBoundaryCommand, 5);
|
|
407
412
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { findWorkspaceRoot, readProjectConfig } = require('./autodev-paths.js');
|
|
7
|
+
|
|
8
|
+
const PRETTIER_EXTENSIONS = new Set([
|
|
9
|
+
'.js', '.jsx', '.cjs', '.mjs',
|
|
10
|
+
'.ts', '.tsx',
|
|
11
|
+
'.json',
|
|
12
|
+
'.css', '.scss', '.less',
|
|
13
|
+
'.html',
|
|
14
|
+
'.yml', '.yaml',
|
|
15
|
+
'.vue', '.svelte'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const PYTHON_EXTENSIONS = new Set(['.py']);
|
|
19
|
+
const SHELL_EXTENSIONS = new Set(['.sh', '.bash', '.zsh']);
|
|
20
|
+
|
|
21
|
+
function fileExists(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
return fs.existsSync(filePath);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isFile(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.statSync(filePath).isFile();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInside(targetPath, rootPath) {
|
|
38
|
+
const relative = path.relative(rootPath, targetPath);
|
|
39
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveHookFilePath(data, cwd) {
|
|
43
|
+
const raw = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
44
|
+
if (!raw) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveExecutable(workspaceRoot, candidates) {
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
if (Array.isArray(candidate)) {
|
|
53
|
+
const absolute = path.join(workspaceRoot, ...candidate);
|
|
54
|
+
if (isFile(absolute)) {
|
|
55
|
+
return absolute;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.platform === 'win32' && isFile(`${absolute}.cmd`)) {
|
|
59
|
+
return `${absolute}.cmd`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (process.platform === 'win32' && isFile(`${absolute}.exe`)) {
|
|
63
|
+
return `${absolute}.exe`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isFile(`${absolute}.cmd`)) {
|
|
67
|
+
return `${absolute}.cmd`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isFile(`${absolute}.exe`)) {
|
|
71
|
+
return `${absolute}.exe`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isFile(`${absolute}.js`)) {
|
|
75
|
+
return `${absolute}.js`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (fileExists(absolute)) {
|
|
79
|
+
return absolute;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = spawnSync('sh', ['-lc', `command -v "${candidate}"`], {
|
|
85
|
+
cwd: workspaceRoot,
|
|
86
|
+
encoding: 'utf8'
|
|
87
|
+
});
|
|
88
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
89
|
+
return result.stdout.trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveFile(workspaceRoot, candidates) {
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
const absolute = path.join(workspaceRoot, ...candidate);
|
|
99
|
+
if (isFile(absolute)) {
|
|
100
|
+
return absolute;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatterCommand(filePath, workspaceRoot) {
|
|
108
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
109
|
+
|
|
110
|
+
if (PRETTIER_EXTENSIONS.has(ext)) {
|
|
111
|
+
const prettierScript = resolveFile(workspaceRoot, [
|
|
112
|
+
['node_modules', 'prettier', 'bin', 'prettier.cjs'],
|
|
113
|
+
['node_modules', 'prettier', 'bin', 'prettier.js']
|
|
114
|
+
]);
|
|
115
|
+
if (prettierScript) {
|
|
116
|
+
return [process.execPath, [prettierScript, '--write', filePath]];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const prettier = resolveExecutable(workspaceRoot, [
|
|
120
|
+
['node_modules', '.bin', 'prettier'],
|
|
121
|
+
'prettier'
|
|
122
|
+
]);
|
|
123
|
+
return prettier ? [prettier, ['--write', filePath]] : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (PYTHON_EXTENSIONS.has(ext)) {
|
|
127
|
+
const ruff = resolveExecutable(workspaceRoot, [
|
|
128
|
+
['.venv', 'bin', 'ruff'],
|
|
129
|
+
['.venv', 'Scripts', 'ruff.exe'],
|
|
130
|
+
'ruff'
|
|
131
|
+
]);
|
|
132
|
+
if (ruff) {
|
|
133
|
+
return [ruff, ['format', filePath]];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const black = resolveExecutable(workspaceRoot, [
|
|
137
|
+
['.venv', 'bin', 'black'],
|
|
138
|
+
['.venv', 'Scripts', 'black.exe'],
|
|
139
|
+
'black'
|
|
140
|
+
]);
|
|
141
|
+
return black ? [black, [filePath]] : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (ext === '.go') {
|
|
145
|
+
const gofmt = resolveExecutable(workspaceRoot, ['gofmt']);
|
|
146
|
+
return gofmt ? [gofmt, ['-w', filePath]] : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (ext === '.rs') {
|
|
150
|
+
const rustfmt = resolveExecutable(workspaceRoot, ['rustfmt']);
|
|
151
|
+
return rustfmt ? [rustfmt, [filePath]] : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (SHELL_EXTENSIONS.has(ext)) {
|
|
155
|
+
const shfmt = resolveExecutable(workspaceRoot, ['shfmt']);
|
|
156
|
+
return shfmt ? [shfmt, ['-w', filePath]] : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const input = fs.readFileSync(0, 'utf8');
|
|
164
|
+
const data = JSON.parse(input);
|
|
165
|
+
if (!['Edit', 'Write', 'MultiEdit'].includes(data.tool_name)) {
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cwd = data.cwd || process.cwd();
|
|
170
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
171
|
+
const config = readProjectConfig(cwd);
|
|
172
|
+
if (config && config.hooks?.auto_format === false) {
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const filePath = resolveHookFilePath(data, cwd);
|
|
177
|
+
if (!filePath || !isFile(filePath)) {
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const autodevRoot = path.join(workspaceRoot, '.autodev');
|
|
182
|
+
if (fileExists(autodevRoot) && isInside(filePath, autodevRoot)) {
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const formatter = formatterCommand(filePath, workspaceRoot);
|
|
187
|
+
if (!formatter) {
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const [command, args] = formatter;
|
|
192
|
+
const result = spawnSync(command, args, {
|
|
193
|
+
cwd: workspaceRoot,
|
|
194
|
+
encoding: 'utf8'
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (result.status !== 0) {
|
|
198
|
+
const stderr = (result.stderr || '').trim();
|
|
199
|
+
process.stdout.write(JSON.stringify({
|
|
200
|
+
hookSpecificOutput: {
|
|
201
|
+
hookEventName: 'PostToolUse',
|
|
202
|
+
additionalContext: stderr
|
|
203
|
+
? `AUTO-FORMAT WARNING: formatting failed for ${path.basename(filePath)}: ${stderr}`
|
|
204
|
+
: `AUTO-FORMAT WARNING: formatting failed for ${path.basename(filePath)}.`
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -54,6 +54,16 @@
|
|
|
54
54
|
}
|
|
55
55
|
],
|
|
56
56
|
"PostToolUse": [
|
|
57
|
+
{
|
|
58
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
59
|
+
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/autodev-auto-format.js\"",
|
|
63
|
+
"timeout": 10
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
},
|
|
57
67
|
{
|
|
58
68
|
"matcher": "Bash|Edit|Write|MultiEdit|Agent|Task",
|
|
59
69
|
"hooks": [
|
package/package.json
CHANGED