claude-git-hooks 2.35.3 → 2.44.0
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 +157 -0
- package/CLAUDE.md +24 -1389
- package/README.md +115 -2
- package/bin/claude-hooks +11 -7
- package/lib/cli-metadata.js +17 -3
- package/lib/commands/analyze-pr.js +270 -145
- package/lib/commands/analyze.js +151 -3
- package/lib/commands/create-pr.js +345 -134
- package/lib/commands/help.js +155 -81
- package/lib/commands/helpers.js +9 -4
- package/lib/commands/hooks.js +5 -5
- package/lib/commands/install.js +77 -28
- package/lib/commands/lint.js +120 -4
- package/lib/config.js +3 -0
- package/lib/hooks/pre-commit.js +26 -5
- package/lib/hooks/prepare-commit-msg.js +78 -4
- package/lib/utils/analysis-engine.js +12 -6
- package/lib/utils/claude-client.js +222 -12
- package/lib/utils/claude-diagnostics.js +5 -4
- package/lib/utils/cost-tracker.js +128 -0
- package/lib/utils/diff-analysis-orchestrator.js +2 -1
- package/lib/utils/git-operations.js +105 -2
- package/lib/utils/hooks-verified-marker.js +121 -0
- package/lib/utils/interactive-ui.js +4 -4
- package/lib/utils/judge.js +3 -2
- package/lib/utils/langfuse-tracer.js +156 -0
- package/lib/utils/logger.js +30 -5
- package/lib/utils/pr-metadata-engine.js +4 -2
- package/package.json +4 -2
- package/templates/HELP_NAVIGATE.md +41 -0
- package/templates/HELP_QUERY.md +7 -11
package/lib/commands/help.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Features:
|
|
6
6
|
* - Dynamic help from command registry (no args, --help, -h)
|
|
7
|
-
* - AI help:
|
|
7
|
+
* - AI help: two-pass librarian navigating .library/ for answers
|
|
8
8
|
* - Report issue: interactive issue creation guided by Claude
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -160,59 +160,130 @@ function printAiResponse(response, source) {
|
|
|
160
160
|
console.log('');
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Package root directory — resolved once from __dirname */
|
|
164
|
+
const _packageRoot = path.join(__dirname, '..', '..');
|
|
165
|
+
|
|
163
166
|
/**
|
|
164
|
-
*
|
|
165
|
-
* Why:
|
|
167
|
+
* Catalog directories within .library/ to auto-discover
|
|
168
|
+
* Why: These contain index and shelf files that form the navigational catalog.
|
|
169
|
+
* books/ is excluded — those are fetched on-demand in Pass 2.
|
|
170
|
+
*/
|
|
171
|
+
const _CATALOG_DIRS = ['by-code', 'by-domain', 'by-task-type'];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Read a single file from package root, returning null on failure
|
|
166
175
|
*
|
|
167
|
-
* @
|
|
176
|
+
* @param {string} relativePath - Path relative to package root
|
|
177
|
+
* @returns {Promise<string|null>} File content or null
|
|
168
178
|
*/
|
|
169
|
-
const
|
|
170
|
-
const claudeMdPath = path.join(__dirname, '..', '..', 'CLAUDE.md');
|
|
179
|
+
const _readPackageFile = async (relativePath) => {
|
|
171
180
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
return await fs.readFile(path.join(_packageRoot, relativePath), 'utf8');
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Read the project catalog from .library/ and CLAUDE.md
|
|
189
|
+
* Why: The catalog provides navigational context for the AI librarian (Pass 1).
|
|
190
|
+
* Auto-discovers index/shelf files so the catalog stays current as .library/ grows.
|
|
191
|
+
*
|
|
192
|
+
* Reads:
|
|
193
|
+
* - CLAUDE.md (global rules)
|
|
194
|
+
* - .library/index.md (repo identity, capabilities)
|
|
195
|
+
* - .library/conventions.md (coding standards)
|
|
196
|
+
* - .library/by-code/*.md (source-path shelves)
|
|
197
|
+
* - .library/by-domain/*.md (workflow reading lists)
|
|
198
|
+
* - .library/by-task-type/*.md (task guidance)
|
|
199
|
+
*
|
|
200
|
+
* @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
|
|
201
|
+
*/
|
|
202
|
+
const readLibraryCatalog = async () => {
|
|
203
|
+
const sections = [];
|
|
204
|
+
|
|
205
|
+
// Fixed files: CLAUDE.md + core library files
|
|
206
|
+
const fixedFiles = [
|
|
207
|
+
{ label: 'CLAUDE.md', path: 'CLAUDE.md' },
|
|
208
|
+
{ label: '.library/index.md', path: '.library/index.md' },
|
|
209
|
+
{ label: '.library/conventions.md', path: '.library/conventions.md' }
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const file of fixedFiles) {
|
|
213
|
+
const content = await _readPackageFile(file.path);
|
|
214
|
+
if (content) {
|
|
215
|
+
sections.push(`--- ${file.label} ---\n${content}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Auto-discover .md files in each catalog directory
|
|
220
|
+
for (const dir of _CATALOG_DIRS) {
|
|
221
|
+
const dirPath = path.join(_packageRoot, '.library', dir);
|
|
222
|
+
let entries;
|
|
223
|
+
try {
|
|
224
|
+
entries = await fs.readdir(dirPath);
|
|
225
|
+
} catch {
|
|
226
|
+
logger.debug('help - readLibraryCatalog', `Could not read .library/${dir}/`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const mdFiles = entries.filter((f) => f.endsWith('.md')).sort();
|
|
231
|
+
for (const file of mdFiles) {
|
|
232
|
+
const relativePath = `.library/${dir}/${file}`;
|
|
233
|
+
const content = await _readPackageFile(relativePath);
|
|
234
|
+
if (content) {
|
|
235
|
+
sections.push(`--- ${relativePath} ---\n${content}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (sections.length === 0) {
|
|
241
|
+
logger.debug('help - readLibraryCatalog', 'No catalog files found');
|
|
180
242
|
return null;
|
|
181
243
|
}
|
|
244
|
+
|
|
245
|
+
const catalog = sections.join('\n\n');
|
|
246
|
+
logger.debug('help - readLibraryCatalog', 'Catalog loaded', {
|
|
247
|
+
sections: sections.length,
|
|
248
|
+
length: catalog.length
|
|
249
|
+
});
|
|
250
|
+
return catalog;
|
|
182
251
|
};
|
|
183
252
|
|
|
184
253
|
/**
|
|
185
|
-
* AI-powered help:
|
|
186
|
-
* Why: Provides intelligent answers
|
|
254
|
+
* AI-powered help: two-pass librarian navigating .library/
|
|
255
|
+
* Why: Provides intelligent answers by reading the project catalog, then
|
|
256
|
+
* fetching specific books from local disk only when deeper detail is needed.
|
|
187
257
|
*
|
|
188
258
|
* Flow:
|
|
189
|
-
* 1. Read
|
|
190
|
-
* 2.
|
|
191
|
-
* 3.
|
|
192
|
-
* 4. If
|
|
193
|
-
* 5.
|
|
194
|
-
* 6. On any error: fall back to static help
|
|
259
|
+
* 1. Read catalog from .library/ (auto-discovered indexes + shelves)
|
|
260
|
+
* 2. Pass 1: Send catalog to Claude via HELP_NAVIGATE.md
|
|
261
|
+
* 3. If ANSWER: display directly (1 Claude call)
|
|
262
|
+
* 4. If BOOKS: read requested books from local disk, send to HELP_QUERY.md (2 Claude calls)
|
|
263
|
+
* 5. On any error: fall back to static help
|
|
195
264
|
*
|
|
196
265
|
* @param {string} question - User's natural language question
|
|
197
266
|
*/
|
|
198
267
|
async function runAiHelp(question) {
|
|
199
268
|
try {
|
|
200
|
-
const
|
|
201
|
-
if (!
|
|
269
|
+
const catalog = await readLibraryCatalog();
|
|
270
|
+
if (!catalog) {
|
|
202
271
|
logger.debug(
|
|
203
272
|
'help - runAiHelp',
|
|
204
|
-
'
|
|
273
|
+
'Catalog not available, falling back to static help'
|
|
205
274
|
);
|
|
206
275
|
showStaticHelp();
|
|
207
276
|
return;
|
|
208
277
|
}
|
|
209
278
|
|
|
210
|
-
const prompt = await loadPrompt('
|
|
211
|
-
|
|
279
|
+
const prompt = await loadPrompt('HELP_NAVIGATE.md', {
|
|
280
|
+
CATALOG: catalog,
|
|
212
281
|
QUESTION: question
|
|
213
282
|
});
|
|
214
283
|
|
|
215
|
-
logger.debug('help - runAiHelp', 'Querying Claude', {
|
|
284
|
+
logger.debug('help - runAiHelp', 'Querying Claude (Pass 1)', {
|
|
285
|
+
questionLength: question.length
|
|
286
|
+
});
|
|
216
287
|
console.log('\nSearching documentation...\n');
|
|
217
288
|
|
|
218
289
|
const pkg = getPackageJson();
|
|
@@ -221,95 +292,98 @@ async function runAiHelp(question) {
|
|
|
221
292
|
const response = await executeClaudeWithRetry(prompt, { timeout: 60000 });
|
|
222
293
|
const trimmedResponse = response.trim();
|
|
223
294
|
|
|
224
|
-
// Check for
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const enrichedResponse = await handleNeedMoreContext(
|
|
230
|
-
needMoreLine,
|
|
295
|
+
// Check for BOOKS: second pass
|
|
296
|
+
const booksLine = trimmedResponse.split('\n').find((l) => l.startsWith('BOOKS:'));
|
|
297
|
+
if (booksLine) {
|
|
298
|
+
const enrichedAnswer = await readAndAnswerWithBooks(
|
|
299
|
+
booksLine,
|
|
231
300
|
question,
|
|
232
|
-
|
|
301
|
+
catalog
|
|
233
302
|
);
|
|
234
|
-
if (
|
|
235
|
-
printAiResponse(
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
// Enrichment failed: show first-pass answer with marker line stripped
|
|
239
|
-
const cleanResponse = trimmedResponse
|
|
240
|
-
.split('\n')
|
|
241
|
-
.filter((l) => !l.includes('NEED_MORE_CONTEXT'))
|
|
242
|
-
.join('\n')
|
|
243
|
-
.trim();
|
|
244
|
-
if (cleanResponse) {
|
|
245
|
-
printAiResponse(cleanResponse, localVersion);
|
|
303
|
+
if (enrichedAnswer) {
|
|
304
|
+
printAiResponse(enrichedAnswer, `${localVersion} + source`);
|
|
246
305
|
return;
|
|
247
306
|
}
|
|
307
|
+
// Books not readable — avoid surfacing raw BOOKS: line to the user
|
|
308
|
+
showStaticHelp();
|
|
309
|
+
return;
|
|
248
310
|
}
|
|
249
311
|
|
|
250
|
-
|
|
251
|
-
|
|
312
|
+
// ANSWER: response or fallback — strip the "ANSWER:" prefix if present
|
|
313
|
+
const answerLine = trimmedResponse.indexOf('ANSWER:');
|
|
314
|
+
const answer =
|
|
315
|
+
answerLine !== -1
|
|
316
|
+
? trimmedResponse.substring(answerLine + 'ANSWER:'.length).trim()
|
|
317
|
+
: trimmedResponse;
|
|
318
|
+
|
|
319
|
+
printAiResponse(answer, localVersion);
|
|
320
|
+
} catch (err) {
|
|
252
321
|
logger.debug('help - runAiHelp', 'AI help failed, falling back to static help', {
|
|
253
|
-
error:
|
|
322
|
+
error: err.message
|
|
254
323
|
});
|
|
255
324
|
showStaticHelp();
|
|
256
325
|
}
|
|
257
326
|
}
|
|
258
327
|
|
|
259
328
|
/**
|
|
260
|
-
*
|
|
329
|
+
* Pass 2: Read requested books from local disk and answer with enriched context
|
|
261
330
|
*
|
|
262
|
-
* @param {string}
|
|
331
|
+
* @param {string} booksLine - The line starting with "BOOKS:" containing comma-separated paths
|
|
263
332
|
* @param {string} question - Original user question
|
|
264
|
-
* @param {string}
|
|
265
|
-
* @returns {Promise<string|null>} Enriched
|
|
333
|
+
* @param {string} catalog - The catalog content from Pass 1
|
|
334
|
+
* @returns {Promise<string|null>} Enriched answer or null on failure
|
|
266
335
|
*/
|
|
267
|
-
async function
|
|
336
|
+
async function readAndAnswerWithBooks(booksLine, question, catalog) {
|
|
268
337
|
try {
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const filePaths = pathsPart
|
|
338
|
+
const pathsPart = booksLine.replace('BOOKS:', '').trim();
|
|
339
|
+
const bookPaths = pathsPart
|
|
272
340
|
.split(',')
|
|
273
341
|
.map((p) => p.trim())
|
|
274
342
|
.filter(Boolean);
|
|
275
343
|
|
|
276
|
-
if (
|
|
277
|
-
logger.debug('help -
|
|
344
|
+
if (bookPaths.length === 0) {
|
|
345
|
+
logger.debug('help - readAndAnswerWithBooks', 'No book paths in BOOKS line');
|
|
278
346
|
return null;
|
|
279
347
|
}
|
|
280
348
|
|
|
281
|
-
logger.debug('help -
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
349
|
+
logger.debug('help - readAndAnswerWithBooks', 'Reading books from disk', { bookPaths });
|
|
350
|
+
|
|
351
|
+
// Read books from local filesystem in parallel
|
|
352
|
+
// Contain LLM-sourced paths to the package root to prevent traversal
|
|
353
|
+
const readResults = await Promise.all(
|
|
354
|
+
bookPaths.map(async (bookPath) => {
|
|
355
|
+
const resolvedPath = path.resolve(_packageRoot, bookPath);
|
|
356
|
+
if (!resolvedPath.startsWith(_packageRoot + path.sep)) {
|
|
357
|
+
logger.debug('help - readAndAnswerWithBooks', 'Path traversal blocked', { bookPath });
|
|
358
|
+
return { bookPath, content: null };
|
|
359
|
+
}
|
|
360
|
+
const content = await _readPackageFile(bookPath);
|
|
361
|
+
return { bookPath, content };
|
|
290
362
|
})
|
|
291
363
|
);
|
|
292
364
|
|
|
293
|
-
|
|
294
|
-
const additionalContent = fetchResults
|
|
365
|
+
const bookContent = readResults
|
|
295
366
|
.filter((r) => r.content !== null)
|
|
296
|
-
.map((r) =>
|
|
297
|
-
.join('\n');
|
|
367
|
+
.map((r) => `--- ${r.bookPath} ---\n${r.content}`)
|
|
368
|
+
.join('\n\n');
|
|
298
369
|
|
|
299
|
-
if (!
|
|
300
|
-
logger.debug('help -
|
|
370
|
+
if (!bookContent) {
|
|
371
|
+
logger.debug('help - readAndAnswerWithBooks', 'No book content read from disk');
|
|
301
372
|
return null;
|
|
302
373
|
}
|
|
303
374
|
|
|
304
375
|
const enrichedPrompt = await loadPrompt('HELP_QUERY.md', {
|
|
305
|
-
|
|
376
|
+
CATALOG: catalog,
|
|
377
|
+
BOOKS: bookContent,
|
|
306
378
|
QUESTION: question
|
|
307
379
|
});
|
|
308
380
|
|
|
309
381
|
const enrichedResponse = await executeClaudeWithRetry(enrichedPrompt, { timeout: 60000 });
|
|
310
382
|
return enrichedResponse.trim();
|
|
311
|
-
} catch (
|
|
312
|
-
logger.debug('help -
|
|
383
|
+
} catch (err) {
|
|
384
|
+
logger.debug('help - readAndAnswerWithBooks', 'Book enrichment failed', {
|
|
385
|
+
error: err.message
|
|
386
|
+
});
|
|
313
387
|
return null;
|
|
314
388
|
}
|
|
315
389
|
}
|
|
@@ -371,8 +445,8 @@ async function runReportIssue() {
|
|
|
371
445
|
return;
|
|
372
446
|
}
|
|
373
447
|
|
|
374
|
-
// Step 5: Read CLAUDE.md for project context
|
|
375
|
-
const claudeMdContent = await
|
|
448
|
+
// Step 5: Read CLAUDE.md for project context (report-issue only needs basic context)
|
|
449
|
+
const claudeMdContent = await _readPackageFile('CLAUDE.md');
|
|
376
450
|
|
|
377
451
|
// Step 6: Generate questions from template using Claude
|
|
378
452
|
const questionsPrompt = await loadPrompt('HELP_REPORT_ISSUE.md', {
|
package/lib/commands/helpers.js
CHANGED
|
@@ -19,6 +19,7 @@ import https from 'https';
|
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { dirname } from 'path';
|
|
21
21
|
import { getRunningWSLDistros } from '../utils/claude-client.js';
|
|
22
|
+
import logger from '../utils/logger.js';
|
|
22
23
|
|
|
23
24
|
// Why: ES6 modules don't have __dirname, need to recreate it
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -38,20 +39,24 @@ export function log(message, color = 'reset') {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function error(message) {
|
|
41
|
-
|
|
42
|
+
logger.error('helpers', message);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fatal(message) {
|
|
46
|
+
logger.error('helpers', message);
|
|
42
47
|
process.exit(1);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export function success(message) {
|
|
46
|
-
|
|
51
|
+
logger.success(message);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export function info(message) {
|
|
50
|
-
|
|
55
|
+
logger.info(message);
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export function warning(message) {
|
|
54
|
-
|
|
59
|
+
logger.warning(message);
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
/**
|
package/lib/commands/hooks.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { error, success, info, warning, checkGitRepo, getGitHooksPath } from './helpers.js';
|
|
8
|
+
import { error, fatal, success, info, warning, checkGitRepo, getGitHooksPath } from './helpers.js';
|
|
9
9
|
import { removeCompletions } from './install.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -14,7 +14,7 @@ import { removeCompletions } from './install.js';
|
|
|
14
14
|
*/
|
|
15
15
|
export function runEnable(hookName) {
|
|
16
16
|
if (!checkGitRepo()) {
|
|
17
|
-
|
|
17
|
+
fatal('You are not in a Git repository.');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const hooksDir = getGitHooksPath();
|
|
@@ -41,7 +41,7 @@ export function runEnable(hookName) {
|
|
|
41
41
|
*/
|
|
42
42
|
export function runDisable(hookName) {
|
|
43
43
|
if (!checkGitRepo()) {
|
|
44
|
-
|
|
44
|
+
fatal('You are not in a Git repository.');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const hooksDir = getGitHooksPath();
|
|
@@ -67,7 +67,7 @@ export function runDisable(hookName) {
|
|
|
67
67
|
*/
|
|
68
68
|
export function runStatus() {
|
|
69
69
|
if (!checkGitRepo()) {
|
|
70
|
-
|
|
70
|
+
fatal('You are not in a Git repository.');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
info('Claude Git Hooks status:\n');
|
|
@@ -128,7 +128,7 @@ export function runStatus() {
|
|
|
128
128
|
*/
|
|
129
129
|
export function runUninstall() {
|
|
130
130
|
if (!checkGitRepo()) {
|
|
131
|
-
|
|
131
|
+
fatal('You are not in a Git repository.');
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
info('Uninstalling Claude Git Hooks...');
|
package/lib/commands/install.js
CHANGED
|
@@ -18,6 +18,7 @@ import os from 'os';
|
|
|
18
18
|
import readline from 'readline';
|
|
19
19
|
import {
|
|
20
20
|
error,
|
|
21
|
+
fatal,
|
|
21
22
|
success,
|
|
22
23
|
info,
|
|
23
24
|
warning,
|
|
@@ -176,7 +177,7 @@ async function checkAndInstallDependencies(skipAuth = false) {
|
|
|
176
177
|
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
|
|
177
178
|
success(`Node.js ${nodeVersion}`);
|
|
178
179
|
} catch (e) {
|
|
179
|
-
|
|
180
|
+
fatal('Node.js is not installed. Install Node.js and try again.');
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
// Check npm
|
|
@@ -184,7 +185,7 @@ async function checkAndInstallDependencies(skipAuth = false) {
|
|
|
184
185
|
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
185
186
|
success(`npm ${npmVersion}`);
|
|
186
187
|
} catch (e) {
|
|
187
|
-
|
|
188
|
+
fatal('npm is not installed.');
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
// Check Maven (optional — needed for backend/fullstack presets with Spotless)
|
|
@@ -205,7 +206,7 @@ async function checkAndInstallDependencies(skipAuth = false) {
|
|
|
205
206
|
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
206
207
|
success(`${gitVersion}`);
|
|
207
208
|
} catch (e) {
|
|
208
|
-
|
|
209
|
+
fatal('Git is not installed. Install Git and try again.');
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
// v2.0.0+: Unix tools (sed, awk, grep, etc.) no longer needed (pure Node.js implementation)
|
|
@@ -387,20 +388,30 @@ async function autoMigrateConfig(newConfigPath, backupConfigPath) {
|
|
|
387
388
|
*/
|
|
388
389
|
export async function runInstall(args) {
|
|
389
390
|
if (!checkGitRepo()) {
|
|
390
|
-
|
|
391
|
+
fatal(
|
|
391
392
|
'You are not in a Git repository. Please run this command from the root of a repository.'
|
|
392
393
|
);
|
|
393
394
|
}
|
|
394
395
|
|
|
395
396
|
const isForce = args.includes('--force');
|
|
396
397
|
const skipAuth = args.includes('--skip-auth');
|
|
398
|
+
const isHeadless = args.includes('--headless');
|
|
399
|
+
const verifySdk = args.includes('--verify-sdk');
|
|
397
400
|
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
if (skipAuth) {
|
|
402
|
+
warning('--skip-auth is deprecated. Use --headless for full CI/container mode.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const skipVersionCheck = skipAuth || isHeadless;
|
|
406
|
+
|
|
407
|
+
// Check for updates (unless --skip-auth, --headless, or --force flag)
|
|
408
|
+
if (!skipVersionCheck && !isForce) {
|
|
400
409
|
await checkVersionAndPromptUpdate();
|
|
401
410
|
}
|
|
402
411
|
|
|
403
|
-
if (
|
|
412
|
+
if (isHeadless) {
|
|
413
|
+
info('Installing Claude Git Hooks (headless mode)...');
|
|
414
|
+
} else if (isForce) {
|
|
404
415
|
info('Installing Claude Git Hooks (force mode)...');
|
|
405
416
|
} else {
|
|
406
417
|
info('Installing Claude Git Hooks...');
|
|
@@ -408,7 +419,12 @@ export async function runInstall(args) {
|
|
|
408
419
|
|
|
409
420
|
// v2.0.0+: No sudo needed (pure Node.js, no system packages required)
|
|
410
421
|
// Check dependencies
|
|
411
|
-
|
|
422
|
+
if (!isHeadless) {
|
|
423
|
+
await checkAndInstallDependencies(skipAuth);
|
|
424
|
+
} else {
|
|
425
|
+
// Headless: skip Claude CLI/auth checks and Maven — CI manages its own deps
|
|
426
|
+
info('Skipping dependency checks (headless mode)');
|
|
427
|
+
}
|
|
412
428
|
|
|
413
429
|
const templatesPath = getTemplatesPath();
|
|
414
430
|
const hooksPath = getGitHooksPath();
|
|
@@ -658,33 +674,66 @@ export async function runInstall(args) {
|
|
|
658
674
|
info('settings.local.json created (add your GitHub token here)');
|
|
659
675
|
}
|
|
660
676
|
|
|
661
|
-
// Configure Git
|
|
662
|
-
|
|
677
|
+
// Configure Git — skipped in headless to avoid mutating CI git config
|
|
678
|
+
if (!isHeadless) {
|
|
679
|
+
configureGit();
|
|
680
|
+
}
|
|
663
681
|
|
|
664
|
-
// Update .gitignore
|
|
665
|
-
|
|
682
|
+
// Update .gitignore — skipped in headless: CI repos manage .gitignore explicitly
|
|
683
|
+
if (!isHeadless) {
|
|
684
|
+
updateGitignore();
|
|
685
|
+
}
|
|
666
686
|
|
|
667
|
-
// Install shell completions
|
|
668
|
-
|
|
687
|
+
// Install shell completions — skipped in headless: shell completions are useless in CI
|
|
688
|
+
if (!isHeadless) {
|
|
689
|
+
installCompletions();
|
|
690
|
+
}
|
|
669
691
|
|
|
670
|
-
// Check linter toolchain availability
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
692
|
+
// Check linter toolchain availability — skipped in headless: linter availability is a CI concern
|
|
693
|
+
if (!isHeadless) {
|
|
694
|
+
try {
|
|
695
|
+
const config = await getConfig();
|
|
696
|
+
const presetName = config.preset || 'default';
|
|
697
|
+
if (config.linting?.enabled !== false) {
|
|
698
|
+
const { checkLinterAvailability } = await import('../utils/linter-runner.js');
|
|
699
|
+
await checkLinterAvailability(presetName);
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
// Non-fatal — linter check failure should not block installation
|
|
677
703
|
}
|
|
678
|
-
} catch {
|
|
679
|
-
// Non-fatal — linter check failure should not block installation
|
|
680
704
|
}
|
|
681
705
|
|
|
682
|
-
|
|
683
|
-
|
|
706
|
+
if (isHeadless) {
|
|
707
|
+
success('claude-hooks installed (headless mode).');
|
|
708
|
+
} else {
|
|
709
|
+
success('Claude Git Hooks installed successfully! 🎉');
|
|
710
|
+
console.log('\nRun claude-hooks --help to see all available commands.');
|
|
711
|
+
|
|
712
|
+
// Run GitHub token setup
|
|
713
|
+
console.log('');
|
|
714
|
+
await runSetupGitHub();
|
|
715
|
+
}
|
|
684
716
|
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
717
|
+
// Optional SDK verification — opt-in, never automatic
|
|
718
|
+
if (verifySdk) {
|
|
719
|
+
info('Verifying SDK connectivity (1-token ping)...');
|
|
720
|
+
try {
|
|
721
|
+
const { verifySDKConnection } = await import('../utils/claude-client.js');
|
|
722
|
+
const result = await verifySDKConnection();
|
|
723
|
+
if (result.ok) {
|
|
724
|
+
success(`SDK verified: ${result.usage.input_tokens} in / ${result.usage.output_tokens} out tokens.`);
|
|
725
|
+
} else {
|
|
726
|
+
throw new Error(result.error);
|
|
727
|
+
}
|
|
728
|
+
} catch (sdkErr) {
|
|
729
|
+
if (isHeadless) {
|
|
730
|
+
error(`SDK verification failed: ${sdkErr.message}`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
} else {
|
|
733
|
+
warning(`SDK verification failed: ${sdkErr.message}. Install completed but SDK is not reachable.`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
688
737
|
}
|
|
689
738
|
|
|
690
739
|
// ── Shell completion generation ──────────────────────────────────────────────
|