claude-git-hooks 2.34.0 → 2.35.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/CHANGELOG.md +52 -0
- package/CLAUDE.md +12 -10
- package/README.md +37 -0
- package/lib/commands/install.js +1 -1
- package/lib/commands/lint.js +1 -1
- package/lib/hooks/pre-commit.js +1 -1
- package/lib/utils/judge.js +1 -1
- package/lib/utils/linter-runner.js +222 -28
- package/lib/utils/tool-runner.js +317 -63
- package/package.json +73 -69
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,58 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
|
|
|
5
5
|
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.35.2] - 2026-04-13
|
|
9
|
+
|
|
10
|
+
### ✨ Added
|
|
11
|
+
- Added Maven Wrapper (mvnw/mvnw.cmd) auto-detection for Spotless — bypasses cmd.exe metacharacter issues on Windows/WSL2
|
|
12
|
+
- Added per-file execution mode for Spotless to avoid pipe (`|`) metacharacter in regex arguments on Windows
|
|
13
|
+
- Added `env` option to `runTool`, `runToolFix`, and `runToolWithAutoFix` for passing extra environment variables to tool invocations
|
|
14
|
+
- Added `configDir` to `_detectInProjectFiles` return value for downstream Maven Wrapper resolution
|
|
15
|
+
|
|
16
|
+
### 🐛 Fixed
|
|
17
|
+
- Fixed Spotless linter failing on Windows/WSL2 when multiple Java files are staged — `|` (pipe) in the `DspotlessFiles` regex was interpreted as a shell pipe by cmd.exe during batch script expansion
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [2.35.1] - 2026-04-08
|
|
21
|
+
|
|
22
|
+
### ✨ Added
|
|
23
|
+
- Local binary resolution for linter tools — walks up from config directory to repo root, preferring project-installed binaries over npx to prevent version mismatches (#123)
|
|
24
|
+
- Array-form `detectInProjectFile` for tool detection — Prettier and ESLint now recognized via config files (.prettierrc, .eslintrc.json, eslint.config.js, etc.) in addition to package.json dependencies
|
|
25
|
+
- WSL2/Windows interop support for .cmd/.bat tool binaries, with proper cmd.exe metacharacter escaping
|
|
26
|
+
- Source logging for linter tool resolution (remote/remote default/local) to make non-determinism visible
|
|
27
|
+
- New `localBin` and `toolCwd` options for `runTool`/`runToolFix`/`runToolWithAutoFix` enabling direct binary execution and correct plugin resolution in monorepos
|
|
28
|
+
|
|
29
|
+
### 🔧 Changed
|
|
30
|
+
- Replaced `execSync` with `execFileSync` in tool-runner — args passed directly to process, preventing shell interpretation of regex metacharacters (e.g., `|` in Spotless patterns) and file path injection
|
|
31
|
+
- Tool detection now requires project-level configuration when `detectInProjectFile` is defined — PATH-only presence no longer sufficient, preventing false positives (e.g., mvn in PATH treated as Spotless available)
|
|
32
|
+
- Project file scanning starts from repo root instead of cwd, fixing detection when git hooks run from subdirectories
|
|
33
|
+
- Increased judge timeout from 120s to 180s
|
|
34
|
+
|
|
35
|
+
### 🐛 Fixed
|
|
36
|
+
- Fixed tool auto-fix being attempted after execution failures (ENOENT, EACCES) by tracking `executionFailed` flag and skipping fix step
|
|
37
|
+
- Fixed ENOENT handling in `runToolFix` — missing command binary now returns gracefully instead of being misinterpreted as a partial fix
|
|
38
|
+
- Fixed `git add` in restage step to use `execFileSync` with proper argument array instead of shell string interpolation
|
|
39
|
+
|
|
40
|
+
### 🔒 Security
|
|
41
|
+
- Eliminated shell injection risk in tool execution by switching from `execSync` (shell-based) to `execFileSync` (direct process spawn)
|
|
42
|
+
- Added `_escapeCmdMeta()` and `_prepareExec()` for safe cmd.exe argument passing when executing .cmd/.bat files on Windows/WSL
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## [2.35.0] - 2026-03-27
|
|
46
|
+
|
|
47
|
+
### ✨ Added
|
|
48
|
+
- Prettier formatting support — auto-formats JS/TS/CSS/HTML/JSON/YAML/MD files before linting (format first, then lint)
|
|
49
|
+
- Remote formatter configuration — preset-to-tools mapping fetched from centralized git-hooks-config repo, allowing team-wide control without releasing new versions
|
|
50
|
+
- New `parsePrettierOutput()` function for parsing Prettier --check output into structured issues
|
|
51
|
+
|
|
52
|
+
### 🔧 Changed
|
|
53
|
+
- Linting pipeline now runs formatters (Prettier) before linters (ESLint) for consistent code style
|
|
54
|
+
- Updated preset-to-tools mapping: frontend, fullstack, ai, and default presets now include Prettier
|
|
55
|
+
- `getLinterToolsForPreset()` now fetches remote config with local fallback instead of using hardcoded mapping only
|
|
56
|
+
- `runLinters()` and `checkLinterAvailability()` converted to async functions to support remote config fetching
|
|
57
|
+
- Unfixable linting issues are now forwarded to the Claude judge for semantic resolution instead of blocking directly
|
|
58
|
+
|
|
59
|
+
|
|
8
60
|
## [2.34.0] - 2026-03-27
|
|
9
61
|
|
|
10
62
|
### ✨ Added
|
package/CLAUDE.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
**Main use cases:**
|
|
8
8
|
|
|
9
|
-
1. **Pre-commit linting**: Runs linters (ESLint, Spotless, sqlfluff) on staged files before Claude analysis — fast, deterministic, auto-fix enabled by default
|
|
9
|
+
1. **Pre-commit linting**: Runs formatters and linters (Prettier, ESLint, Spotless, sqlfluff) on staged files before Claude analysis — fast, deterministic, auto-fix enabled by default
|
|
10
10
|
2. **Pre-commit analysis**: Detects security issues, bugs, and code smells before each commit (blocks on CRITICAL/BLOCKER only)
|
|
11
11
|
3. **Interactive analysis**: `claude-hooks analyze` - review all issues (INFO to BLOCKER) interactively before committing
|
|
12
12
|
4. **Automatic messages**: Write `git commit -m "auto"` and Claude generates the message in Conventional Commits format with task-id extracted from branch
|
|
13
13
|
5. **PR analysis**: `claude-hooks analyze-diff [branch]` generates title, description, and test plan for PRs
|
|
14
14
|
6. **PR review**: `claude-hooks analyze-pr <url>` analyzes a GitHub PR with preset guidelines, Linear ticket enrichment, and posts review comments
|
|
15
15
|
7. **PR creation**: `claude-hooks create-pr [branch]` creates the PR on GitHub with automatic metadata (reviewers from CODEOWNERS, labels by preset, merge strategy auto-detected from branch naming)
|
|
16
|
-
8. **Linting**: `claude-hooks lint [paths...]` runs linters on staged files, directories, or specific files — supports ESLint, Spotless, sqlfluff per preset
|
|
16
|
+
8. **Linting**: `claude-hooks lint [paths...]` runs formatters and linters on staged files, directories, or specific files — supports Prettier, ESLint, Spotless, sqlfluff per preset (remote config priority with local fallback)
|
|
17
17
|
9. **Coupling detection**: `claude-hooks check-coupling` scans open PRs targeting a base branch, computes file overlap, and reports which features are coupled (share modified files) — helps TL make informed decisions before cutting a release
|
|
18
18
|
10. **Shadow management**: `claude-hooks shadow <analyze|reset|sync>` manages the shadow branch lifecycle — analyze divergence vs main and active RC, reset shadow to a clean copy of main, or sync shadow with a source branch (RC, develop, feature)
|
|
19
19
|
11. **Release creation**: `claude-hooks create-release <major|minor|patch>` creates a release-candidate branch from develop, bumps version files, commits, pushes, and deploys to shadow — replaces the 8 manual steps executed every Tuesday by the Tech Lead
|
|
@@ -86,7 +86,7 @@ claude-git-hooks/
|
|
|
86
86
|
│ │ └── prepare-commit-msg.js # Message generation - auto commit messages
|
|
87
87
|
│ └── utils/ # Reusable modules - shared logic
|
|
88
88
|
│ ├── tool-runner.js # Generic tool executor - resolve, spawn, parse, auto-fix (v2.34.0)
|
|
89
|
-
│ ├── linter-runner.js # Linter orchestration - preset mapping, ESLint/Spotless/sqlfluff (v2.34.0)
|
|
89
|
+
│ ├── linter-runner.js # Linter orchestration - preset mapping, Prettier/ESLint/Spotless/sqlfluff, remote config (v2.34.0)
|
|
90
90
|
│ ├── analysis-engine.js # Shared analysis logic - file data, 3-tier routing, results (v2.13.0+)
|
|
91
91
|
│ ├── diff-analysis-orchestrator.js # Intelligent batch orchestration via Opus (v2.20.0)
|
|
92
92
|
│ ├── claude-client.js # Claude CLI wrapper - spawn, retry, model override
|
|
@@ -183,6 +183,7 @@ preset config (.claude/presets/{name}/config.json) ← HIGHEST PRIORITY
|
|
|
183
183
|
**Team-wide remote config** ([`mscope-S-L/git-hooks-config`](https://github.com/mscope-S-L/git-hooks-config)):
|
|
184
184
|
|
|
185
185
|
- `labels.json` — PR label rules (fetched by `remote-config.js`, consumed by `label-resolver.js`)
|
|
186
|
+
- `formatters.json` — preset-to-tools mapping for linting/formatting (fetched by `remote-config.js`, consumed by `linter-runner.js`)
|
|
186
187
|
- `permissions.json` — role-based authorization (fetched directly by `authorization.js`, fail-closed)
|
|
187
188
|
- Changes take effect immediately across all governed repos — no tool update needed
|
|
188
189
|
|
|
@@ -319,7 +320,7 @@ consolidateResults()
|
|
|
319
320
|
| `lib/cli-metadata.js` | Command registry | `commands`, `buildCommandMap()`, `generateCompletionData()`, `PRESET_NAMES`, `HOOK_NAMES`, `BUMP_TYPES` |
|
|
320
321
|
| `lib/config.js` | Config system | `getConfig()` |
|
|
321
322
|
| `tool-runner.js` | Generic tool executor | `isToolAvailable()`, `filterFilesByTool()`, `runTool()`, `runToolFix()`, `runToolWithAutoFix()`, `displayToolResult()` (v2.34.0) |
|
|
322
|
-
| `linter-runner.js` | Linter orchestration | `runLinters()`, `displayLintResults()`, `checkLinterAvailability()`, `getLinterToolsForPreset()`, `LINTER_TOOLS`, `PRESET_LINTERS`, `parseEslintOutput()`, `parseSpotlessOutput()`, `parseSqlfluffOutput()`, `filesToSpotlessRegex()` (v2.34.0)
|
|
323
|
+
| `linter-runner.js` | Linter orchestration | `runLinters()`, `displayLintResults()`, `checkLinterAvailability()`, `getLinterToolsForPreset()`, `LINTER_TOOLS`, `PRESET_LINTERS`, `parsePrettierOutput()`, `parseEslintOutput()`, `parseSpotlessOutput()`, `parseSqlfluffOutput()`, `filesToSpotlessRegex()` (v2.34.0) |
|
|
323
324
|
| `analysis-engine.js` | Shared analysis logic | `buildFileData()`, `buildFilesData()`, `runAnalysis()`, `consolidateResults()`, `hasBlockingIssues()`, `hasAnyIssues()`, `displayResults()`, `displayIssueSummary()` (v2.13.0+) |
|
|
324
325
|
| `diff-analysis-orchestrator.js` | Intelligent batch orchestration | `orchestrateBatches()`, `buildFileOverview()`, `detectDependencies()` (v2.20.0) |
|
|
325
326
|
| `claude-client.js` | Claude CLI wrapper | `analyzeCode()`, `executeClaudeWithRetry()`, `extractJSON()` — spawn, retry, model override |
|
|
@@ -359,8 +360,8 @@ consolidateResults()
|
|
|
359
360
|
7. **Adapter Pattern**: `git-operations.js` abstracts git commands into JS functions
|
|
360
361
|
8. **Singleton Pattern**: `config.js` loads configuration once per execution
|
|
361
362
|
9. **Guard Pattern**: `authorization.js` — fail-closed gate in `bin/claude-hooks` before command dispatch; static `PROTECTED_COMMANDS` set avoids API calls for unprotected commands; permissions sourced from `mscope-S-L/git-hooks-config/permissions.json`
|
|
362
|
-
10. **Remote Config Pattern**: `remote-config.js` — fetches JSON from `mscope-S-L/git-hooks-config`, caches per-process (including nulls), graceful degradation (warn + return null); `label-resolver.js` — callers
|
|
363
|
-
11. **Pipeline Pattern**: `tool-runner.js` + `linter-runner.js` — generic tool execution infrastructure;
|
|
363
|
+
10. **Remote Config Pattern**: `remote-config.js` — fetches JSON from `mscope-S-L/git-hooks-config`, caches per-process (including nulls), graceful degradation (warn + return null); `label-resolver.js` and `linter-runner.js` — callers fetch remote config and decide fallback (`labels.json` for PR labels, `formatters.json` for preset-to-tools mapping)
|
|
364
|
+
11. **Pipeline Pattern**: `tool-runner.js` + `linter-runner.js` — generic tool execution infrastructure; formatters (Prettier) and linters (ESLint, Spotless, sqlfluff) share the same resolve → spawn → parse → fix → re-stage pipeline. Tool definitions are data objects, not classes. Preset-to-tools mapping fetched from `mscope-S-L/git-hooks-config/formatters.json` (remote config priority, local fallback).
|
|
364
365
|
|
|
365
366
|
### Key Data Flows
|
|
366
367
|
|
|
@@ -372,13 +373,14 @@ git commit
|
|
|
372
373
|
→ filters by preset extensions + size
|
|
373
374
|
↓
|
|
374
375
|
→ LINTING STEP (fast, deterministic)
|
|
375
|
-
→ getLinterToolsForPreset(presetName)
|
|
376
|
-
|
|
376
|
+
→ getLinterToolsForPreset(presetName):
|
|
377
|
+
1. fetchRemoteConfig('formatters.json') → remote presetTools mapping
|
|
378
|
+
2. fallback to local PRESET_LINTERS if remote unavailable
|
|
379
|
+
→ for each tool (formatters first, then linters):
|
|
377
380
|
isToolAvailable() → not found? warn + install hint → skip
|
|
378
381
|
filterFilesByTool() → matching files
|
|
379
382
|
runToolWithAutoFix() → check → auto-fix → re-stage → re-check
|
|
380
|
-
→
|
|
381
|
-
→ if failOnWarning && warnings → exit 1 (COMMIT BLOCKED)
|
|
383
|
+
→ unfixable issues forwarded to judge
|
|
382
384
|
↓
|
|
383
385
|
→ continues to Claude analysis
|
|
384
386
|
```
|
package/README.md
CHANGED
|
@@ -82,6 +82,43 @@ export GITHUB_TOKEN="ghp_..."
|
|
|
82
82
|
|
|
83
83
|
Create token at https://github.com/settings/tokens with scopes: `repo`, `read:org`
|
|
84
84
|
|
|
85
|
+
### Linting & Formatting
|
|
86
|
+
|
|
87
|
+
Runs formatters and linters on staged files automatically during pre-commit, or on demand:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Lint staged files (default)
|
|
91
|
+
claude-hooks lint
|
|
92
|
+
|
|
93
|
+
# Lint all files in a directory
|
|
94
|
+
claude-hooks lint src/
|
|
95
|
+
|
|
96
|
+
# Lint specific files
|
|
97
|
+
claude-hooks lint file1.js file2.java
|
|
98
|
+
|
|
99
|
+
# Mix of directories and files
|
|
100
|
+
claude-hooks lint src/ lib/utils/ file.js
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Tools per preset** (configured via [remote config](https://github.com/mscope-S-L/git-hooks-config)):
|
|
104
|
+
|
|
105
|
+
| Preset | Tools |
|
|
106
|
+
| ----------- | ----------------------------- |
|
|
107
|
+
| `frontend` | Prettier, ESLint |
|
|
108
|
+
| `backend` | Spotless |
|
|
109
|
+
| `fullstack` | Prettier, ESLint, Spotless |
|
|
110
|
+
| `database` | sqlfluff |
|
|
111
|
+
| `ai` | Prettier, ESLint |
|
|
112
|
+
| `default` | Prettier, ESLint |
|
|
113
|
+
|
|
114
|
+
**Behavior:**
|
|
115
|
+
|
|
116
|
+
- Formatters run first (Prettier), then linters (ESLint) — format before lint
|
|
117
|
+
- Auto-fix enabled by default — fixes and re-stages files automatically
|
|
118
|
+
- Missing tools are skipped with install instructions (never blocks)
|
|
119
|
+
- Unfixable issues are forwarded to the Claude judge for semantic resolution
|
|
120
|
+
- Tool-to-preset mapping is fetched from remote config (team-controlled, no release needed)
|
|
121
|
+
|
|
85
122
|
### Analyze Code (Interactive Review)
|
|
86
123
|
|
|
87
124
|
Run interactive code analysis before committing:
|
package/lib/commands/install.js
CHANGED
|
@@ -673,7 +673,7 @@ export async function runInstall(args) {
|
|
|
673
673
|
const presetName = config.preset || 'default';
|
|
674
674
|
if (config.linting?.enabled !== false) {
|
|
675
675
|
const { checkLinterAvailability } = await import('../utils/linter-runner.js');
|
|
676
|
-
checkLinterAvailability(presetName);
|
|
676
|
+
await checkLinterAvailability(presetName);
|
|
677
677
|
}
|
|
678
678
|
} catch {
|
|
679
679
|
// Non-fatal — linter check failure should not block installation
|
package/lib/commands/lint.js
CHANGED
|
@@ -170,7 +170,7 @@ export async function runLint(args = []) {
|
|
|
170
170
|
|
|
171
171
|
info(`🎯 Linting ${filesToLint.length} file(s) with '${metadata.displayName}' preset`);
|
|
172
172
|
|
|
173
|
-
const lintResult = runLinters(filesToLint, config, presetName);
|
|
173
|
+
const lintResult = await runLinters(filesToLint, config, presetName);
|
|
174
174
|
displayLintResults(lintResult);
|
|
175
175
|
|
|
176
176
|
// Exit with error code if linting failed
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -152,7 +152,7 @@ const main = async () => {
|
|
|
152
152
|
logger.info('🔍 Running linters...');
|
|
153
153
|
const lintStartTime = Date.now();
|
|
154
154
|
const filePaths = validFiles.map((f) => (typeof f === 'string' ? f : f.path));
|
|
155
|
-
const lintResult = runLinters(filePaths, config, presetName);
|
|
155
|
+
const lintResult = await runLinters(filePaths, config, presetName);
|
|
156
156
|
displayLintResults(lintResult);
|
|
157
157
|
|
|
158
158
|
// Record lint metric
|
package/lib/utils/judge.js
CHANGED
|
@@ -28,7 +28,7 @@ import logger from './logger.js';
|
|
|
28
28
|
import { recordMetric } from './metrics.js';
|
|
29
29
|
|
|
30
30
|
const JUDGE_DEFAULT_MODEL = 'sonnet';
|
|
31
|
-
const JUDGE_TIMEOUT =
|
|
31
|
+
const JUDGE_TIMEOUT = 180000;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Applies a single search/replace fix to a file and stages it
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* - logger: Debug and error logging
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import os from 'os';
|
|
18
20
|
import path from 'path';
|
|
19
21
|
import {
|
|
20
22
|
isToolAvailable,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
displayToolResult
|
|
24
26
|
} from './tool-runner.js';
|
|
25
27
|
import { getRepoRoot } from './git-operations.js';
|
|
28
|
+
import { fetchRemoteConfig } from './remote-config.js';
|
|
26
29
|
import logger from './logger.js';
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -157,6 +160,46 @@ export function parseSqlfluffOutput(stdout) {
|
|
|
157
160
|
return { errors, warnings };
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Parse Prettier --check output into structured issues
|
|
165
|
+
* Prettier outputs [warn] lines for files that need formatting
|
|
166
|
+
*
|
|
167
|
+
* @param {string} stdout - Prettier --check stdout
|
|
168
|
+
* @returns {{ errors: Array, warnings: Array }}
|
|
169
|
+
*/
|
|
170
|
+
export function parsePrettierOutput(stdout) {
|
|
171
|
+
const errors = [];
|
|
172
|
+
const warnings = [];
|
|
173
|
+
|
|
174
|
+
const lines = stdout.split('\n');
|
|
175
|
+
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
const trimmed = line.trim();
|
|
178
|
+
|
|
179
|
+
// Match [warn] <filepath> lines — skip summary lines
|
|
180
|
+
const match = trimmed.match(/^\[warn\]\s+(.+)$/);
|
|
181
|
+
if (!match) continue;
|
|
182
|
+
|
|
183
|
+
const content = match[1];
|
|
184
|
+
|
|
185
|
+
// Skip Prettier's summary lines (not file paths)
|
|
186
|
+
if (
|
|
187
|
+
content.includes('Code style issues') ||
|
|
188
|
+
content.includes('Forgot to run Prettier')
|
|
189
|
+
) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
errors.push({
|
|
194
|
+
file: content,
|
|
195
|
+
severity: 'error',
|
|
196
|
+
message: 'File is not properly formatted'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { errors, warnings };
|
|
201
|
+
}
|
|
202
|
+
|
|
160
203
|
/**
|
|
161
204
|
* Convert file paths to Spotless-compatible regex pattern
|
|
162
205
|
* Spotless -DspotlessFiles accepts a regex matching absolute file paths
|
|
@@ -171,25 +214,94 @@ export function filesToSpotlessRegex(files) {
|
|
|
171
214
|
return escaped.join('|');
|
|
172
215
|
}
|
|
173
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Find Maven Wrapper (mvnw/mvnw.cmd) near the pom.xml directory.
|
|
219
|
+
* Maven Wrapper invokes Java directly, bypassing cmd.exe — avoids
|
|
220
|
+
* metacharacter issues with | in command-line args on Windows/WSL2.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} configDir - Directory containing pom.xml
|
|
223
|
+
* @returns {string|null} Path to mvnw/mvnw.cmd, or null if not found
|
|
224
|
+
*/
|
|
225
|
+
function _findMavenWrapper(configDir) {
|
|
226
|
+
if (!configDir) return null;
|
|
227
|
+
|
|
228
|
+
// On Windows, prefer .cmd (executable) over extensionless (bash script).
|
|
229
|
+
// On Linux/macOS/WSL, prefer extensionless (native) over .cmd.
|
|
230
|
+
const candidates = os.platform() === 'win32'
|
|
231
|
+
? [path.join(configDir, 'mvnw.cmd'), path.join(configDir, 'mvnw')]
|
|
232
|
+
: [path.join(configDir, 'mvnw'), path.join(configDir, 'mvnw.cmd')];
|
|
233
|
+
|
|
234
|
+
for (const candidate of candidates) {
|
|
235
|
+
if (fs.existsSync(candidate)) {
|
|
236
|
+
logger.debug('linter-runner - _findMavenWrapper',
|
|
237
|
+
'Found Maven Wrapper', { path: candidate });
|
|
238
|
+
return candidate;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
logger.debug('linter-runner - _findMavenWrapper',
|
|
243
|
+
'No Maven Wrapper found', { configDir });
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
174
247
|
/**
|
|
175
248
|
* Available linter tool definitions
|
|
176
249
|
* @type {Object<string, import('./tool-runner.js').ToolDefinition>}
|
|
177
250
|
*/
|
|
178
251
|
export const LINTER_TOOLS = {
|
|
252
|
+
prettier: {
|
|
253
|
+
name: 'prettier',
|
|
254
|
+
command: 'npx',
|
|
255
|
+
args: (files) => ['prettier', '--check', ...files],
|
|
256
|
+
fixArgs: (files) => ['prettier', '--write', ...files],
|
|
257
|
+
detectCommand: 'prettier',
|
|
258
|
+
// Array form: multiple detection strategies. First match wins.
|
|
259
|
+
// Prettier may be a direct dep, or bundled via meta-frameworks,
|
|
260
|
+
// or only indicated by a config file.
|
|
261
|
+
detectInProjectFile: [
|
|
262
|
+
{
|
|
263
|
+
filename: 'package.json',
|
|
264
|
+
check: (content) => {
|
|
265
|
+
const pkg = JSON.parse(content);
|
|
266
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
267
|
+
return 'prettier' in allDeps;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{ filename: '.prettierrc', check: () => true },
|
|
271
|
+
{ filename: '.prettierrc.json', check: () => true },
|
|
272
|
+
{ filename: '.prettierrc.js', check: () => true },
|
|
273
|
+
{ filename: 'prettier.config.js', check: () => true },
|
|
274
|
+
{ filename: 'prettier.config.mjs', check: () => true }
|
|
275
|
+
],
|
|
276
|
+
installHint: 'npm install --save-dev prettier',
|
|
277
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.html', '.json', '.md', '.yaml', '.yml'],
|
|
278
|
+
parseOutput: parsePrettierOutput,
|
|
279
|
+
timeout: 30000
|
|
280
|
+
},
|
|
179
281
|
eslint: {
|
|
180
282
|
name: 'eslint',
|
|
181
283
|
command: 'npx',
|
|
182
284
|
args: (files) => ['eslint', '--format', 'json', '--no-error-on-unmatched-pattern', ...files],
|
|
183
285
|
fixArgs: (files) => ['eslint', '--fix', '--no-error-on-unmatched-pattern', ...files],
|
|
184
286
|
detectCommand: 'eslint',
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
287
|
+
// Array form: ESLint may be a direct dep, bundled via react-scripts/next,
|
|
288
|
+
// indicated by eslintConfig in package.json, or by a config file.
|
|
289
|
+
detectInProjectFile: [
|
|
290
|
+
{
|
|
291
|
+
filename: 'package.json',
|
|
292
|
+
check: (content) => {
|
|
293
|
+
const pkg = JSON.parse(content);
|
|
294
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
295
|
+
return 'eslint' in allDeps || 'eslintConfig' in pkg;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{ filename: '.eslintrc.json', check: () => true },
|
|
299
|
+
{ filename: '.eslintrc.js', check: () => true },
|
|
300
|
+
{ filename: '.eslintrc.yml', check: () => true },
|
|
301
|
+
{ filename: '.eslintrc', check: () => true },
|
|
302
|
+
{ filename: 'eslint.config.js', check: () => true },
|
|
303
|
+
{ filename: 'eslint.config.mjs', check: () => true }
|
|
304
|
+
],
|
|
193
305
|
installHint: 'npm install --save-dev eslint',
|
|
194
306
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
195
307
|
parseOutput: parseEslintOutput,
|
|
@@ -212,7 +324,14 @@ export const LINTER_TOOLS = {
|
|
|
212
324
|
installHint: 'Add spotless-maven-plugin to pom.xml (see https://github.com/diffplug/spotless)',
|
|
213
325
|
extensions: ['.java'],
|
|
214
326
|
parseOutput: parseSpotlessOutput,
|
|
215
|
-
timeout: 60000
|
|
327
|
+
timeout: 60000,
|
|
328
|
+
// Run per-file to avoid | (pipe) in regex on Windows.
|
|
329
|
+
// filesToSpotlessRegex joins files with | for regex alternation.
|
|
330
|
+
// cmd.exe interprets | as a pipe at every expansion level —
|
|
331
|
+
// ^ escaping is consumed by the first cmd.exe parse, and %VAR% expansion
|
|
332
|
+
// in batch scripts (mvn.cmd) re-exposes unescaped | to a second parse.
|
|
333
|
+
// Per-file execution ensures each regex has at most one file pattern (no |).
|
|
334
|
+
perFile: true
|
|
216
335
|
},
|
|
217
336
|
sqlfluff: {
|
|
218
337
|
name: 'sqlfluff',
|
|
@@ -228,27 +347,63 @@ export const LINTER_TOOLS = {
|
|
|
228
347
|
};
|
|
229
348
|
|
|
230
349
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
350
|
+
* Local fallback preset-to-tool mapping
|
|
351
|
+
* Used when remote formatters.json is unavailable
|
|
233
352
|
* @type {Object<string, string[]>}
|
|
234
353
|
*/
|
|
235
354
|
export const PRESET_LINTERS = {
|
|
236
|
-
frontend: ['eslint'],
|
|
355
|
+
frontend: ['prettier', 'eslint'],
|
|
237
356
|
backend: ['spotless'],
|
|
238
|
-
fullstack: ['eslint', 'spotless'],
|
|
357
|
+
fullstack: ['prettier', 'eslint', 'spotless'],
|
|
239
358
|
database: ['sqlfluff'],
|
|
240
|
-
ai: ['eslint'],
|
|
241
|
-
default: ['eslint']
|
|
359
|
+
ai: ['prettier', 'eslint'],
|
|
360
|
+
default: ['prettier', 'eslint']
|
|
242
361
|
};
|
|
243
362
|
|
|
244
363
|
/**
|
|
245
|
-
* Get linter tool definitions for a preset
|
|
364
|
+
* Get linter/formatter tool definitions for a preset.
|
|
365
|
+
* Fetches preset-to-tools mapping from remote config (formatters.json),
|
|
366
|
+
* falls back to local PRESET_LINTERS when remote is unavailable.
|
|
246
367
|
*
|
|
247
368
|
* @param {string} presetName - Preset name
|
|
248
|
-
* @returns {import('./tool-runner.js').ToolDefinition[]} Tool definitions
|
|
369
|
+
* @returns {Promise<import('./tool-runner.js').ToolDefinition[]>} Tool definitions
|
|
249
370
|
*/
|
|
250
|
-
export function getLinterToolsForPreset(presetName) {
|
|
251
|
-
|
|
371
|
+
export async function getLinterToolsForPreset(presetName) {
|
|
372
|
+
// Try remote config first
|
|
373
|
+
const remoteConfig = await fetchRemoteConfig('formatters.json');
|
|
374
|
+
|
|
375
|
+
let toolNames;
|
|
376
|
+
|
|
377
|
+
if (remoteConfig?.presetTools?.[presetName]) {
|
|
378
|
+
toolNames = remoteConfig.presetTools[presetName];
|
|
379
|
+
logger.debug('linter-runner - getLinterToolsForPreset', 'Using remote preset tools', {
|
|
380
|
+
preset: presetName,
|
|
381
|
+
tools: toolNames
|
|
382
|
+
});
|
|
383
|
+
} else if (remoteConfig?.presetTools?.default) {
|
|
384
|
+
toolNames = remoteConfig.presetTools.default;
|
|
385
|
+
logger.debug('linter-runner - getLinterToolsForPreset',
|
|
386
|
+
'Preset not in remote config, using remote default', {
|
|
387
|
+
preset: presetName,
|
|
388
|
+
tools: toolNames
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
// Fall back to local
|
|
392
|
+
toolNames = PRESET_LINTERS[presetName] || PRESET_LINTERS.default;
|
|
393
|
+
logger.debug('linter-runner - getLinterToolsForPreset', 'Using local fallback', {
|
|
394
|
+
preset: presetName,
|
|
395
|
+
tools: toolNames,
|
|
396
|
+
remoteAvailable: remoteConfig !== null
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// User-visible source logging — makes non-determinism visible when remote config
|
|
401
|
+
// flips between available and unavailable across invocations
|
|
402
|
+
const hasPresetInRemote = remoteConfig?.presetTools?.[presetName];
|
|
403
|
+
const hasDefaultInRemote = remoteConfig?.presetTools?.default;
|
|
404
|
+
const source = hasPresetInRemote ? 'remote' : hasDefaultInRemote ? 'remote default' : 'local';
|
|
405
|
+
logger.info(`Linter tools [${source}]: ${toolNames.join(', ')}`);
|
|
406
|
+
|
|
252
407
|
return toolNames.map((name) => LINTER_TOOLS[name]).filter(Boolean);
|
|
253
408
|
}
|
|
254
409
|
|
|
@@ -260,12 +415,12 @@ export function getLinterToolsForPreset(presetName) {
|
|
|
260
415
|
* @param {string} [presetName] - Preset name (default: 'default')
|
|
261
416
|
* @returns {{ results: ToolRunResult[], totalErrors: number, totalWarnings: number, totalFixed: number }}
|
|
262
417
|
*/
|
|
263
|
-
export function runLinters(files, config, presetName = 'default') {
|
|
418
|
+
export async function runLinters(files, config, presetName = 'default') {
|
|
264
419
|
const lintConfig = config.linting || {};
|
|
265
420
|
const autoFix = lintConfig.autoFix !== false;
|
|
266
421
|
const timeout = lintConfig.timeout || 30000;
|
|
267
422
|
|
|
268
|
-
const tools = getLinterToolsForPreset(presetName);
|
|
423
|
+
const tools = await getLinterToolsForPreset(presetName);
|
|
269
424
|
const results = [];
|
|
270
425
|
|
|
271
426
|
logger.debug('linter-runner - runLinters', 'Starting linters', {
|
|
@@ -306,14 +461,53 @@ export function runLinters(files, config, presetName = 'default') {
|
|
|
306
461
|
continue;
|
|
307
462
|
}
|
|
308
463
|
|
|
309
|
-
//
|
|
310
|
-
|
|
464
|
+
// Maven Wrapper: prefer mvnw when available (invokes Java directly, bypasses cmd.exe).
|
|
465
|
+
// Cannot reuse localBin — _prepareToolCall strips args[0] for npx tools,
|
|
466
|
+
// but Spotless args are Maven goals (spotless:check), not a tool name to strip.
|
|
467
|
+
// Shallow-clone toolDef with wrapper as command instead.
|
|
468
|
+
let effectiveToolDef = toolDef;
|
|
469
|
+
if (!availability.localBin && availability.configDir && toolDef.command === 'mvn') {
|
|
470
|
+
const wrapper = _findMavenWrapper(availability.configDir);
|
|
471
|
+
if (wrapper) {
|
|
472
|
+
effectiveToolDef = { ...toolDef, command: wrapper };
|
|
473
|
+
logger.info(`Using Maven Wrapper: ${path.basename(wrapper)}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const toolOpts = {
|
|
311
478
|
autoFix,
|
|
312
479
|
timeout,
|
|
313
|
-
restage: true
|
|
314
|
-
|
|
480
|
+
restage: true,
|
|
481
|
+
localBin: availability.localBin,
|
|
482
|
+
toolCwd: availability.configDir
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// Per-file execution: run once per file to avoid | (pipe) in command args.
|
|
486
|
+
// On Windows, cmd.exe interprets | as a pipe at every expansion level —
|
|
487
|
+
// ^ escaping is consumed at the first parse, and %VAR% expansion in batch
|
|
488
|
+
// scripts (mvn.cmd) re-exposes unescaped | to a second parse.
|
|
489
|
+
// Running per-file ensures each invocation has a single-file regex (no |).
|
|
490
|
+
if (effectiveToolDef.perFile && matchingFiles.length > 1) {
|
|
491
|
+
logger.debug('linter-runner - runLinters',
|
|
492
|
+
`Running ${effectiveToolDef.name} per-file`, { fileCount: matchingFiles.length });
|
|
493
|
+
|
|
494
|
+
const perFileResults = matchingFiles.map((file) =>
|
|
495
|
+
runToolWithAutoFix(effectiveToolDef, [file], toolOpts)
|
|
496
|
+
);
|
|
315
497
|
|
|
316
|
-
|
|
498
|
+
results.push({
|
|
499
|
+
tool: effectiveToolDef.name,
|
|
500
|
+
skipped: false,
|
|
501
|
+
errors: perFileResults.flatMap((r) => r.errors),
|
|
502
|
+
warnings: perFileResults.flatMap((r) => r.warnings),
|
|
503
|
+
fixedCount: perFileResults.reduce((sum, r) => sum + r.fixedCount, 0),
|
|
504
|
+
fixedFiles: perFileResults.flatMap((r) => r.fixedFiles)
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
// Normal batch execution
|
|
508
|
+
const result = runToolWithAutoFix(effectiveToolDef, matchingFiles, toolOpts);
|
|
509
|
+
results.push(result);
|
|
510
|
+
}
|
|
317
511
|
}
|
|
318
512
|
|
|
319
513
|
// Aggregate totals
|
|
@@ -415,8 +609,8 @@ export function lintIssuesToAnalysisDetails(lintResult) {
|
|
|
415
609
|
*
|
|
416
610
|
* @param {string} presetName - Preset name
|
|
417
611
|
*/
|
|
418
|
-
export function checkLinterAvailability(presetName) {
|
|
419
|
-
const tools = getLinterToolsForPreset(presetName);
|
|
612
|
+
export async function checkLinterAvailability(presetName) {
|
|
613
|
+
const tools = await getLinterToolsForPreset(presetName);
|
|
420
614
|
|
|
421
615
|
if (tools.length === 0) {
|
|
422
616
|
return;
|
package/lib/utils/tool-runner.js
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
* - logger: Debug and error logging
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { execFileSync } from 'child_process';
|
|
21
21
|
import fs from 'fs';
|
|
22
|
+
import os from 'os';
|
|
22
23
|
import path from 'path';
|
|
23
24
|
import { which } from './which-command.js';
|
|
24
25
|
import { walkDirectoryTree } from './file-utils.js';
|
|
26
|
+
import { getRepoRoot } from './git-operations.js';
|
|
25
27
|
import logger from './logger.js';
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -31,11 +33,13 @@ import logger from './logger.js';
|
|
|
31
33
|
* @property {function(string[]): string[]} args - Build args for check mode
|
|
32
34
|
* @property {function(string[]): string[]} [fixArgs] - Build args for auto-fix mode
|
|
33
35
|
* @property {string} detectCommand - Binary to check via which() (e.g., 'eslint')
|
|
36
|
+
* @property {Object|Object[]} [detectInProjectFile] - Project file indicator(s). Single { filename, check } or array of them. When defined, project file validation is mandatory — PATH alone is not sufficient.
|
|
34
37
|
* @property {string} [detectFallback] - Fallback path to check (e.g., 'node_modules/.bin/eslint')
|
|
35
38
|
* @property {string} installHint - Install instruction shown when tool is missing
|
|
36
39
|
* @property {string[]} extensions - File extensions this tool handles
|
|
37
40
|
* @property {function(string): Object} parseOutput - Parse stdout into structured results
|
|
38
41
|
* @property {number} [timeout] - Per-tool timeout in ms (default from config)
|
|
42
|
+
* @property {function(string[]): Object} [getEnv] - Build extra env vars for invocation (e.g., MAVEN_OPTS for Spotless regex)
|
|
39
43
|
*/
|
|
40
44
|
|
|
41
45
|
/**
|
|
@@ -62,38 +66,51 @@ import logger from './logger.js';
|
|
|
62
66
|
/**
|
|
63
67
|
* Check if a tool is available on the system
|
|
64
68
|
*
|
|
69
|
+
* When detectInProjectFile is defined, the tool requires project-level
|
|
70
|
+
* configuration (e.g., spotless-maven-plugin in pom.xml, eslint in package.json).
|
|
71
|
+
* In that case, finding the command binary in PATH is necessary but NOT sufficient —
|
|
72
|
+
* the project must declare the tool. This prevents false positives like mvn in PATH
|
|
73
|
+
* being treated as "Spotless available" when the project has no Spotless plugin.
|
|
74
|
+
*
|
|
65
75
|
* @param {ToolDefinition} toolDef - Tool definition
|
|
66
76
|
* @returns {{ available: boolean, path: string|null }} Availability info
|
|
67
77
|
*/
|
|
68
78
|
export function isToolAvailable(toolDef) {
|
|
69
|
-
// Strategy 1:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
logger.debug('tool-runner - isToolAvailable', `Found ${toolDef.name} in PATH`, { path: resolved });
|
|
73
|
-
return { available: true, path: resolved };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Strategy 2: Check project files for the tool as a dependency
|
|
77
|
-
// Why: local installs (npm install --save-dev) live in node_modules/.bin,
|
|
78
|
-
// not in PATH. npx resolves them at runtime, so we just need to confirm
|
|
79
|
-
// the tool is declared in a project file (package.json deps or pom.xml plugin).
|
|
80
|
-
// Uses walkDirectoryTree (same walker as version-manager) to scan up to 3 levels.
|
|
79
|
+
// Strategy 1: Tools that require project-level configuration
|
|
80
|
+
// The detectInProjectFile field signals that detectCommand (e.g., 'mvn') is a
|
|
81
|
+
// build tool, not the tool itself. Project file validation is mandatory.
|
|
81
82
|
if (toolDef.detectInProjectFile) {
|
|
82
83
|
const found = _detectInProjectFiles(toolDef);
|
|
83
84
|
if (found) {
|
|
84
|
-
// Tool is configured but
|
|
85
|
+
// Tool is configured but not runnable (binary missing or not installed locally)
|
|
85
86
|
if (found.configured && !found.available) {
|
|
86
87
|
logger.debug('tool-runner - isToolAvailable',
|
|
87
|
-
`${toolDef.name} configured but
|
|
88
|
+
`${toolDef.name} configured but not runnable`, {
|
|
89
|
+
missingCommand: found.missingCommand || null
|
|
90
|
+
}
|
|
88
91
|
);
|
|
89
92
|
return {
|
|
90
93
|
available: false,
|
|
91
94
|
path: null,
|
|
92
|
-
|
|
95
|
+
configured: true,
|
|
96
|
+
installHint: found.installHint || `${toolDef.name} is configured in your project but '${found.missingCommand}' is not in PATH. Install it and ensure it's accessible from your terminal.`
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
return found;
|
|
96
100
|
}
|
|
101
|
+
// Project file not found or tool not declared → not available for this project
|
|
102
|
+
logger.debug('tool-runner - isToolAvailable',
|
|
103
|
+
`${toolDef.name} not configured in project files`
|
|
104
|
+
);
|
|
105
|
+
return { available: false, path: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Strategy 2: Simple tools without project file requirements (e.g., sqlfluff)
|
|
109
|
+
// PATH detection is sufficient — the tool is standalone.
|
|
110
|
+
const resolved = which(toolDef.detectCommand);
|
|
111
|
+
if (resolved) {
|
|
112
|
+
logger.debug('tool-runner - isToolAvailable', `Found ${toolDef.name} in PATH`, { path: resolved });
|
|
113
|
+
return { available: true, path: resolved };
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
logger.debug('tool-runner - isToolAvailable', `${toolDef.name} not found`);
|
|
@@ -101,17 +118,81 @@ export function isToolAvailable(toolDef) {
|
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
121
|
+
* Find a tool binary in local node_modules/.bin, walking up from configDir to rootDir.
|
|
122
|
+
* Prevents npx from downloading or using a globally cached version that may be
|
|
123
|
+
* incompatible with the project's configuration format.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} command - Binary name to find (e.g., 'eslint', 'prettier')
|
|
126
|
+
* @param {string} configDir - Directory where the tool's config was found
|
|
127
|
+
* @param {string} rootDir - Repo root (stop searching here)
|
|
128
|
+
* @returns {string|null} Path to local binary, or null if not found
|
|
129
|
+
*/
|
|
130
|
+
function _findLocalBinary(command, configDir, rootDir) {
|
|
131
|
+
let dir = configDir;
|
|
132
|
+
|
|
133
|
+
while (dir) {
|
|
134
|
+
const binDir = path.join(dir, 'node_modules', '.bin');
|
|
135
|
+
// Check both extensionless and .cmd — WSL repos on Windows filesystem
|
|
136
|
+
// may have .cmd files from a Windows npm install
|
|
137
|
+
// On Windows, prefer .cmd (executable) over extensionless (bash script).
|
|
138
|
+
// On Linux/macOS, prefer extensionless (native) over .cmd.
|
|
139
|
+
const candidates = os.platform() === 'win32'
|
|
140
|
+
? [path.join(binDir, `${command}.cmd`), path.join(binDir, command)]
|
|
141
|
+
: [path.join(binDir, command), path.join(binDir, `${command}.cmd`)];
|
|
142
|
+
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
if (fs.existsSync(candidate)) {
|
|
145
|
+
return candidate;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Stop at repo root
|
|
150
|
+
if (dir === rootDir) break;
|
|
151
|
+
const parent = path.dirname(dir);
|
|
152
|
+
if (parent === dir) break; // filesystem root
|
|
153
|
+
dir = parent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Scan project files (package.json, pom.xml, config files) up to 3 levels deep
|
|
161
|
+
* to detect if a tool is declared as a dependency, plugin, or has a config file.
|
|
162
|
+
*
|
|
163
|
+
* detectInProjectFile can be a single { filename, check } or an array of them.
|
|
164
|
+
* Array form allows multiple detection strategies (e.g., ESLint: check package.json
|
|
165
|
+
* deps, eslintConfig key, OR .eslintrc.json existence). First match wins.
|
|
106
166
|
*
|
|
107
167
|
* @param {ToolDefinition} toolDef - Tool definition with detectInProjectFile
|
|
108
168
|
* @returns {{ available: boolean, path: string|null } | null} Result or null if not found
|
|
109
169
|
*/
|
|
110
170
|
export function _detectInProjectFiles(toolDef) {
|
|
111
|
-
|
|
171
|
+
// Normalize to array — supports both single object and array of indicators
|
|
172
|
+
const checks = Array.isArray(toolDef.detectInProjectFile)
|
|
173
|
+
? toolDef.detectInProjectFile
|
|
174
|
+
: [toolDef.detectInProjectFile];
|
|
175
|
+
|
|
112
176
|
let found = false;
|
|
177
|
+
let configDir = null;
|
|
178
|
+
|
|
179
|
+
// Use repo root instead of cwd — git hooks may run from subdirectories
|
|
180
|
+
// and pom.xml / package.json at repo root could be beyond maxDepth from a deep cwd.
|
|
181
|
+
let startDir;
|
|
182
|
+
try {
|
|
183
|
+
startDir = getRepoRoot();
|
|
184
|
+
} catch {
|
|
185
|
+
startDir = process.cwd();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
logger.debug('tool-runner - _detectInProjectFiles', 'Scanning from directory', {
|
|
189
|
+
startDir,
|
|
190
|
+
cwd: process.cwd(),
|
|
191
|
+
usingRepoRoot: startDir !== process.cwd(),
|
|
192
|
+
filenames: checks.map((c) => c.filename)
|
|
193
|
+
});
|
|
113
194
|
|
|
114
|
-
walkDirectoryTree(
|
|
195
|
+
walkDirectoryTree(startDir, {
|
|
115
196
|
maxDepth: 3,
|
|
116
197
|
ignoreSet: new Set([
|
|
117
198
|
'.git', 'node_modules', 'target', 'build', 'dist',
|
|
@@ -119,17 +200,21 @@ export function _detectInProjectFiles(toolDef) {
|
|
|
119
200
|
]),
|
|
120
201
|
onFile: (entry, fullPath) => {
|
|
121
202
|
if (found) return; // short-circuit after first match
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
203
|
+
for (const { filename, check } of checks) {
|
|
204
|
+
if (entry.name === filename) {
|
|
205
|
+
try {
|
|
206
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
207
|
+
if (check(content)) {
|
|
208
|
+
logger.debug('tool-runner - _detectInProjectFiles',
|
|
209
|
+
`Found ${toolDef.name} in ${path.relative(startDir, fullPath)}`
|
|
210
|
+
);
|
|
211
|
+
found = true;
|
|
212
|
+
configDir = path.dirname(fullPath);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// File read error — skip
|
|
130
217
|
}
|
|
131
|
-
} catch {
|
|
132
|
-
// File read error — skip
|
|
133
218
|
}
|
|
134
219
|
}
|
|
135
220
|
}
|
|
@@ -140,24 +225,109 @@ export function _detectInProjectFiles(toolDef) {
|
|
|
140
225
|
}
|
|
141
226
|
|
|
142
227
|
// Project file declares the tool, but can we actually run the command?
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
228
|
+
if (toolDef.command === 'npx') {
|
|
229
|
+
// npx-based tools: verify the package is installed locally.
|
|
230
|
+
// Without this, npx may download or use a globally cached version
|
|
231
|
+
// that's incompatible with the project's config (e.g., ESLint v10
|
|
232
|
+
// when the project uses .eslintrc.* from v8).
|
|
233
|
+
const localBin = _findLocalBinary(toolDef.detectCommand, configDir, startDir);
|
|
234
|
+
if (localBin) {
|
|
148
235
|
logger.debug('tool-runner - _detectInProjectFiles',
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
available: false,
|
|
153
|
-
path: null,
|
|
154
|
-
configured: true,
|
|
155
|
-
missingCommand: toolDef.command
|
|
156
|
-
};
|
|
236
|
+
`Found local ${toolDef.name} binary`, { path: localBin });
|
|
237
|
+
return { available: true, path: toolDef.command, localBin, configDir };
|
|
157
238
|
}
|
|
239
|
+
logger.debug('tool-runner - _detectInProjectFiles',
|
|
240
|
+
`${toolDef.name} configured but not installed locally`);
|
|
241
|
+
return {
|
|
242
|
+
available: false,
|
|
243
|
+
path: null,
|
|
244
|
+
configured: true,
|
|
245
|
+
installHint: `${toolDef.name} config found but the package is not installed locally. Run: ${toolDef.installHint}`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Non-npx tools (mvn, sqlfluff, etc.) need the command binary in PATH.
|
|
250
|
+
const cmdResolved = which(toolDef.command);
|
|
251
|
+
if (!cmdResolved) {
|
|
252
|
+
logger.debug('tool-runner - _detectInProjectFiles',
|
|
253
|
+
`${toolDef.name} configured in project but '${toolDef.command}' not in PATH`
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
available: false,
|
|
257
|
+
path: null,
|
|
258
|
+
configured: true,
|
|
259
|
+
missingCommand: toolDef.command
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { available: true, path: toolDef.command, configDir };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve a tool's command to an absolute path.
|
|
268
|
+
* execFileSync bypasses the shell, so commands like 'npx' or 'mvn' that live
|
|
269
|
+
* in nvm/shell-initialized paths won't be found via process.env.PATH alone.
|
|
270
|
+
* which() uses the platform shell (which/where) to resolve the full path.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} command - Command name (e.g., 'npx', 'mvn', 'git')
|
|
273
|
+
* @returns {string} Resolved path, or original command as fallback
|
|
274
|
+
*/
|
|
275
|
+
function _resolveCommand(command) {
|
|
276
|
+
const resolved = which(command);
|
|
277
|
+
if (resolved) {
|
|
278
|
+
logger.debug('tool-runner - _resolveCommand', `Resolved '${command}' to '${resolved}'`);
|
|
279
|
+
return resolved;
|
|
158
280
|
}
|
|
281
|
+
logger.debug('tool-runner - _resolveCommand', `Could not resolve '${command}', using as-is`);
|
|
282
|
+
return command;
|
|
283
|
+
}
|
|
159
284
|
|
|
160
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Escape cmd.exe metacharacters with ^ so they are treated as literals.
|
|
287
|
+
* Required when passing args through cmd.exe /c (e.g., for .cmd/.bat files).
|
|
288
|
+
* Without escaping, characters like | are interpreted as pipes by cmd.exe.
|
|
289
|
+
*
|
|
290
|
+
* @param {string} arg - Single argument string
|
|
291
|
+
* @returns {string} Escaped argument safe for cmd.exe
|
|
292
|
+
*/
|
|
293
|
+
function _escapeCmdMeta(arg) {
|
|
294
|
+
// ^ must be escaped first to avoid double-escaping
|
|
295
|
+
return arg.replace(/\^/g, '^^')
|
|
296
|
+
.replace(/%/g, '%%')
|
|
297
|
+
.replace(/&/g, '^&')
|
|
298
|
+
.replace(/\|/g, '^|')
|
|
299
|
+
.replace(/</g, '^<')
|
|
300
|
+
.replace(/>/g, '^>')
|
|
301
|
+
.replace(/\(/g, '^(')
|
|
302
|
+
.replace(/\)/g, '^)');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Prepare command and args for execFileSync when the resolved command is a
|
|
307
|
+
* .cmd/.bat file. These are batch scripts that must run through cmd.exe —
|
|
308
|
+
* execFileSync bypasses the shell, so direct execution throws EINVAL
|
|
309
|
+
* (CVE-2024-27980) on native Windows, or pipes args through cmd.exe on WSL
|
|
310
|
+
* where Windows interop auto-routes .cmd files.
|
|
311
|
+
*
|
|
312
|
+
* Detection is by file extension, NOT os.platform(), because WSL2 reports
|
|
313
|
+
* 'linux' but resolves Windows tools (npx.cmd, mvn.cmd) via PATH interop.
|
|
314
|
+
*
|
|
315
|
+
* Args are escaped for cmd.exe metacharacters (|, &, <, >, ^, (, ))
|
|
316
|
+
* to prevent shell interpretation (e.g., | in Spotless regex patterns).
|
|
317
|
+
*
|
|
318
|
+
* @param {string} command - Resolved command path
|
|
319
|
+
* @param {string[]} args - Command arguments
|
|
320
|
+
* @returns {{ command: string, args: string[] }} Adjusted for platform
|
|
321
|
+
*/
|
|
322
|
+
function _prepareExec(command, args) {
|
|
323
|
+
if (/\.(cmd|bat)$/i.test(command)) {
|
|
324
|
+
logger.debug('tool-runner - _prepareExec', 'Wrapping .cmd with cmd.exe /c', { command });
|
|
325
|
+
return {
|
|
326
|
+
command: process.env.ComSpec || 'cmd.exe',
|
|
327
|
+
args: ['/c', command, ...args.map(_escapeCmdMeta)]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return { command, args };
|
|
161
331
|
}
|
|
162
332
|
|
|
163
333
|
/**
|
|
@@ -174,6 +344,58 @@ export function filterFilesByTool(files, toolDef) {
|
|
|
174
344
|
});
|
|
175
345
|
}
|
|
176
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Resolve command, args, and cwd for a tool invocation.
|
|
349
|
+
* Shared by runTool and runToolFix to avoid duplication.
|
|
350
|
+
*
|
|
351
|
+
* When a local binary is provided, it replaces npx and the first arg (tool name)
|
|
352
|
+
* is stripped. When toolCwd is provided, files become absolute so they resolve
|
|
353
|
+
* correctly from the tool's config directory (needed for plugin resolution).
|
|
354
|
+
*
|
|
355
|
+
* @param {ToolDefinition} toolDef - Tool definition
|
|
356
|
+
* @param {string[]} rawArgs - Args from toolDef.args() or toolDef.fixArgs()
|
|
357
|
+
* @param {Object} options - Caller options (cwd, localBin, toolCwd)
|
|
358
|
+
* @param {string} cwd - Base working directory (repo root)
|
|
359
|
+
* @returns {{ exec: { command: string, args: string[] }, cwd: string, fullCommand: string }}
|
|
360
|
+
*/
|
|
361
|
+
function _prepareToolCall(toolDef, rawArgs, options, cwd) {
|
|
362
|
+
let resolvedCommand, execArgs;
|
|
363
|
+
if (options.localBin) {
|
|
364
|
+
resolvedCommand = options.localBin;
|
|
365
|
+
// rawArgs[0] is the tool name (e.g. 'eslint') as passed to npx.
|
|
366
|
+
// Strip it since localBin IS the tool — the wrapper is not needed.
|
|
367
|
+
// Contract: toolDef.args() must always place the tool name first.
|
|
368
|
+
execArgs = rawArgs.slice(1);
|
|
369
|
+
logger.debug('tool-runner - _prepareToolCall', `Using local binary for ${toolDef.name}`, {
|
|
370
|
+
localBin: options.localBin,
|
|
371
|
+
toolCwd: options.toolCwd || null
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
resolvedCommand = _resolveCommand(toolDef.command);
|
|
375
|
+
execArgs = rawArgs;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const exec = _prepareExec(resolvedCommand, execArgs);
|
|
379
|
+
const effectiveCwd = options.toolCwd || cwd;
|
|
380
|
+
const fullCommand = [exec.command, ...exec.args].join(' ');
|
|
381
|
+
|
|
382
|
+
return { exec, cwd: effectiveCwd, fullCommand };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* When toolCwd is set, convert repo-relative file paths to absolute so they
|
|
387
|
+
* resolve correctly from the tool's config directory.
|
|
388
|
+
*
|
|
389
|
+
* @param {string[]} files - File paths (may be repo-relative)
|
|
390
|
+
* @param {string} cwd - Repo root (base for resolving relative paths)
|
|
391
|
+
* @param {string} [toolCwd] - Tool's config directory
|
|
392
|
+
* @returns {string[]} Adjusted file paths
|
|
393
|
+
*/
|
|
394
|
+
function _resolveFilePaths(files, cwd, toolCwd) {
|
|
395
|
+
if (!toolCwd) return files;
|
|
396
|
+
return files.map((f) => path.isAbsolute(f) ? f : path.resolve(cwd, f));
|
|
397
|
+
}
|
|
398
|
+
|
|
177
399
|
/**
|
|
178
400
|
* Execute a tool on files and parse output
|
|
179
401
|
*
|
|
@@ -182,13 +404,17 @@ export function filterFilesByTool(files, toolDef) {
|
|
|
182
404
|
* @param {Object} options - Execution options
|
|
183
405
|
* @param {number} [options.timeout] - Timeout in ms
|
|
184
406
|
* @param {string} [options.cwd] - Working directory
|
|
407
|
+
* @param {string} [options.localBin] - Local binary path (bypasses npx resolution)
|
|
408
|
+
* @param {string} [options.toolCwd] - Run tool from this directory (for plugin resolution)
|
|
409
|
+
* @param {Object} [options.env] - Extra environment variables (merged with process.env)
|
|
185
410
|
* @returns {ToolRunResult} Parsed result
|
|
186
411
|
*/
|
|
187
412
|
export function runTool(toolDef, files, options = {}) {
|
|
188
413
|
const { timeout = toolDef.timeout || 30000, cwd = process.cwd() } = options;
|
|
189
414
|
|
|
190
|
-
const
|
|
191
|
-
const
|
|
415
|
+
const effectiveFiles = _resolveFilePaths(files, cwd, options.toolCwd);
|
|
416
|
+
const rawArgs = toolDef.args(effectiveFiles);
|
|
417
|
+
const { exec, cwd: effectiveCwd, fullCommand } = _prepareToolCall(toolDef, rawArgs, options, cwd);
|
|
192
418
|
|
|
193
419
|
logger.debug('tool-runner - runTool', `Executing ${toolDef.name}`, {
|
|
194
420
|
command: fullCommand,
|
|
@@ -205,13 +431,23 @@ export function runTool(toolDef, files, options = {}) {
|
|
|
205
431
|
fixedFiles: []
|
|
206
432
|
};
|
|
207
433
|
|
|
434
|
+
// Build execFileSync options. When tool provides env overrides (e.g., MAVEN_OPTS
|
|
435
|
+
// for Spotless regex), merge them with process.env so PATH and other vars are preserved.
|
|
436
|
+
const execOptions = {
|
|
437
|
+
encoding: 'utf8',
|
|
438
|
+
timeout,
|
|
439
|
+
cwd: effectiveCwd,
|
|
440
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
441
|
+
};
|
|
442
|
+
if (options.env) {
|
|
443
|
+
execOptions.env = { ...process.env, ...options.env };
|
|
444
|
+
}
|
|
445
|
+
|
|
208
446
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
214
|
-
});
|
|
447
|
+
// execFileSync bypasses shell — args passed directly to process.
|
|
448
|
+
// Prevents shell interpretation of regex metacharacters (e.g., | in Spotless regex)
|
|
449
|
+
// and eliminates shell injection risk from file paths.
|
|
450
|
+
const stdout = execFileSync(exec.command, exec.args, execOptions);
|
|
215
451
|
|
|
216
452
|
// Exit code 0 = no issues; parse anyway for warnings
|
|
217
453
|
if (toolDef.parseOutput && stdout.trim()) {
|
|
@@ -234,13 +470,16 @@ export function runTool(toolDef, files, options = {}) {
|
|
|
234
470
|
message: `${toolDef.name} timed out after ${timeout / 1000}s`
|
|
235
471
|
});
|
|
236
472
|
} else {
|
|
237
|
-
// Genuine execution error
|
|
473
|
+
// Genuine execution error (ENOENT, EACCES, etc.)
|
|
238
474
|
const stderr = err.stderr ? err.stderr.toString().trim() : '';
|
|
239
475
|
logger.debug('tool-runner - runTool', `${toolDef.name} execution error`, {
|
|
240
476
|
exitCode: err.status,
|
|
477
|
+
code: err.code,
|
|
241
478
|
stderr
|
|
242
479
|
});
|
|
243
480
|
|
|
481
|
+
result.executionFailed = true;
|
|
482
|
+
|
|
244
483
|
// If no parseable output, treat as tool error
|
|
245
484
|
if (!err.stdout) {
|
|
246
485
|
result.errors.push({
|
|
@@ -269,6 +508,9 @@ export function runTool(toolDef, files, options = {}) {
|
|
|
269
508
|
* @param {number} [options.timeout] - Timeout in ms
|
|
270
509
|
* @param {string} [options.cwd] - Working directory
|
|
271
510
|
* @param {boolean} [options.restage] - Re-stage fixed files (default: true)
|
|
511
|
+
* @param {string} [options.localBin] - Local binary path (bypasses npx resolution)
|
|
512
|
+
* @param {string} [options.toolCwd] - Run tool from this directory (for plugin resolution)
|
|
513
|
+
* @param {Object} [options.env] - Extra environment variables (merged with process.env)
|
|
272
514
|
* @returns {{ fixedFiles: string[], fixedCount: number }} Fix results
|
|
273
515
|
*/
|
|
274
516
|
export function runToolFix(toolDef, files, options = {}) {
|
|
@@ -279,26 +521,37 @@ export function runToolFix(toolDef, files, options = {}) {
|
|
|
279
521
|
return { fixedFiles: [], fixedCount: 0 };
|
|
280
522
|
}
|
|
281
523
|
|
|
282
|
-
const
|
|
283
|
-
const
|
|
524
|
+
const effectiveFiles = _resolveFilePaths(files, cwd, options.toolCwd);
|
|
525
|
+
const rawArgs = toolDef.fixArgs(effectiveFiles);
|
|
526
|
+
const { exec, cwd: effectiveCwd, fullCommand } = _prepareToolCall(toolDef, rawArgs, options, cwd);
|
|
284
527
|
|
|
285
528
|
logger.debug('tool-runner - runToolFix', `Auto-fixing with ${toolDef.name}`, {
|
|
286
529
|
command: fullCommand,
|
|
287
530
|
fileCount: files.length
|
|
288
531
|
});
|
|
289
532
|
|
|
533
|
+
const execOptions = {
|
|
534
|
+
encoding: 'utf8',
|
|
535
|
+
timeout,
|
|
536
|
+
cwd: effectiveCwd,
|
|
537
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
538
|
+
};
|
|
539
|
+
if (options.env) {
|
|
540
|
+
execOptions.env = { ...process.env, ...options.env };
|
|
541
|
+
}
|
|
542
|
+
|
|
290
543
|
try {
|
|
291
|
-
|
|
292
|
-
encoding: 'utf8',
|
|
293
|
-
timeout,
|
|
294
|
-
cwd,
|
|
295
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
296
|
-
});
|
|
544
|
+
execFileSync(exec.command, exec.args, execOptions);
|
|
297
545
|
} catch (err) {
|
|
298
546
|
if (err.killed) {
|
|
299
547
|
logger.warning(`${toolDef.name} auto-fix timed out`);
|
|
300
548
|
return { fixedFiles: [], fixedCount: 0 };
|
|
301
549
|
}
|
|
550
|
+
// ENOENT = command binary not found (process never started, err.status is null)
|
|
551
|
+
if (err.code === 'ENOENT') {
|
|
552
|
+
logger.debug('tool-runner - runToolFix', `${toolDef.name} not found (ENOENT)`);
|
|
553
|
+
return { fixedFiles: [], fixedCount: 0 };
|
|
554
|
+
}
|
|
302
555
|
// Distinguish "command not found" from "partial fix exited non-zero"
|
|
303
556
|
// Exit code 1 with stderr containing "not recognized" or "not found" = command missing
|
|
304
557
|
const stderr = err.stderr ? err.stderr.toString() : '';
|
|
@@ -323,7 +576,7 @@ export function runToolFix(toolDef, files, options = {}) {
|
|
|
323
576
|
if (restage) {
|
|
324
577
|
for (const file of files) {
|
|
325
578
|
try {
|
|
326
|
-
|
|
579
|
+
execFileSync(_resolveCommand('git'), ['add', file], { cwd, stdio: 'pipe' });
|
|
327
580
|
fixedFiles.push(file);
|
|
328
581
|
} catch {
|
|
329
582
|
// File may not have changed; that's fine
|
|
@@ -348,6 +601,7 @@ export function runToolFix(toolDef, files, options = {}) {
|
|
|
348
601
|
* @param {number} [options.timeout] - Timeout in ms
|
|
349
602
|
* @param {string} [options.cwd] - Working directory
|
|
350
603
|
* @param {boolean} [options.restage] - Re-stage after fix (default: true)
|
|
604
|
+
* @param {Object} [options.env] - Extra environment variables (merged with process.env)
|
|
351
605
|
* @returns {ToolRunResult} Final result after optional fix
|
|
352
606
|
*/
|
|
353
607
|
export function runToolWithAutoFix(toolDef, files, options = {}) {
|
|
@@ -358,7 +612,7 @@ export function runToolWithAutoFix(toolDef, files, options = {}) {
|
|
|
358
612
|
|
|
359
613
|
// Step 2: If any issues (errors or warnings) and autoFix enabled, try to fix
|
|
360
614
|
const hasIssues = initialResult.errors.length > 0 || initialResult.warnings.length > 0;
|
|
361
|
-
if (autoFix && toolDef.fixArgs && hasIssues) {
|
|
615
|
+
if (autoFix && toolDef.fixArgs && hasIssues && !initialResult.executionFailed) {
|
|
362
616
|
logger.info(`🔧 Auto-fixing ${toolDef.name} issues...`);
|
|
363
617
|
|
|
364
618
|
const fixResult = runToolFix(toolDef, files, options);
|
package/package.json
CHANGED
|
@@ -1,69 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "claude-git-hooks",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "npm run test:all",
|
|
11
|
-
"test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
12
|
-
"test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
|
|
13
|
-
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
|
|
14
|
-
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
15
|
-
"test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
16
|
-
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
|
-
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
-
"lint": "eslint lib/ bin/claude-hooks",
|
|
20
|
-
"lint:fix": "eslint lib/ bin/claude-hooks --fix",
|
|
21
|
-
"format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
|
|
22
|
-
"precommit": "npm run lint && npm run test:smoke",
|
|
23
|
-
"prepublishOnly": "npm run test:all"
|
|
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
|
-
"jest": "^29.
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-git-hooks",
|
|
3
|
+
"version": "2.35.2",
|
|
4
|
+
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "npm run test:all",
|
|
11
|
+
"test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
|
|
12
|
+
"test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
|
|
13
|
+
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
|
|
14
|
+
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
15
|
+
"test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
|
|
16
|
+
"test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
|
|
17
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
18
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
+
"lint": "eslint lib/ bin/claude-hooks",
|
|
20
|
+
"lint:fix": "eslint lib/ bin/claude-hooks --fix",
|
|
21
|
+
"format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
|
|
22
|
+
"precommit": "npm run lint && npm run test:smoke",
|
|
23
|
+
"prepublishOnly": "npm run test:all",
|
|
24
|
+
"library:tokens": "node .library/tools/measure-tokens.js",
|
|
25
|
+
"library:graph": "node .library/tools/generate-graph.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"git",
|
|
29
|
+
"hooks",
|
|
30
|
+
"claude",
|
|
31
|
+
"ai",
|
|
32
|
+
"code-review",
|
|
33
|
+
"commit-messages",
|
|
34
|
+
"pre-commit",
|
|
35
|
+
"automation"
|
|
36
|
+
],
|
|
37
|
+
"author": "Pablo Rovito",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/mscope-S-L/git-hooks.git"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=16.9.0"
|
|
45
|
+
},
|
|
46
|
+
"engineStrict": false,
|
|
47
|
+
"os": [
|
|
48
|
+
"darwin",
|
|
49
|
+
"linux",
|
|
50
|
+
"win32"
|
|
51
|
+
],
|
|
52
|
+
"preferGlobal": true,
|
|
53
|
+
"files": [
|
|
54
|
+
"bin/",
|
|
55
|
+
"lib/",
|
|
56
|
+
"templates/",
|
|
57
|
+
"README.md",
|
|
58
|
+
"CHANGELOG.md",
|
|
59
|
+
"CLAUDE.md",
|
|
60
|
+
"LICENSE"
|
|
61
|
+
],
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@octokit/rest": "^21.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/jest": "^29.5.0",
|
|
67
|
+
"eslint": "^8.57.1",
|
|
68
|
+
"jest": "^29.7.0",
|
|
69
|
+
"js-tiktoken": "^1.0.18",
|
|
70
|
+
"madge": "^8.0.0",
|
|
71
|
+
"prettier": "^3.2.0"
|
|
72
|
+
}
|
|
73
|
+
}
|