xtrm-tools 2.4.1 → 2.4.3
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/README.md +15 -6
- package/cli/dist/index.cjs +738 -239
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +10 -0
- package/config/pi/extensions/core/adapter.ts +2 -14
- package/config/pi/extensions/core/guard-rules.ts +70 -0
- package/config/pi/extensions/core/session-state.ts +59 -0
- package/config/pi/extensions/main-guard.ts +10 -14
- package/config/pi/extensions/plan-mode/README.md +65 -0
- package/config/pi/extensions/plan-mode/index.ts +340 -0
- package/config/pi/extensions/plan-mode/utils.ts +168 -0
- package/config/pi/extensions/service-skills.ts +51 -7
- package/config/pi/extensions/session-flow.ts +117 -0
- package/hooks/beads-claim-sync.mjs +140 -14
- package/hooks/beads-compact-restore.mjs +41 -9
- package/hooks/beads-compact-save.mjs +36 -5
- package/hooks/beads-gate-messages.mjs +27 -1
- package/hooks/beads-memory-gate.mjs +24 -16
- package/hooks/beads-stop-gate.mjs +58 -8
- package/hooks/guard-rules.mjs +117 -0
- package/hooks/hooks.json +28 -18
- package/hooks/main-guard.mjs +22 -22
- package/hooks/quality-check.cjs +1286 -0
- package/hooks/quality-check.py +345 -0
- package/hooks/session-state.mjs +138 -0
- package/package.json +2 -1
- package/project-skills/quality-gates/.claude/settings.json +1 -24
- package/skills/creating-service-skills/SKILL.md +433 -0
- package/skills/creating-service-skills/references/script_quality_standards.md +425 -0
- package/skills/creating-service-skills/references/service_skill_system_guide.md +278 -0
- package/skills/creating-service-skills/scripts/bootstrap.py +326 -0
- package/skills/creating-service-skills/scripts/deep_dive.py +304 -0
- package/skills/creating-service-skills/scripts/scaffolder.py +482 -0
- package/skills/scoping-service-skills/SKILL.md +231 -0
- package/skills/scoping-service-skills/scripts/scope.py +74 -0
- package/skills/sync-docs/SKILL.md +235 -0
- package/skills/sync-docs/evals/evals.json +89 -0
- package/skills/sync-docs/references/doc-structure.md +104 -0
- package/skills/sync-docs/references/schema.md +103 -0
- package/skills/sync-docs/scripts/context_gatherer.py +246 -0
- package/skills/sync-docs/scripts/doc_structure_analyzer.py +495 -0
- package/skills/sync-docs/scripts/validate_doc.py +365 -0
- package/skills/sync-docs-workspace/iteration-1/benchmark.json +293 -0
- package/skills/sync-docs-workspace/iteration-1/benchmark.md +13 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/outputs/result.md +210 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/outputs/result.md +101 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/outputs/result.md +198 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/outputs/result.md +94 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/outputs/result.md +237 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/outputs/result.md +134 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/grading.json +28 -0
- package/skills/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/benchmark.json +297 -0
- package/skills/sync-docs-workspace/iteration-2/benchmark.md +13 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/outputs/result.md +137 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/grading.json +92 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/outputs/result.md +134 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/grading.json +86 -0
- package/skills/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/outputs/result.md +193 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/grading.json +72 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/outputs/result.md +211 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/grading.json +91 -0
- package/skills/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/outputs/result.md +182 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/outputs/result.md +222 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/grading.json +88 -0
- package/skills/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/benchmark.json +298 -0
- package/skills/sync-docs-workspace/iteration-3/benchmark.md +13 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/outputs/result.md +125 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/grading.json +97 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/outputs/result.md +144 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/grading.json +78 -0
- package/skills/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/outputs/result.md +104 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/grading.json +91 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/outputs/result.md +79 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/grading.json +82 -0
- package/skills/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/eval_metadata.json +27 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase1_context.json +302 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase2_drift.txt +33 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase3_analysis.json +114 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase4_fix.txt +118 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase5_validate.txt +38 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/result.md +158 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/timing.json +5 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/outputs/result.md +71 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/grading.json +90 -0
- package/skills/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
- package/skills/updating-service-skills/SKILL.md +136 -0
- package/skills/updating-service-skills/scripts/drift_detector.py +222 -0
- package/skills/using-quality-gates/SKILL.md +254 -0
- package/skills/using-service-skills/SKILL.md +108 -0
- package/skills/using-service-skills/scripts/cataloger.py +74 -0
- package/skills/using-service-skills/scripts/skill_activator.py +152 -0
- package/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
- package/skills/using-xtrm/SKILL.md +34 -38
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Node.js Quality Check Hook
|
|
4
|
+
* Optimized for Node.js TypeScript projects with sensible defaults
|
|
5
|
+
*
|
|
6
|
+
* EXIT CODES:
|
|
7
|
+
* 0 - Success (all checks passed)
|
|
8
|
+
* 1 - General error (missing dependencies, etc.)
|
|
9
|
+
* 2 - Quality issues found - ALL must be fixed (blocking)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get project root using CLAUDE_PROJECT_DIR environment variable
|
|
18
|
+
* @returns {string} Project root directory
|
|
19
|
+
*/
|
|
20
|
+
function getProjectRoot() {
|
|
21
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const projectRoot = getProjectRoot();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Intelligent TypeScript Config Cache with checksum validation
|
|
28
|
+
* Handles multiple tsconfig files and maps files to appropriate configs
|
|
29
|
+
*/
|
|
30
|
+
class TypeScriptConfigCache {
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new TypeScript config cache instance.
|
|
33
|
+
* Loads existing cache or initializes empty cache.
|
|
34
|
+
*/
|
|
35
|
+
constructor() {
|
|
36
|
+
// Store cache in the hook's directory for isolation
|
|
37
|
+
this.cacheFile = path.join(__dirname, 'tsconfig-cache.json');
|
|
38
|
+
this.cache = { hashes: {}, mappings: {} };
|
|
39
|
+
this.loadCache();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get config hash for cache validation
|
|
44
|
+
* @param {string} configPath - Path to tsconfig file
|
|
45
|
+
* @returns {string} SHA256 hash of config content
|
|
46
|
+
*/
|
|
47
|
+
getConfigHash(configPath) {
|
|
48
|
+
try {
|
|
49
|
+
const content = require('fs').readFileSync(configPath, 'utf8');
|
|
50
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find all tsconfig files in project
|
|
58
|
+
* @returns {string[]} Array of tsconfig file paths
|
|
59
|
+
*/
|
|
60
|
+
findTsConfigFiles() {
|
|
61
|
+
const configs = [];
|
|
62
|
+
try {
|
|
63
|
+
// Try to use glob if available, fallback to manual search
|
|
64
|
+
const globSync = require('glob').sync;
|
|
65
|
+
return globSync('tsconfig*.json', { cwd: projectRoot }).map((file) =>
|
|
66
|
+
path.join(projectRoot, file),
|
|
67
|
+
);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Fallback: manually check common config files
|
|
70
|
+
const commonConfigs = [
|
|
71
|
+
'tsconfig.json',
|
|
72
|
+
'tsconfig.webview.json',
|
|
73
|
+
'tsconfig.test.json',
|
|
74
|
+
'tsconfig.node.json',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const config of commonConfigs) {
|
|
78
|
+
const configPath = path.join(projectRoot, config);
|
|
79
|
+
if (require('fs').existsSync(configPath)) {
|
|
80
|
+
configs.push(configPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return configs;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if cache is valid by comparing config hashes
|
|
89
|
+
* @returns {boolean} True if cache is valid
|
|
90
|
+
*/
|
|
91
|
+
isValid() {
|
|
92
|
+
const configFiles = this.findTsConfigFiles();
|
|
93
|
+
|
|
94
|
+
// Check if we have the same number of configs
|
|
95
|
+
if (Object.keys(this.cache.hashes).length !== configFiles.length) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check each config hash
|
|
100
|
+
for (const configPath of configFiles) {
|
|
101
|
+
const currentHash = this.getConfigHash(configPath);
|
|
102
|
+
if (currentHash !== this.cache.hashes[configPath]) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rebuild cache by parsing all configs and creating file mappings
|
|
112
|
+
*/
|
|
113
|
+
rebuild() {
|
|
114
|
+
this.cache = { hashes: {}, mappings: {} };
|
|
115
|
+
|
|
116
|
+
// Process configs in priority order (most specific first)
|
|
117
|
+
const configPriority = [
|
|
118
|
+
'tsconfig.webview.json', // Most specific
|
|
119
|
+
'tsconfig.test.json', // Test-specific
|
|
120
|
+
'tsconfig.json', // Base config
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
configPriority.forEach((configName) => {
|
|
124
|
+
const configPath = path.join(projectRoot, configName);
|
|
125
|
+
if (!require('fs').existsSync(configPath)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Store hash for validation
|
|
130
|
+
this.cache.hashes[configPath] = this.getConfigHash(configPath);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const configContent = require('fs').readFileSync(configPath, 'utf8');
|
|
134
|
+
const config = JSON.parse(configContent);
|
|
135
|
+
|
|
136
|
+
// Build file pattern mappings
|
|
137
|
+
if (config.include) {
|
|
138
|
+
config.include.forEach((pattern) => {
|
|
139
|
+
// Only set if not already mapped by a more specific config
|
|
140
|
+
if (!this.cache.mappings[pattern]) {
|
|
141
|
+
this.cache.mappings[pattern] = {
|
|
142
|
+
configPath,
|
|
143
|
+
excludes: config.exclude || [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
// Skip invalid configs
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.saveCache();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load cache from disk
|
|
158
|
+
*/
|
|
159
|
+
loadCache() {
|
|
160
|
+
try {
|
|
161
|
+
const cacheContent = require('fs').readFileSync(this.cacheFile, 'utf8');
|
|
162
|
+
this.cache = JSON.parse(cacheContent);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// Cache doesn't exist or is invalid, will rebuild
|
|
165
|
+
this.cache = { hashes: {}, mappings: {} };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Save cache to disk
|
|
171
|
+
*/
|
|
172
|
+
saveCache() {
|
|
173
|
+
try {
|
|
174
|
+
// Save cache directly in hook directory (directory already exists)
|
|
175
|
+
require('fs').writeFileSync(this.cacheFile, JSON.stringify(this.cache, null, 2));
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Ignore cache save errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get appropriate tsconfig for a file
|
|
183
|
+
* @param {string} filePath - File path to check
|
|
184
|
+
* @returns {string} Path to appropriate tsconfig file
|
|
185
|
+
*/
|
|
186
|
+
getTsConfigForFile(filePath) {
|
|
187
|
+
// Ensure cache is valid
|
|
188
|
+
if (!this.isValid()) {
|
|
189
|
+
this.rebuild();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
193
|
+
|
|
194
|
+
// Check cached mappings first - these are from actual tsconfig includes
|
|
195
|
+
// Sort patterns by specificity to match most specific first
|
|
196
|
+
const sortedMappings = Object.entries(this.cache.mappings).sort(([a], [b]) => {
|
|
197
|
+
// More specific patterns first
|
|
198
|
+
const aSpecificity = a.split('/').length + (a.includes('**') ? 0 : 10);
|
|
199
|
+
const bSpecificity = b.split('/').length + (b.includes('**') ? 0 : 10);
|
|
200
|
+
return bSpecificity - aSpecificity;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
for (const [pattern, mapping] of sortedMappings) {
|
|
204
|
+
// Handle both old format (string) and new format (object with excludes)
|
|
205
|
+
const configPath = typeof mapping === 'string' ? mapping : mapping.configPath;
|
|
206
|
+
const excludes = typeof mapping === 'string' ? [] : mapping.excludes;
|
|
207
|
+
|
|
208
|
+
if (this.matchesPattern(relativePath, pattern)) {
|
|
209
|
+
// Check if file is excluded
|
|
210
|
+
let isExcluded = false;
|
|
211
|
+
for (const exclude of excludes) {
|
|
212
|
+
if (this.matchesPattern(relativePath, exclude)) {
|
|
213
|
+
isExcluded = true;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!isExcluded) {
|
|
219
|
+
return configPath;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fast heuristics for common cases not in cache
|
|
225
|
+
// Webview files
|
|
226
|
+
if (relativePath.includes('src/webview/') || relativePath.includes('/webview/')) {
|
|
227
|
+
const webviewConfig = path.join(projectRoot, 'tsconfig.webview.json');
|
|
228
|
+
if (require('fs').existsSync(webviewConfig)) {
|
|
229
|
+
return webviewConfig;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Test files
|
|
234
|
+
if (
|
|
235
|
+
relativePath.includes('/test/') ||
|
|
236
|
+
relativePath.includes('.test.') ||
|
|
237
|
+
relativePath.includes('.spec.')
|
|
238
|
+
) {
|
|
239
|
+
const testConfig = path.join(projectRoot, 'tsconfig.test.json');
|
|
240
|
+
if (require('fs').existsSync(testConfig)) {
|
|
241
|
+
return testConfig;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Default fallback
|
|
246
|
+
return path.join(projectRoot, 'tsconfig.json');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Simple pattern matching for file paths
|
|
251
|
+
* @param {string} filePath - File path to test
|
|
252
|
+
* @param {string} pattern - Glob-like pattern
|
|
253
|
+
* @returns {boolean} True if file matches pattern
|
|
254
|
+
*/
|
|
255
|
+
matchesPattern(filePath, pattern) {
|
|
256
|
+
// Simple pattern matching - convert glob to regex
|
|
257
|
+
// Handle the common patterns specially
|
|
258
|
+
if (pattern.endsWith('/**/*')) {
|
|
259
|
+
// For patterns like src/webview/**/* or src/protocol/**/*
|
|
260
|
+
// Match any file under that directory
|
|
261
|
+
const baseDir = pattern.slice(0, -5); // Remove /**/*
|
|
262
|
+
return filePath.startsWith(baseDir);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// For other patterns, use regex conversion
|
|
266
|
+
let regexPattern = pattern
|
|
267
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
268
|
+
.replace(/\*\*/g, '🌟') // Temporary placeholder for **
|
|
269
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
270
|
+
.replace(/🌟/g, '.*') // ** matches anything including /
|
|
271
|
+
.replace(/\?/g, '.'); // ? matches single character
|
|
272
|
+
|
|
273
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
274
|
+
const result = regex.test(filePath);
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Global config cache instance
|
|
281
|
+
const tsConfigCache = new TypeScriptConfigCache();
|
|
282
|
+
|
|
283
|
+
// ANSI color codes
|
|
284
|
+
const colors = {
|
|
285
|
+
red: '\x1b[0;31m',
|
|
286
|
+
green: '\x1b[0;32m',
|
|
287
|
+
yellow: '\x1b[0;33m',
|
|
288
|
+
blue: '\x1b[0;34m',
|
|
289
|
+
cyan: '\x1b[0;36m',
|
|
290
|
+
reset: '\x1b[0m',
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Load configuration from JSON file with environment variable overrides
|
|
295
|
+
* @returns {Object} Configuration object
|
|
296
|
+
*/
|
|
297
|
+
function loadConfig() {
|
|
298
|
+
let fileConfig = {};
|
|
299
|
+
|
|
300
|
+
// Try to load hook-config.json
|
|
301
|
+
try {
|
|
302
|
+
const configPath = path.join(__dirname, 'hook-config.json');
|
|
303
|
+
if (require('fs').existsSync(configPath)) {
|
|
304
|
+
fileConfig = JSON.parse(require('fs').readFileSync(configPath, 'utf8'));
|
|
305
|
+
}
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// Config file not found or invalid, use defaults
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Build config with file settings as base, env vars as overrides
|
|
311
|
+
return {
|
|
312
|
+
// TypeScript settings
|
|
313
|
+
typescriptEnabled:
|
|
314
|
+
process.env.CLAUDE_HOOKS_TYPESCRIPT_ENABLED !== undefined
|
|
315
|
+
? process.env.CLAUDE_HOOKS_TYPESCRIPT_ENABLED !== 'false'
|
|
316
|
+
: (fileConfig.typescript?.enabled ?? true),
|
|
317
|
+
|
|
318
|
+
showDependencyErrors:
|
|
319
|
+
process.env.CLAUDE_HOOKS_SHOW_DEPENDENCY_ERRORS !== undefined
|
|
320
|
+
? process.env.CLAUDE_HOOKS_SHOW_DEPENDENCY_ERRORS === 'true'
|
|
321
|
+
: (fileConfig.typescript?.showDependencyErrors ?? false),
|
|
322
|
+
|
|
323
|
+
// ESLint settings
|
|
324
|
+
eslintEnabled:
|
|
325
|
+
process.env.CLAUDE_HOOKS_ESLINT_ENABLED !== undefined
|
|
326
|
+
? process.env.CLAUDE_HOOKS_ESLINT_ENABLED !== 'false'
|
|
327
|
+
: (fileConfig.eslint?.enabled ?? true),
|
|
328
|
+
|
|
329
|
+
eslintAutofix:
|
|
330
|
+
process.env.CLAUDE_HOOKS_ESLINT_AUTOFIX !== undefined
|
|
331
|
+
? process.env.CLAUDE_HOOKS_ESLINT_AUTOFIX === 'true'
|
|
332
|
+
: (fileConfig.eslint?.autofix ?? false),
|
|
333
|
+
|
|
334
|
+
// Prettier settings
|
|
335
|
+
prettierEnabled:
|
|
336
|
+
process.env.CLAUDE_HOOKS_PRETTIER_ENABLED !== undefined
|
|
337
|
+
? process.env.CLAUDE_HOOKS_PRETTIER_ENABLED !== 'false'
|
|
338
|
+
: (fileConfig.prettier?.enabled ?? true),
|
|
339
|
+
|
|
340
|
+
prettierAutofix:
|
|
341
|
+
process.env.CLAUDE_HOOKS_PRETTIER_AUTOFIX !== undefined
|
|
342
|
+
? process.env.CLAUDE_HOOKS_PRETTIER_AUTOFIX === 'true'
|
|
343
|
+
: (fileConfig.prettier?.autofix ?? false),
|
|
344
|
+
|
|
345
|
+
// General settings
|
|
346
|
+
autofixSilent:
|
|
347
|
+
process.env.CLAUDE_HOOKS_AUTOFIX_SILENT !== undefined
|
|
348
|
+
? process.env.CLAUDE_HOOKS_AUTOFIX_SILENT === 'true'
|
|
349
|
+
: (fileConfig.general?.autofixSilent ?? false),
|
|
350
|
+
|
|
351
|
+
debug:
|
|
352
|
+
process.env.CLAUDE_HOOKS_DEBUG !== undefined
|
|
353
|
+
? process.env.CLAUDE_HOOKS_DEBUG === 'true'
|
|
354
|
+
: (fileConfig.general?.debug ?? false),
|
|
355
|
+
|
|
356
|
+
// Ignore patterns
|
|
357
|
+
ignorePatterns: fileConfig.ignore?.patterns || [],
|
|
358
|
+
|
|
359
|
+
// Store the full config for rule access
|
|
360
|
+
_fileConfig: fileConfig,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Hook Configuration
|
|
366
|
+
*
|
|
367
|
+
* Configuration is loaded from (in order of precedence):
|
|
368
|
+
* 1. Environment variables (highest priority)
|
|
369
|
+
* 2. .claude/hooks/config.json file
|
|
370
|
+
* 3. Built-in defaults
|
|
371
|
+
*/
|
|
372
|
+
const config = loadConfig();
|
|
373
|
+
|
|
374
|
+
// Logging functions - define before using
|
|
375
|
+
const log = {
|
|
376
|
+
info: (msg) => console.error(`${colors.blue}[INFO]${colors.reset} ${msg}`),
|
|
377
|
+
error: (msg) => console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`),
|
|
378
|
+
success: (msg) => console.error(`${colors.green}[OK]${colors.reset} ${msg}`),
|
|
379
|
+
warning: (msg) => console.error(`${colors.yellow}[WARN]${colors.reset} ${msg}`),
|
|
380
|
+
debug: (msg) => {
|
|
381
|
+
if (config.debug) {
|
|
382
|
+
console.error(`${colors.cyan}[DEBUG]${colors.reset} ${msg}`);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Note: errors and autofixes are tracked per QualityChecker instance
|
|
388
|
+
|
|
389
|
+
// Try to load modules, but make them optional
|
|
390
|
+
let ESLint, prettier, ts;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
({ ESLint } = require(path.join(projectRoot, 'node_modules', 'eslint')));
|
|
394
|
+
} catch (e) {
|
|
395
|
+
log.debug('ESLint not found in project - will skip ESLint checks');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
prettier = require(path.join(projectRoot, 'node_modules', 'prettier'));
|
|
400
|
+
} catch (e) {
|
|
401
|
+
log.debug('Prettier not found in project - will skip Prettier checks');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
ts = require(path.join(projectRoot, 'node_modules', 'typescript'));
|
|
406
|
+
} catch (e) {
|
|
407
|
+
log.debug('TypeScript not found in project - will skip TypeScript checks');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Quality checker for a single file.
|
|
412
|
+
* Runs TypeScript, ESLint, and Prettier checks with optional auto-fixing.
|
|
413
|
+
*/
|
|
414
|
+
class QualityChecker {
|
|
415
|
+
/**
|
|
416
|
+
* Creates a new QualityChecker instance.
|
|
417
|
+
* @param {string} filePath - Path to file to check
|
|
418
|
+
*/
|
|
419
|
+
constructor(filePath) {
|
|
420
|
+
this.filePath = filePath;
|
|
421
|
+
this.fileType = this.detectFileType(filePath);
|
|
422
|
+
this.errors = [];
|
|
423
|
+
this.autofixes = [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Detect file type from path
|
|
428
|
+
* @param {string} filePath - File path
|
|
429
|
+
* @returns {string} File type
|
|
430
|
+
*/
|
|
431
|
+
detectFileType(filePath) {
|
|
432
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
|
433
|
+
return 'test';
|
|
434
|
+
}
|
|
435
|
+
// MCP transport files
|
|
436
|
+
if (/\/(client|server)\/(stdio|sse|websocket|http)/.test(filePath)) {
|
|
437
|
+
return 'transport';
|
|
438
|
+
}
|
|
439
|
+
// CLI entry points
|
|
440
|
+
if (/\/cli\/|\/bin\/|index\.(ts|js)$/.test(filePath)) {
|
|
441
|
+
return 'cli';
|
|
442
|
+
}
|
|
443
|
+
// Services
|
|
444
|
+
if (/\/services\//.test(filePath)) {
|
|
445
|
+
return 'service';
|
|
446
|
+
}
|
|
447
|
+
if (/\.(ts|tsx)$/.test(filePath)) {
|
|
448
|
+
return 'typescript';
|
|
449
|
+
}
|
|
450
|
+
if (/\.(js|jsx)$/.test(filePath)) {
|
|
451
|
+
return 'javascript';
|
|
452
|
+
}
|
|
453
|
+
return 'unknown';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Run all quality checks
|
|
458
|
+
* @returns {Promise<{errors: string[], autofixes: string[]}>} Check results
|
|
459
|
+
*/
|
|
460
|
+
async checkAll() {
|
|
461
|
+
// This should never happen now since we filter out non-source files earlier,
|
|
462
|
+
// but keeping for consistency with shell version
|
|
463
|
+
if (this.fileType === 'unknown') {
|
|
464
|
+
log.info('Unknown file type, skipping detailed checks');
|
|
465
|
+
return { errors: [], autofixes: [] };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Run all checks in parallel for speed
|
|
469
|
+
const checkPromises = [];
|
|
470
|
+
|
|
471
|
+
if (config.typescriptEnabled) {
|
|
472
|
+
checkPromises.push(this.checkTypeScript());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (config.eslintEnabled) {
|
|
476
|
+
checkPromises.push(this.checkESLint());
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (config.prettierEnabled) {
|
|
480
|
+
checkPromises.push(this.checkPrettier());
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
checkPromises.push(this.checkCommonIssues());
|
|
484
|
+
checkPromises.push(this.checkNodePatterns());
|
|
485
|
+
|
|
486
|
+
await Promise.all(checkPromises);
|
|
487
|
+
|
|
488
|
+
// Check for related tests (not critical, so separate)
|
|
489
|
+
await this.suggestRelatedTests();
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
errors: this.errors,
|
|
493
|
+
autofixes: this.autofixes,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get file dependencies by parsing imports
|
|
499
|
+
* @param {string} filePath - File to analyze
|
|
500
|
+
* @returns {string[]} Array of file paths including dependencies
|
|
501
|
+
*/
|
|
502
|
+
getFileDependencies(filePath) {
|
|
503
|
+
const dependencies = new Set([filePath]);
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
507
|
+
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
|
|
508
|
+
let match;
|
|
509
|
+
|
|
510
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
511
|
+
const importPath = match[1];
|
|
512
|
+
|
|
513
|
+
// Only include relative imports (project files)
|
|
514
|
+
if (importPath.startsWith('.')) {
|
|
515
|
+
const resolvedPath = this.resolveImportPath(filePath, importPath);
|
|
516
|
+
if (resolvedPath && require('fs').existsSync(resolvedPath)) {
|
|
517
|
+
dependencies.add(resolvedPath);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (e) {
|
|
522
|
+
// If we can't parse imports, just use the original file
|
|
523
|
+
log.debug(`Could not parse imports for ${filePath}: ${e.message}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return Array.from(dependencies);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Resolve relative import path to absolute path
|
|
531
|
+
* @param {string} fromFile - File doing the import
|
|
532
|
+
* @param {string} importPath - Relative import path
|
|
533
|
+
* @returns {string|null} Absolute file path or null if not found
|
|
534
|
+
*/
|
|
535
|
+
resolveImportPath(fromFile, importPath) {
|
|
536
|
+
const dir = path.dirname(fromFile);
|
|
537
|
+
const resolved = path.resolve(dir, importPath);
|
|
538
|
+
|
|
539
|
+
// Try common extensions
|
|
540
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
541
|
+
for (const ext of extensions) {
|
|
542
|
+
const fullPath = resolved + ext;
|
|
543
|
+
if (require('fs').existsSync(fullPath)) {
|
|
544
|
+
return fullPath;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Try index files
|
|
549
|
+
for (const ext of extensions) {
|
|
550
|
+
const indexPath = path.join(resolved, 'index' + ext);
|
|
551
|
+
if (require('fs').existsSync(indexPath)) {
|
|
552
|
+
return indexPath;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Check TypeScript compilation
|
|
561
|
+
* @returns {Promise<void>}
|
|
562
|
+
*/
|
|
563
|
+
async checkTypeScript() {
|
|
564
|
+
if (!config.typescriptEnabled || !ts) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Skip TypeScript checking for JavaScript files in hook directories
|
|
569
|
+
if (this.filePath.endsWith('.js') && this.filePath.includes('.claude/hooks/')) {
|
|
570
|
+
log.debug('Skipping TypeScript check for JavaScript hook file');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
log.info('Running TypeScript compilation check...');
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
// Get intelligent config for this file
|
|
578
|
+
const configPath = tsConfigCache.getTsConfigForFile(this.filePath);
|
|
579
|
+
|
|
580
|
+
if (!require('fs').existsSync(configPath)) {
|
|
581
|
+
log.debug(`No TypeScript config found: ${configPath}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
log.debug(
|
|
586
|
+
`Using TypeScript config: ${path.basename(configPath)} for ${path.relative(projectRoot, this.filePath)}`,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
590
|
+
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
591
|
+
configFile.config,
|
|
592
|
+
ts.sys,
|
|
593
|
+
path.dirname(configPath),
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// Only check the edited file, not its dependencies
|
|
597
|
+
// Dependencies will be type-checked with their own appropriate configs
|
|
598
|
+
log.debug(`TypeScript checking edited file only`);
|
|
599
|
+
|
|
600
|
+
// Create program with just the edited file
|
|
601
|
+
const program = ts.createProgram([this.filePath], parsedConfig.options);
|
|
602
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
603
|
+
|
|
604
|
+
// Group diagnostics by file
|
|
605
|
+
const diagnosticsByFile = new Map();
|
|
606
|
+
diagnostics.forEach((d) => {
|
|
607
|
+
if (d.file) {
|
|
608
|
+
const fileName = d.file.fileName;
|
|
609
|
+
if (!diagnosticsByFile.has(fileName)) {
|
|
610
|
+
diagnosticsByFile.set(fileName, []);
|
|
611
|
+
}
|
|
612
|
+
diagnosticsByFile.get(fileName).push(d);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Report edited file first
|
|
617
|
+
const editedFileDiagnostics = diagnosticsByFile.get(this.filePath) || [];
|
|
618
|
+
if (editedFileDiagnostics.length > 0) {
|
|
619
|
+
this.errors.push(`TypeScript errors in edited file (using ${path.basename(configPath)})`);
|
|
620
|
+
editedFileDiagnostics.forEach((diagnostic) => {
|
|
621
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
622
|
+
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
|
|
623
|
+
diagnostic.start,
|
|
624
|
+
);
|
|
625
|
+
console.error(
|
|
626
|
+
` ❌ ${diagnostic.file.fileName}:${line + 1}:${character + 1} - ${message}`,
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Report dependencies separately (as warnings, not errors) - only if enabled
|
|
632
|
+
if (config.showDependencyErrors) {
|
|
633
|
+
let hasDepErrors = false;
|
|
634
|
+
diagnosticsByFile.forEach((diags, fileName) => {
|
|
635
|
+
if (fileName !== this.filePath) {
|
|
636
|
+
if (!hasDepErrors) {
|
|
637
|
+
console.error('\n[DEPENDENCY ERRORS] Files imported by your edited file:');
|
|
638
|
+
hasDepErrors = true;
|
|
639
|
+
}
|
|
640
|
+
console.error(` ⚠️ ${fileName}:`);
|
|
641
|
+
diags.forEach((diagnostic) => {
|
|
642
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
643
|
+
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
|
|
644
|
+
diagnostic.start,
|
|
645
|
+
);
|
|
646
|
+
console.error(` Line ${line + 1}:${character + 1} - ${message}`);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (diagnostics.length === 0) {
|
|
653
|
+
log.success('TypeScript compilation passed');
|
|
654
|
+
}
|
|
655
|
+
} catch (error) {
|
|
656
|
+
log.debug(`TypeScript check error: ${error.message}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Check ESLint rules
|
|
662
|
+
* @returns {Promise<void>}
|
|
663
|
+
*/
|
|
664
|
+
async checkESLint() {
|
|
665
|
+
if (!config.eslintEnabled || !ESLint) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
log.info('Running ESLint...');
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
const eslint = new ESLint({
|
|
673
|
+
fix: config.eslintAutofix,
|
|
674
|
+
cwd: projectRoot,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const results = await eslint.lintFiles([this.filePath]);
|
|
678
|
+
const result = results[0];
|
|
679
|
+
|
|
680
|
+
if (result.errorCount > 0 || result.warningCount > 0) {
|
|
681
|
+
if (config.eslintAutofix) {
|
|
682
|
+
log.warning('ESLint issues found, attempting auto-fix...');
|
|
683
|
+
|
|
684
|
+
// Write the fixed output
|
|
685
|
+
if (result.output) {
|
|
686
|
+
await fs.writeFile(this.filePath, result.output);
|
|
687
|
+
|
|
688
|
+
// Re-lint to see if issues remain
|
|
689
|
+
const resultsAfterFix = await eslint.lintFiles([this.filePath]);
|
|
690
|
+
const resultAfterFix = resultsAfterFix[0];
|
|
691
|
+
|
|
692
|
+
if (resultAfterFix.errorCount === 0 && resultAfterFix.warningCount === 0) {
|
|
693
|
+
log.success('ESLint auto-fixed all issues!');
|
|
694
|
+
if (config.autofixSilent) {
|
|
695
|
+
this.autofixes.push('ESLint auto-fixed formatting/style issues');
|
|
696
|
+
} else {
|
|
697
|
+
this.errors.push('ESLint issues were auto-fixed - verify the changes');
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
this.errors.push(
|
|
701
|
+
`ESLint found issues that couldn't be auto-fixed in ${this.filePath}`,
|
|
702
|
+
);
|
|
703
|
+
const formatter = await eslint.loadFormatter('stylish');
|
|
704
|
+
const output = formatter.format(resultsAfterFix);
|
|
705
|
+
console.error(output);
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
this.errors.push(`ESLint found issues in ${this.filePath}`);
|
|
709
|
+
const formatter = await eslint.loadFormatter('stylish');
|
|
710
|
+
const output = formatter.format(results);
|
|
711
|
+
console.error(output);
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
this.errors.push(`ESLint found issues in ${this.filePath}`);
|
|
715
|
+
const formatter = await eslint.loadFormatter('stylish');
|
|
716
|
+
const output = formatter.format(results);
|
|
717
|
+
console.error(output);
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
log.success('ESLint passed');
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
log.debug(`ESLint check error: ${error.message}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Check Prettier formatting
|
|
729
|
+
* @returns {Promise<void>}
|
|
730
|
+
*/
|
|
731
|
+
async checkPrettier() {
|
|
732
|
+
if (!config.prettierEnabled || !prettier) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
log.info('Running Prettier check...');
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const fileContent = await fs.readFile(this.filePath, 'utf8');
|
|
740
|
+
const prettierConfig = await prettier.resolveConfig(this.filePath);
|
|
741
|
+
|
|
742
|
+
const isFormatted = await prettier.check(fileContent, {
|
|
743
|
+
...prettierConfig,
|
|
744
|
+
filepath: this.filePath,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (!isFormatted) {
|
|
748
|
+
if (config.prettierAutofix) {
|
|
749
|
+
log.warning('Prettier formatting issues found, auto-fixing...');
|
|
750
|
+
|
|
751
|
+
const formatted = await prettier.format(fileContent, {
|
|
752
|
+
...prettierConfig,
|
|
753
|
+
filepath: this.filePath,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
await fs.writeFile(this.filePath, formatted);
|
|
757
|
+
log.success('Prettier auto-formatted the file!');
|
|
758
|
+
|
|
759
|
+
if (config.autofixSilent) {
|
|
760
|
+
this.autofixes.push('Prettier auto-formatted the file');
|
|
761
|
+
} else {
|
|
762
|
+
this.errors.push('Prettier formatting was auto-fixed - verify the changes');
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
this.errors.push(`Prettier formatting issues in ${this.filePath}`);
|
|
766
|
+
console.error('Run prettier --write to fix');
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
log.success('Prettier formatting correct');
|
|
770
|
+
}
|
|
771
|
+
} catch (error) {
|
|
772
|
+
log.debug(`Prettier check error: ${error.message}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Check for common code issues
|
|
778
|
+
* @returns {Promise<void>}
|
|
779
|
+
*/
|
|
780
|
+
async checkCommonIssues() {
|
|
781
|
+
log.info('Checking for common issues...');
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
const content = await fs.readFile(this.filePath, 'utf8');
|
|
785
|
+
const lines = content.split('\n');
|
|
786
|
+
let foundIssues = false;
|
|
787
|
+
|
|
788
|
+
// Check for 'as any' in TypeScript files
|
|
789
|
+
const asAnyRule = config._fileConfig.rules?.asAny || {};
|
|
790
|
+
if (
|
|
791
|
+
(this.fileType === 'typescript' || this.fileType === 'component') &&
|
|
792
|
+
asAnyRule.enabled !== false
|
|
793
|
+
) {
|
|
794
|
+
lines.forEach((line, index) => {
|
|
795
|
+
if (line.includes('as any')) {
|
|
796
|
+
const severity = asAnyRule.severity || 'error';
|
|
797
|
+
const message =
|
|
798
|
+
asAnyRule.message || 'Prefer proper types or "as unknown" for type assertions';
|
|
799
|
+
|
|
800
|
+
if (severity === 'error') {
|
|
801
|
+
this.errors.push(`Found 'as any' usage in ${this.filePath} - ${message}`);
|
|
802
|
+
console.error(` Line ${index + 1}: ${line.trim()}`);
|
|
803
|
+
foundIssues = true;
|
|
804
|
+
} else {
|
|
805
|
+
// Warning level - just warn, don't block
|
|
806
|
+
log.warning(`'as any' usage at line ${index + 1}: ${message}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Check for console statements based on React app rules
|
|
813
|
+
const consoleRule = config._fileConfig.rules?.console || {};
|
|
814
|
+
let allowConsole = false;
|
|
815
|
+
|
|
816
|
+
// Check if console is allowed in this file
|
|
817
|
+
if (consoleRule.enabled === false) {
|
|
818
|
+
allowConsole = true;
|
|
819
|
+
} else {
|
|
820
|
+
// Check allowed paths
|
|
821
|
+
const allowedPaths = consoleRule.allowIn?.paths || [];
|
|
822
|
+
if (allowedPaths.some((path) => this.filePath.includes(path))) {
|
|
823
|
+
allowConsole = true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check allowed file types
|
|
827
|
+
const allowedFileTypes = consoleRule.allowIn?.fileTypes || [];
|
|
828
|
+
if (allowedFileTypes.includes(this.fileType)) {
|
|
829
|
+
allowConsole = true;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Check allowed patterns
|
|
833
|
+
const allowedPatterns = consoleRule.allowIn?.patterns || [];
|
|
834
|
+
const fileName = path.basename(this.filePath);
|
|
835
|
+
if (
|
|
836
|
+
allowedPatterns.some((pattern) => {
|
|
837
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
838
|
+
return regex.test(fileName);
|
|
839
|
+
})
|
|
840
|
+
) {
|
|
841
|
+
allowConsole = true;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// For React apps, console is generally allowed but shows as info
|
|
846
|
+
if (!allowConsole && consoleRule.enabled !== false) {
|
|
847
|
+
lines.forEach((line, index) => {
|
|
848
|
+
if (/console\./.test(line)) {
|
|
849
|
+
const severity = consoleRule.severity || 'info';
|
|
850
|
+
const message = consoleRule.message || 'Consider using a logging library';
|
|
851
|
+
|
|
852
|
+
if (severity === 'error') {
|
|
853
|
+
this.errors.push(`Found console statements in ${this.filePath} - ${message}`);
|
|
854
|
+
console.error(` Line ${index + 1}: ${line.trim()}`);
|
|
855
|
+
foundIssues = true;
|
|
856
|
+
} else {
|
|
857
|
+
// Info level - just warn, don't block
|
|
858
|
+
log.warning(`Console usage at line ${index + 1}: ${message}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Check for debugger statements
|
|
865
|
+
const debuggerRule = config._fileConfig.rules?.debugger || {};
|
|
866
|
+
if (debuggerRule.enabled !== false) {
|
|
867
|
+
lines.forEach((line, index) => {
|
|
868
|
+
if (/\bdebugger\b/.test(line)) {
|
|
869
|
+
const severity = debuggerRule.severity || 'error';
|
|
870
|
+
const message =
|
|
871
|
+
debuggerRule.message || 'Remove debugger statements before committing';
|
|
872
|
+
|
|
873
|
+
if (severity === 'error') {
|
|
874
|
+
this.errors.push(`Found debugger statement in ${this.filePath} - ${message}`);
|
|
875
|
+
console.error(` Line ${index + 1}: ${line.trim()}`);
|
|
876
|
+
foundIssues = true;
|
|
877
|
+
} else {
|
|
878
|
+
log.warning(`Debugger statement at line ${index + 1}: ${message}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Check for TODO/FIXME comments
|
|
885
|
+
lines.forEach((line, index) => {
|
|
886
|
+
if (/TODO|FIXME/.test(line)) {
|
|
887
|
+
log.warning(`Found TODO/FIXME comment at line ${index + 1}`);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
if (!foundIssues) {
|
|
892
|
+
log.success('No common issues found');
|
|
893
|
+
}
|
|
894
|
+
} catch (error) {
|
|
895
|
+
log.debug(`Common issues check error: ${error.message}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Check for Node.js specific patterns and issues
|
|
901
|
+
* @returns {Promise<void>}
|
|
902
|
+
*/
|
|
903
|
+
async checkNodePatterns() {
|
|
904
|
+
// Only check TypeScript/JavaScript files
|
|
905
|
+
if (this.fileType === 'unknown') {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
log.info('Checking Node.js specific patterns...');
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const content = await fs.readFile(this.filePath, 'utf8');
|
|
913
|
+
let foundIssues = false;
|
|
914
|
+
|
|
915
|
+
// Check for unhandled process.exit()
|
|
916
|
+
if (/process\.exit\([^)]*\)/.test(content)) {
|
|
917
|
+
// Check if there are cleanup handlers
|
|
918
|
+
if (
|
|
919
|
+
!/process\.on\(['"]exit['"]/.test(content) &&
|
|
920
|
+
!/process\.on\(['"]SIGINT['"]/.test(content) &&
|
|
921
|
+
!/process\.on\(['"]SIGTERM['"]/.test(content)
|
|
922
|
+
) {
|
|
923
|
+
log.warning(
|
|
924
|
+
'Found process.exit() without cleanup handlers - consider adding signal handlers',
|
|
925
|
+
);
|
|
926
|
+
foundIssues = true;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Check for spawn/exec without error handling
|
|
931
|
+
if (/spawn\(|exec\(|execFile\(|fork\(/.test(content)) {
|
|
932
|
+
// Look for .on('error') in the same file
|
|
933
|
+
if (!/.on\(['"]error['"]/.test(content)) {
|
|
934
|
+
log.warning('Child process spawned without error handling - add .on("error") handler');
|
|
935
|
+
foundIssues = true;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Check for stream pipes without error handling
|
|
940
|
+
if (/\.pipe\(/.test(content)) {
|
|
941
|
+
// Count pipes and error handlers
|
|
942
|
+
const pipeCount = (content.match(/\.pipe\(/g) || []).length;
|
|
943
|
+
const errorHandlerCount = (content.match(/\.on\(['"]error['"]/g) || []).length;
|
|
944
|
+
|
|
945
|
+
if (pipeCount > 0 && errorHandlerCount < pipeCount) {
|
|
946
|
+
log.warning('Stream pipe without error handling - each pipe should have error handlers');
|
|
947
|
+
foundIssues = true;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Check for unhandled promise rejections in main/cli files
|
|
952
|
+
if (this.fileType === 'cli' || /index\.(ts|js)$/.test(this.filePath)) {
|
|
953
|
+
if (
|
|
954
|
+
/new Promise|async|await/.test(content) &&
|
|
955
|
+
!/process\.on\(['"]unhandledRejection['"]/.test(content)
|
|
956
|
+
) {
|
|
957
|
+
log.warning('Consider adding process.on("unhandledRejection") handler for CLI apps');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// MCP transport-specific checks
|
|
962
|
+
if (/\/(client|server)\/(stdio|sse|websocket|http)/.test(this.filePath)) {
|
|
963
|
+
if (!content.includes('try') && !content.includes('.catch')) {
|
|
964
|
+
log.warning('Transport implementation should have error handling (try/catch or .catch)');
|
|
965
|
+
foundIssues = true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (!foundIssues) {
|
|
970
|
+
log.success('Node.js patterns look good');
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
log.debug(`Node.js patterns check error: ${error.message}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Suggest related test files
|
|
979
|
+
* @returns {Promise<void>}
|
|
980
|
+
*/
|
|
981
|
+
async suggestRelatedTests() {
|
|
982
|
+
// Skip for test files
|
|
983
|
+
if (this.fileType === 'test') {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const baseName = this.filePath.replace(/\.[^.]+$/, '');
|
|
988
|
+
const testExtensions = ['test.ts', 'test.tsx', 'spec.ts', 'spec.tsx'];
|
|
989
|
+
let hasTests = false;
|
|
990
|
+
|
|
991
|
+
for (const ext of testExtensions) {
|
|
992
|
+
try {
|
|
993
|
+
await fs.access(`${baseName}.${ext}`);
|
|
994
|
+
hasTests = true;
|
|
995
|
+
log.warning(`💡 Related test found: ${path.basename(baseName)}.${ext}`);
|
|
996
|
+
log.warning(' Consider running the tests to ensure nothing broke');
|
|
997
|
+
break;
|
|
998
|
+
} catch {
|
|
999
|
+
// File doesn't exist, continue
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (!hasTests) {
|
|
1004
|
+
// Check __tests__ directory
|
|
1005
|
+
const dir = path.dirname(this.filePath);
|
|
1006
|
+
const fileName = path.basename(this.filePath);
|
|
1007
|
+
const baseFileName = fileName.replace(/\.[^.]+$/, '');
|
|
1008
|
+
|
|
1009
|
+
for (const ext of testExtensions) {
|
|
1010
|
+
try {
|
|
1011
|
+
await fs.access(path.join(dir, '__tests__', `${baseFileName}.${ext}`));
|
|
1012
|
+
hasTests = true;
|
|
1013
|
+
log.warning(`💡 Related test found: __tests__/${baseFileName}.${ext}`);
|
|
1014
|
+
log.warning(' Consider running the tests to ensure nothing broke');
|
|
1015
|
+
break;
|
|
1016
|
+
} catch {
|
|
1017
|
+
// File doesn't exist, continue
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (!hasTests) {
|
|
1023
|
+
log.warning(`💡 No test file found for ${path.basename(this.filePath)}`);
|
|
1024
|
+
log.warning(' Consider adding tests for better code quality');
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Special reminders for specific file types
|
|
1028
|
+
if (/\/services\//.test(this.filePath)) {
|
|
1029
|
+
log.warning('💡 Service file! Consider testing business logic');
|
|
1030
|
+
} else if (/\/(client|server)\//.test(this.filePath)) {
|
|
1031
|
+
log.warning('💡 Transport file! Consider testing connection handling');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Parse JSON input from stdin
|
|
1038
|
+
* @returns {Promise<Object>} Parsed JSON object
|
|
1039
|
+
*/
|
|
1040
|
+
async function parseJsonInput() {
|
|
1041
|
+
let inputData = '';
|
|
1042
|
+
|
|
1043
|
+
// Read from stdin
|
|
1044
|
+
for await (const chunk of process.stdin) {
|
|
1045
|
+
inputData += chunk;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (!inputData.trim()) {
|
|
1049
|
+
log.warning('No JSON input provided. This hook expects JSON input from Claude Code.');
|
|
1050
|
+
log.info(
|
|
1051
|
+
'For testing, provide JSON like: echo \'{"tool_name":"Edit","tool_input":{"file_path":"/path/to/file.ts"}}\' | node hook.js',
|
|
1052
|
+
);
|
|
1053
|
+
console.error(`\n${colors.yellow}👉 Hook executed but no input to process.${colors.reset}`);
|
|
1054
|
+
process.exit(0);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
try {
|
|
1058
|
+
return JSON.parse(inputData);
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
log.error(`Failed to parse JSON input: ${error.message}`);
|
|
1061
|
+
log.debug(`Input was: ${inputData}`);
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Extract file path from tool input
|
|
1068
|
+
* @param {Object} input - Tool input object
|
|
1069
|
+
* @returns {string|null} File path or null
|
|
1070
|
+
*/
|
|
1071
|
+
function extractFilePath(input) {
|
|
1072
|
+
const { tool_input } = input;
|
|
1073
|
+
if (!tool_input) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const filePath =
|
|
1078
|
+
tool_input.file_path ||
|
|
1079
|
+
tool_input.path ||
|
|
1080
|
+
tool_input.notebook_path ||
|
|
1081
|
+
tool_input.relative_path ||
|
|
1082
|
+
null;
|
|
1083
|
+
if (!filePath) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (path.isAbsolute(filePath)) {
|
|
1088
|
+
return filePath;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return path.join(projectRoot, filePath);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Check if file exists
|
|
1096
|
+
* @param {string} filePath - Path to check
|
|
1097
|
+
* @returns {Promise<boolean>} True if exists
|
|
1098
|
+
*/
|
|
1099
|
+
async function fileExists(filePath) {
|
|
1100
|
+
try {
|
|
1101
|
+
await fs.access(filePath);
|
|
1102
|
+
return true;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if file is a source file
|
|
1110
|
+
* @param {string} filePath - Path to check
|
|
1111
|
+
* @returns {boolean} True if source file
|
|
1112
|
+
*/
|
|
1113
|
+
function isSourceFile(filePath) {
|
|
1114
|
+
return /\.(ts|tsx|js|jsx)$/.test(filePath);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Print summary of errors and autofixes
|
|
1119
|
+
* @param {string[]} errors - List of errors
|
|
1120
|
+
* @param {string[]} autofixes - List of autofixes
|
|
1121
|
+
*/
|
|
1122
|
+
function printSummary(errors, autofixes) {
|
|
1123
|
+
// Show auto-fixes if any
|
|
1124
|
+
if (autofixes.length > 0) {
|
|
1125
|
+
console.error(`\n${colors.blue}═══ Auto-fixes Applied ═══${colors.reset}`);
|
|
1126
|
+
autofixes.forEach((fix) => {
|
|
1127
|
+
console.error(`${colors.green}✨${colors.reset} ${fix}`);
|
|
1128
|
+
});
|
|
1129
|
+
console.error(
|
|
1130
|
+
`${colors.green}Automatically fixed ${autofixes.length} issue(s) for you!${colors.reset}`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Show errors if any
|
|
1135
|
+
if (errors.length > 0) {
|
|
1136
|
+
console.error(`\n${colors.blue}═══ Quality Check Summary ═══${colors.reset}`);
|
|
1137
|
+
errors.forEach((error) => {
|
|
1138
|
+
console.error(`${colors.red}❌${colors.reset} ${error}`);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
console.error(
|
|
1142
|
+
`\n${colors.red}Found ${errors.length} issue(s) that MUST be fixed!${colors.reset}`,
|
|
1143
|
+
);
|
|
1144
|
+
console.error(`${colors.red}════════════════════════════════════════════${colors.reset}`);
|
|
1145
|
+
console.error(`${colors.red}❌ ALL ISSUES ARE BLOCKING ❌${colors.reset}`);
|
|
1146
|
+
console.error(`${colors.red}════════════════════════════════════════════${colors.reset}`);
|
|
1147
|
+
console.error(`${colors.red}Fix EVERYTHING above until all checks are ✅ GREEN${colors.reset}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Main entry point
|
|
1153
|
+
* @returns {Promise<void>}
|
|
1154
|
+
*/
|
|
1155
|
+
async function main() {
|
|
1156
|
+
// Show header with version
|
|
1157
|
+
const hookVersion = config._fileConfig.version || '1.0.0';
|
|
1158
|
+
console.error('');
|
|
1159
|
+
console.error(`📦 Node.js Quality Check v${hookVersion} - Starting...`);
|
|
1160
|
+
console.error('────────────────────────────────────────────');
|
|
1161
|
+
|
|
1162
|
+
// Debug: show loaded configuration
|
|
1163
|
+
log.debug(`Loaded config: ${JSON.stringify(config, null, 2)}`);
|
|
1164
|
+
|
|
1165
|
+
// Parse input
|
|
1166
|
+
const input = await parseJsonInput();
|
|
1167
|
+
const filePath = extractFilePath(input);
|
|
1168
|
+
|
|
1169
|
+
if (!filePath) {
|
|
1170
|
+
log.warning('No file path found in JSON input. Tool might not be file-related.');
|
|
1171
|
+
log.debug(`JSON input was: ${JSON.stringify(input)}`);
|
|
1172
|
+
console.error(
|
|
1173
|
+
`\n${colors.yellow}👉 No file to check - tool may not be file-related.${colors.reset}`,
|
|
1174
|
+
);
|
|
1175
|
+
process.exit(0);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Check if file exists
|
|
1179
|
+
if (!(await fileExists(filePath))) {
|
|
1180
|
+
log.info(`File does not exist: ${filePath} (may have been deleted)`);
|
|
1181
|
+
console.error(`\n${colors.yellow}👉 File skipped - doesn't exist.${colors.reset}`);
|
|
1182
|
+
process.exit(0);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// For non-source files, exit successfully without checks (matching shell behavior)
|
|
1186
|
+
if (!isSourceFile(filePath)) {
|
|
1187
|
+
log.info(`Skipping non-source file: ${filePath}`);
|
|
1188
|
+
console.error(`\n${colors.yellow}👉 File skipped - not a source file.${colors.reset}`);
|
|
1189
|
+
console.error(
|
|
1190
|
+
`\n${colors.green}✅ No checks needed for ${path.basename(filePath)}${colors.reset}`,
|
|
1191
|
+
);
|
|
1192
|
+
process.exit(0);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Update header with file name
|
|
1196
|
+
console.error('');
|
|
1197
|
+
console.error(`🔍 Validating: ${path.basename(filePath)}`);
|
|
1198
|
+
console.error('────────────────────────────────────────────');
|
|
1199
|
+
log.info(`Checking: ${filePath}`);
|
|
1200
|
+
|
|
1201
|
+
// Run quality checks
|
|
1202
|
+
const checker = new QualityChecker(filePath);
|
|
1203
|
+
const { errors, autofixes } = await checker.checkAll();
|
|
1204
|
+
|
|
1205
|
+
// Print summary
|
|
1206
|
+
printSummary(errors, autofixes);
|
|
1207
|
+
|
|
1208
|
+
// Separate edited file errors from other issues
|
|
1209
|
+
const editedFileErrors = errors.filter(
|
|
1210
|
+
(e) =>
|
|
1211
|
+
e.includes('edited file') ||
|
|
1212
|
+
e.includes('ESLint found issues') ||
|
|
1213
|
+
e.includes('Prettier formatting issues') ||
|
|
1214
|
+
e.includes('console statements') ||
|
|
1215
|
+
e.includes('debugger statement') ||
|
|
1216
|
+
e.includes("'as any' usage") ||
|
|
1217
|
+
e.includes('were auto-fixed'),
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
const dependencyWarnings = errors.filter((e) => !editedFileErrors.includes(e));
|
|
1221
|
+
|
|
1222
|
+
// Exit with appropriate code
|
|
1223
|
+
if (editedFileErrors.length > 0) {
|
|
1224
|
+
// Critical - blocks immediately
|
|
1225
|
+
console.error(`\n${colors.red}🛑 FAILED - Fix issues in your edited file! 🛑${colors.reset}`);
|
|
1226
|
+
console.error(`${colors.cyan}💡 CLAUDE.md CHECK:${colors.reset}`);
|
|
1227
|
+
console.error(
|
|
1228
|
+
`${colors.cyan} → What CLAUDE.md pattern would have prevented this?${colors.reset}`,
|
|
1229
|
+
);
|
|
1230
|
+
console.error(`${colors.cyan} → Are you following JSDoc batching strategy?${colors.reset}`);
|
|
1231
|
+
console.error(`${colors.yellow}📋 NEXT STEPS:${colors.reset}`);
|
|
1232
|
+
console.error(`${colors.yellow} 1. Fix the issues listed above${colors.reset}`);
|
|
1233
|
+
console.error(`${colors.yellow} 2. The hook will run again automatically${colors.reset}`);
|
|
1234
|
+
console.error(
|
|
1235
|
+
`${colors.yellow} 3. Continue with your original task once all checks pass${colors.reset}`,
|
|
1236
|
+
);
|
|
1237
|
+
process.exit(2);
|
|
1238
|
+
} else if (dependencyWarnings.length > 0) {
|
|
1239
|
+
// Warning - shows but doesn't block
|
|
1240
|
+
console.error(`\n${colors.yellow}⚠️ WARNING - Dependency issues found${colors.reset}`);
|
|
1241
|
+
console.error(
|
|
1242
|
+
`${colors.yellow}These won't block your progress but should be addressed${colors.reset}`,
|
|
1243
|
+
);
|
|
1244
|
+
console.error(
|
|
1245
|
+
`\n${colors.green}✅ Quality check passed for ${path.basename(filePath)}${colors.reset}`,
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
if (autofixes.length > 0 && config.autofixSilent) {
|
|
1249
|
+
console.error(
|
|
1250
|
+
`\n${colors.yellow}👉 File quality verified. Auto-fixes applied. Continue with your task.${colors.reset}`,
|
|
1251
|
+
);
|
|
1252
|
+
} else {
|
|
1253
|
+
console.error(
|
|
1254
|
+
`\n${colors.yellow}👉 File quality verified. Continue with your task.${colors.reset}`,
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
process.exit(0); // Don't block on dependency issues
|
|
1258
|
+
} else {
|
|
1259
|
+
console.error(
|
|
1260
|
+
`\n${colors.green}✅ Quality check passed for ${path.basename(filePath)}${colors.reset}`,
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
if (autofixes.length > 0 && config.autofixSilent) {
|
|
1264
|
+
console.error(
|
|
1265
|
+
`\n${colors.yellow}👉 File quality verified. Auto-fixes applied. Continue with your task.${colors.reset}`,
|
|
1266
|
+
);
|
|
1267
|
+
} else {
|
|
1268
|
+
console.error(
|
|
1269
|
+
`\n${colors.yellow}👉 File quality verified. Continue with your task.${colors.reset}`,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
process.exit(0);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Handle errors
|
|
1277
|
+
process.on('unhandledRejection', (error) => {
|
|
1278
|
+
log.error(`Unhandled error: ${error.message}`);
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// Run main
|
|
1283
|
+
main().catch((error) => {
|
|
1284
|
+
log.error(`Fatal error: ${error.message}`);
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
});
|