@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "autodev",
3
3
  "description": "A lean Claude Code workflow system with a single entrypoint, task-based phase execution, and read-only git.",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "author": {
6
6
  "name": "mthanhlm"
7
7
  },
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
 
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = {
16
16
  mode: 'read-only'
17
17
  },
18
18
  hooks: {
19
+ auto_format: true,
19
20
  context_warnings: true,
20
21
  read_guard: true,
21
22
  workflow_guard: true,
@@ -10,6 +10,7 @@
10
10
  "mode": "read-only"
11
11
  },
12
12
  "hooks": {
13
+ "auto_format": true,
13
14
  "context_warnings": true,
14
15
  "read_guard": true,
15
16
  "workflow_guard": true,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthanhlm/autodev",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "A lean Claude Code workflow system with a single entrypoint, task-based phase execution, and read-only git.",
5
5
  "bin": {
6
6
  "autodev": "bin/install.js"