claude-git-hooks 2.35.2 → 2.43.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 +150 -0
- package/CLAUDE.md +24 -1384
- package/README.md +113 -0
- 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/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 +4 -1
- 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 +4 -3
- package/lib/utils/langfuse-tracer.js +156 -0
- package/lib/utils/linter-runner.js +29 -4
- package/lib/utils/logger.js +30 -5
- package/lib/utils/pr-metadata-engine.js +4 -2
- package/lib/utils/tool-runner.js +4 -3
- package/package.json +4 -2
- package/templates/CUSTOMIZATION_GUIDE.md +2 -2
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File: create-pr.js
|
|
3
3
|
* Purpose: Create pull request with auto-generated metadata and reviewers
|
|
4
|
+
*
|
|
5
|
+
* Headless mode (--headless): skips all interactive prompts, uses documented defaults.
|
|
6
|
+
* JSON output (--format json): emits single JSON line on stdout, all other output to stderr.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { execSync } from 'child_process';
|
|
@@ -22,9 +25,35 @@ import {
|
|
|
22
25
|
import { getBranchPushStatus, pushBranch } from '../utils/git-operations.js';
|
|
23
26
|
import logger from '../utils/logger.js';
|
|
24
27
|
import { resolveLabels } from '../utils/label-resolver.js';
|
|
25
|
-
import {
|
|
28
|
+
import { CostTracker } from '../utils/cost-tracker.js';
|
|
29
|
+
import { error, fatal, checkGitRepo } from './helpers.js';
|
|
26
30
|
import { recordMetric } from '../utils/metrics.js';
|
|
27
31
|
|
|
32
|
+
// ─── JSON Helpers ────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Emit error JSON to stdout and set exit code
|
|
36
|
+
* @param {string} message - Error message
|
|
37
|
+
* @param {number} exitCode - Process exit code (default: 1)
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
function _emitErrorJSON(message, exitCode = 1) {
|
|
41
|
+
process.stdout.write(`${JSON.stringify({ status: 'error', error: message })}\n`);
|
|
42
|
+
process.exitCode = exitCode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Emit JSON result to stdout (does not exit — caller handles exit)
|
|
47
|
+
* @param {Object} payload - JSON payload
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
function _emitJSON(payload) {
|
|
51
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Known flags that should not be treated as branch names
|
|
55
|
+
const KNOWN_FLAGS = new Set(['--headless', '--format']);
|
|
56
|
+
|
|
28
57
|
/**
|
|
29
58
|
* Detect expected merge strategy from branch naming conventions
|
|
30
59
|
* @param {string} sourceBranch - Source (head) branch name
|
|
@@ -45,9 +74,33 @@ function detectMergeStrategy(sourceBranch, targetBranch) {
|
|
|
45
74
|
* @param {Array<string>} args - Command arguments
|
|
46
75
|
*/
|
|
47
76
|
export async function runCreatePr(args) {
|
|
48
|
-
|
|
77
|
+
// ── Parse headless/format flags ──
|
|
78
|
+
const headless = args.includes('--headless');
|
|
79
|
+
const fmtIdx = args.indexOf('--format');
|
|
80
|
+
const format = fmtIdx >= 0 ? args[fmtIdx + 1] : null;
|
|
81
|
+
const isJSON = format === 'json';
|
|
82
|
+
if (format && !isJSON) {
|
|
83
|
+
logger.warning(`Unknown --format value '${format}'. Only 'json' is supported.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Disallowed combo: --format json without --headless
|
|
87
|
+
if (isJSON && !headless) {
|
|
88
|
+
_emitErrorJSON('--format json requires --headless', 2);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Activate JSON mode before any output so info/warning route to stderr
|
|
93
|
+
if (isJSON) logger.setJSONMode(true);
|
|
94
|
+
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
const costTracker = headless ? new CostTracker() : null;
|
|
97
|
+
let branchWasPushed = false;
|
|
98
|
+
let pushedTags = [];
|
|
99
|
+
|
|
100
|
+
logger.debug('create-pr', 'Starting create-pr command', { args, headless, isJSON });
|
|
49
101
|
|
|
50
102
|
if (!checkGitRepo()) {
|
|
103
|
+
if (isJSON) { _emitErrorJSON('Not a git repository'); return; }
|
|
51
104
|
error('You are not in a Git repository.');
|
|
52
105
|
logger.debug('create-pr', 'Not in a git repository, exiting');
|
|
53
106
|
return;
|
|
@@ -71,7 +124,7 @@ export async function runCreatePr(args) {
|
|
|
71
124
|
const { parseGitHubRepo } = await import('../utils/github-client.js');
|
|
72
125
|
|
|
73
126
|
showInfo('🚀 Creating Pull Request...');
|
|
74
|
-
console.log('');
|
|
127
|
+
if (!isJSON) console.log('');
|
|
75
128
|
|
|
76
129
|
// Step 1: Validate GitHub token
|
|
77
130
|
logger.debug('create-pr', 'Step 1: Validating GitHub token');
|
|
@@ -80,6 +133,10 @@ export async function runCreatePr(args) {
|
|
|
80
133
|
logger.error('create-pr', 'GitHub authentication failed', {
|
|
81
134
|
error: tokenValidation.error
|
|
82
135
|
});
|
|
136
|
+
if (isJSON) {
|
|
137
|
+
_emitErrorJSON('GitHub authentication failed. Run: claude-hooks setup-github', 2);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
83
140
|
showError('GitHub authentication failed');
|
|
84
141
|
console.log('');
|
|
85
142
|
console.log('Please configure your GitHub token:');
|
|
@@ -102,39 +159,49 @@ export async function runCreatePr(args) {
|
|
|
102
159
|
showWarning('Token may lack "repo" scope - PR creation might fail');
|
|
103
160
|
}
|
|
104
161
|
|
|
105
|
-
// Step 2:
|
|
106
|
-
logger.debug('create-pr', 'Step 2: Getting
|
|
162
|
+
// Step 2: Detect current branch (fail fast before interactive prompts)
|
|
163
|
+
logger.debug('create-pr', 'Step 2: Getting current branch and repo info');
|
|
164
|
+
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
165
|
+
if (!currentBranch) {
|
|
166
|
+
if (isJSON) {
|
|
167
|
+
_emitErrorJSON('Detached HEAD detected. create-pr requires a named branch.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
fatal(
|
|
171
|
+
'Detached HEAD detected. create-pr requires a named branch.\n' +
|
|
172
|
+
' Check out a branch first: git checkout -b <branch-name>\n' +
|
|
173
|
+
' Or, if running in CI/container: configure the orchestrator to checkout\n' +
|
|
174
|
+
' the branch by name, not by SHA. See CLAUDE.md "Operational Contract".'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const repoInfo = parseGitHubRepo();
|
|
179
|
+
|
|
180
|
+
// Step 3: Get or prompt for task-id
|
|
181
|
+
// Decision #1: headless → prompt:false (null if not auto-detected from branch)
|
|
182
|
+
logger.debug('create-pr', 'Step 3: Getting or prompting for task-id');
|
|
107
183
|
const taskId = await getOrPromptTaskId({
|
|
108
|
-
prompt:
|
|
109
|
-
required: false,
|
|
110
|
-
config
|
|
184
|
+
prompt: !headless, // DO prompt for PRs (unless headless)
|
|
185
|
+
required: false,
|
|
186
|
+
config
|
|
111
187
|
});
|
|
112
188
|
logger.debug('create-pr', 'Task ID determined', { taskId });
|
|
113
189
|
|
|
114
|
-
// Step
|
|
115
|
-
|
|
190
|
+
// Step 4: Parse arguments and determine base branch
|
|
191
|
+
// Filter out known flags and their values from args before resolving base branch
|
|
192
|
+
logger.debug('create-pr', 'Step 4: Parsing arguments and determining base branch', {
|
|
116
193
|
args
|
|
117
194
|
});
|
|
118
|
-
|
|
195
|
+
const cleanArgs = args.filter((a, i) => {
|
|
196
|
+
if (KNOWN_FLAGS.has(a)) return false;
|
|
197
|
+
if (i > 0 && args[i - 1] === '--format') return false;
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
let baseBranchArg = cleanArgs[0];
|
|
119
201
|
if (baseBranchArg && /^[A-Z]{2,10}-\d+$/i.test(baseBranchArg)) {
|
|
120
|
-
baseBranchArg =
|
|
202
|
+
baseBranchArg = cleanArgs[1];
|
|
121
203
|
}
|
|
122
204
|
const baseBranch = baseBranchArg || config.github?.pr?.defaultBase || 'develop';
|
|
123
|
-
logger.debug('create-pr', 'Base branch determined', {
|
|
124
|
-
baseBranch,
|
|
125
|
-
fromConfig: !baseBranchArg
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Step 4: Get current branch and repo info
|
|
129
|
-
logger.debug('create-pr', 'Step 4: Getting current branch and repo info');
|
|
130
|
-
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
131
|
-
if (!currentBranch) {
|
|
132
|
-
logger.error('create-pr', 'Could not determine current branch');
|
|
133
|
-
error('Could not determine current branch');
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const repoInfo = parseGitHubRepo();
|
|
138
205
|
logger.debug('create-pr', 'Repository and branch info', {
|
|
139
206
|
owner: repoInfo.owner,
|
|
140
207
|
repo: repoInfo.repo,
|
|
@@ -159,9 +226,24 @@ export async function runCreatePr(args) {
|
|
|
159
226
|
prNumber: existingPR.number,
|
|
160
227
|
prUrl: existingPR.html_url
|
|
161
228
|
});
|
|
229
|
+
if (isJSON) {
|
|
230
|
+
_emitJSON({
|
|
231
|
+
status: 'exists',
|
|
232
|
+
prUrl: existingPR.html_url,
|
|
233
|
+
prNumber: existingPR.number,
|
|
234
|
+
head: currentBranch,
|
|
235
|
+
base: baseBranch,
|
|
236
|
+
durationMs: Date.now() - startTime,
|
|
237
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
238
|
+
});
|
|
239
|
+
process.exit(0);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
162
242
|
showWarning(`A PR already exists for this branch: #${existingPR.number}`);
|
|
163
|
-
|
|
164
|
-
|
|
243
|
+
if (!isJSON) {
|
|
244
|
+
console.log(` ${existingPR.html_url}`);
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
165
247
|
return;
|
|
166
248
|
}
|
|
167
249
|
|
|
@@ -188,6 +270,10 @@ export async function runCreatePr(args) {
|
|
|
188
270
|
branch: currentBranch,
|
|
189
271
|
upstream: pushStatus.upstreamBranch
|
|
190
272
|
});
|
|
273
|
+
if (isJSON) {
|
|
274
|
+
_emitErrorJSON(`Branch has diverged from remote. Run: git pull origin ${currentBranch}`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
191
277
|
showError('Branch has diverged from remote');
|
|
192
278
|
console.log('');
|
|
193
279
|
console.log('Please resolve conflicts manually:');
|
|
@@ -201,6 +287,10 @@ export async function runCreatePr(args) {
|
|
|
201
287
|
logger.debug('create-pr', 'Auto-push disabled, aborting', {
|
|
202
288
|
autoPush: pushConfig?.autoPush
|
|
203
289
|
});
|
|
290
|
+
if (isJSON) {
|
|
291
|
+
_emitErrorJSON(`Branch not pushed to remote and autoPush is disabled. Run: git push -u origin ${currentBranch}`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
204
294
|
showError('Branch not pushed to remote');
|
|
205
295
|
console.log('');
|
|
206
296
|
console.log('Please push your branch first:');
|
|
@@ -210,7 +300,7 @@ export async function runCreatePr(args) {
|
|
|
210
300
|
}
|
|
211
301
|
|
|
212
302
|
// Show push preview
|
|
213
|
-
if (pushConfig?.showCommits && pushStatus.unpushedCommits.length > 0) {
|
|
303
|
+
if (!isJSON && pushConfig?.showCommits && pushStatus.unpushedCommits.length > 0) {
|
|
214
304
|
console.log('');
|
|
215
305
|
showInfo('Commits to push:');
|
|
216
306
|
pushStatus.unpushedCommits.forEach((commit) => {
|
|
@@ -218,13 +308,15 @@ export async function runCreatePr(args) {
|
|
|
218
308
|
});
|
|
219
309
|
}
|
|
220
310
|
|
|
221
|
-
//
|
|
311
|
+
// Decision #2: headless → skip prompt, proceed with push
|
|
222
312
|
if (pushConfig?.pushConfirm) {
|
|
223
|
-
console.log('');
|
|
224
|
-
const shouldPush =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
313
|
+
if (!isJSON) console.log('');
|
|
314
|
+
const shouldPush = headless
|
|
315
|
+
? true
|
|
316
|
+
: await promptConfirmation(
|
|
317
|
+
`Push branch '${currentBranch}' to remote?`,
|
|
318
|
+
true
|
|
319
|
+
);
|
|
228
320
|
|
|
229
321
|
if (!shouldPush) {
|
|
230
322
|
logger.debug('create-pr', 'User declined push, aborting');
|
|
@@ -234,7 +326,7 @@ export async function runCreatePr(args) {
|
|
|
234
326
|
}
|
|
235
327
|
|
|
236
328
|
// Execute push
|
|
237
|
-
console.log('');
|
|
329
|
+
if (!isJSON) console.log('');
|
|
238
330
|
showInfo('Pushing branch to remote...');
|
|
239
331
|
logger.debug('create-pr', 'Executing push', {
|
|
240
332
|
branch: currentBranch,
|
|
@@ -250,6 +342,10 @@ export async function runCreatePr(args) {
|
|
|
250
342
|
branch: currentBranch,
|
|
251
343
|
error: pushResult.error
|
|
252
344
|
});
|
|
345
|
+
if (isJSON) {
|
|
346
|
+
_emitErrorJSON(`Failed to push branch: ${pushResult.error}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
253
349
|
showError('Failed to push branch');
|
|
254
350
|
console.error('');
|
|
255
351
|
console.error(` ${pushResult.error}`);
|
|
@@ -257,9 +353,10 @@ export async function runCreatePr(args) {
|
|
|
257
353
|
process.exit(1);
|
|
258
354
|
}
|
|
259
355
|
|
|
356
|
+
branchWasPushed = true;
|
|
260
357
|
logger.debug('create-pr', 'Push completed successfully');
|
|
261
358
|
showSuccess('Branch pushed successfully');
|
|
262
|
-
console.log('');
|
|
359
|
+
if (!isJSON) console.log('');
|
|
263
360
|
} else {
|
|
264
361
|
logger.debug('create-pr', 'Branch already up-to-date with remote, skipping push');
|
|
265
362
|
}
|
|
@@ -271,21 +368,28 @@ export async function runCreatePr(args) {
|
|
|
271
368
|
|
|
272
369
|
if (!versionCheck.aligned) {
|
|
273
370
|
showWarning('Version misalignment detected:');
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
371
|
+
if (!isJSON) {
|
|
372
|
+
console.log('');
|
|
373
|
+
for (const issue of versionCheck.issues) {
|
|
374
|
+
console.log(` ${issue.source}: ${issue.version || 'not found'}`);
|
|
375
|
+
}
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log('To fix version alignment:');
|
|
378
|
+
console.log(' claude-hooks bump-version patch --update-changelog');
|
|
379
|
+
console.log('');
|
|
278
380
|
}
|
|
279
381
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
382
|
+
// Decision #3: headless → continue + log warning
|
|
383
|
+
const shouldContinue = headless
|
|
384
|
+
? true
|
|
385
|
+
: await promptConfirmation(
|
|
386
|
+
'Continue creating PR despite version misalignment?',
|
|
387
|
+
false
|
|
388
|
+
);
|
|
284
389
|
|
|
285
|
-
|
|
286
|
-
'
|
|
287
|
-
|
|
288
|
-
);
|
|
390
|
+
if (headless) {
|
|
391
|
+
logger.warning('create-pr', 'Continuing despite version misalignment (headless)');
|
|
392
|
+
}
|
|
289
393
|
|
|
290
394
|
if (!shouldContinue) {
|
|
291
395
|
showInfo('PR creation cancelled - please align versions first');
|
|
@@ -295,17 +399,22 @@ export async function runCreatePr(args) {
|
|
|
295
399
|
|
|
296
400
|
if (versionCheck.aligned && versionCheck.comparison === 'equal') {
|
|
297
401
|
showWarning('Local version equals remote version');
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
402
|
+
if (!isJSON) {
|
|
403
|
+
console.log(` Local: ${versionCheck.localVersion}`);
|
|
404
|
+
console.log(` Remote: ${versionCheck.remoteVersion}`);
|
|
405
|
+
console.log('');
|
|
406
|
+
console.log('To bump version:');
|
|
407
|
+
console.log(' claude-hooks bump-version patch # or minor/major');
|
|
408
|
+
console.log('');
|
|
409
|
+
}
|
|
304
410
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
true
|
|
308
|
-
|
|
411
|
+
// Decision #4: headless → continue (matches default 'yes')
|
|
412
|
+
const shouldContinue = headless
|
|
413
|
+
? true
|
|
414
|
+
: await promptConfirmation(
|
|
415
|
+
'Continue creating PR without version bump?',
|
|
416
|
+
true
|
|
417
|
+
);
|
|
309
418
|
|
|
310
419
|
if (!shouldContinue) {
|
|
311
420
|
showInfo('PR creation cancelled');
|
|
@@ -315,16 +424,25 @@ export async function runCreatePr(args) {
|
|
|
315
424
|
|
|
316
425
|
if (versionCheck.aligned && versionCheck.comparison === 'less') {
|
|
317
426
|
showError('Local version is less than remote version!');
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
427
|
+
if (!isJSON) {
|
|
428
|
+
console.log(` Local: ${versionCheck.localVersion}`);
|
|
429
|
+
console.log(` Remote: ${versionCheck.remoteVersion}`);
|
|
430
|
+
console.log('');
|
|
431
|
+
console.log('This usually means you need to pull latest changes:');
|
|
432
|
+
console.log(` git pull origin ${baseBranch}`);
|
|
433
|
+
console.log('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Decision #5: headless → ABORT (dangerous; needs human review)
|
|
437
|
+
if (headless) {
|
|
438
|
+
const msg = `Local version (${versionCheck.localVersion}) is behind remote (${versionCheck.remoteVersion}). Run: git pull origin ${baseBranch}`;
|
|
439
|
+
if (isJSON) { _emitErrorJSON(msg); return; }
|
|
440
|
+
fatal(msg);
|
|
441
|
+
}
|
|
324
442
|
|
|
325
443
|
const shouldContinue = await promptConfirmation(
|
|
326
444
|
'Continue creating PR despite version being behind?',
|
|
327
|
-
false
|
|
445
|
+
false
|
|
328
446
|
);
|
|
329
447
|
|
|
330
448
|
if (!shouldContinue) {
|
|
@@ -362,7 +480,7 @@ export async function runCreatePr(args) {
|
|
|
362
480
|
unpushedTags: tagComparison.localNewer
|
|
363
481
|
});
|
|
364
482
|
|
|
365
|
-
let
|
|
483
|
+
let shouldPushTags = false;
|
|
366
484
|
let userChoice = null;
|
|
367
485
|
|
|
368
486
|
// Case 1: Local tag > Remote tag → Auto-push
|
|
@@ -374,7 +492,7 @@ export async function runCreatePr(args) {
|
|
|
374
492
|
|
|
375
493
|
showInfo(`Local tag ${latestLocalTag} is newer than remote ${latestRemoteTag}`);
|
|
376
494
|
showInfo('Auto-pushing tag to remote...');
|
|
377
|
-
|
|
495
|
+
shouldPushTags = true;
|
|
378
496
|
|
|
379
497
|
// Case 2: Local tag = Remote tag → Prompt with warning
|
|
380
498
|
} else if (
|
|
@@ -388,27 +506,32 @@ export async function runCreatePr(args) {
|
|
|
388
506
|
});
|
|
389
507
|
|
|
390
508
|
showWarning('Local and remote tags have the same version:');
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
509
|
+
if (!isJSON) {
|
|
510
|
+
console.log(` Local: ${latestLocalTag} (${localVersion})`);
|
|
511
|
+
console.log(` Remote: ${latestRemoteTag} (${remoteVersion})`);
|
|
512
|
+
console.log('');
|
|
513
|
+
console.log('This might indicate the tag was already pushed.');
|
|
514
|
+
console.log('');
|
|
515
|
+
}
|
|
396
516
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
517
|
+
// Decision #6: headless → continue without push (default 'c')
|
|
518
|
+
userChoice = headless
|
|
519
|
+
? 'c'
|
|
520
|
+
: await promptMenu(
|
|
521
|
+
'What would you like to do?',
|
|
522
|
+
[
|
|
523
|
+
{ key: 'c', label: 'Continue without pushing' },
|
|
524
|
+
{ key: 'p', label: 'Force push tag' },
|
|
525
|
+
{ key: 'a', label: 'Abort PR creation' }
|
|
526
|
+
],
|
|
527
|
+
'c'
|
|
528
|
+
);
|
|
406
529
|
|
|
407
530
|
if (userChoice === 'a') {
|
|
408
531
|
showInfo('PR creation cancelled');
|
|
409
532
|
process.exit(0);
|
|
410
533
|
} else if (userChoice === 'p') {
|
|
411
|
-
|
|
534
|
+
shouldPushTags = true;
|
|
412
535
|
}
|
|
413
536
|
|
|
414
537
|
// Case 3: Local tag < Remote tag → Prompt with error
|
|
@@ -423,12 +546,21 @@ export async function runCreatePr(args) {
|
|
|
423
546
|
});
|
|
424
547
|
|
|
425
548
|
showError('Local tag is older than remote tag:');
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
549
|
+
if (!isJSON) {
|
|
550
|
+
console.log(` Local: ${latestLocalTag} (${localVersion})`);
|
|
551
|
+
console.log(` Remote: ${latestRemoteTag} (${remoteVersion})`);
|
|
552
|
+
console.log('');
|
|
553
|
+
console.log('This usually means you need to pull latest tags:');
|
|
554
|
+
console.log(' git fetch --tags');
|
|
555
|
+
console.log('');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Decision #7: headless → ABORT
|
|
559
|
+
if (headless) {
|
|
560
|
+
const msg = `Local tag (${localVersion}) is older than remote (${remoteVersion}). Run: git fetch --tags`;
|
|
561
|
+
if (isJSON) { _emitErrorJSON(msg); return; }
|
|
562
|
+
fatal(msg);
|
|
563
|
+
}
|
|
432
564
|
|
|
433
565
|
userChoice = await promptMenu(
|
|
434
566
|
'What would you like to do?',
|
|
@@ -454,49 +586,62 @@ export async function runCreatePr(args) {
|
|
|
454
586
|
});
|
|
455
587
|
|
|
456
588
|
showWarning('Unable to compare tag versions:');
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
589
|
+
if (!isJSON) {
|
|
590
|
+
console.log(` Local tag: ${latestLocalTag || 'none'}`);
|
|
591
|
+
console.log(` Remote tag: ${latestRemoteTag || 'none'}`);
|
|
592
|
+
console.log('');
|
|
593
|
+
console.log('Unpushed tags:', tagComparison.localNewer.join(', '));
|
|
594
|
+
console.log('');
|
|
595
|
+
}
|
|
462
596
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
597
|
+
// Decision #8: headless → continue without push
|
|
598
|
+
userChoice = headless
|
|
599
|
+
? 'c'
|
|
600
|
+
: await promptMenu(
|
|
601
|
+
'What would you like to do?',
|
|
602
|
+
[
|
|
603
|
+
{ key: 'p', label: 'Push tags to remote' },
|
|
604
|
+
{ key: 'c', label: 'Continue without pushing' },
|
|
605
|
+
{ key: 'a', label: 'Abort PR creation' }
|
|
606
|
+
],
|
|
607
|
+
'p'
|
|
608
|
+
);
|
|
472
609
|
|
|
473
610
|
if (userChoice === 'a') {
|
|
474
611
|
showInfo('PR creation cancelled');
|
|
475
612
|
process.exit(0);
|
|
476
613
|
} else if (userChoice === 'p') {
|
|
477
|
-
|
|
614
|
+
shouldPushTags = true;
|
|
478
615
|
}
|
|
479
616
|
}
|
|
480
617
|
|
|
481
|
-
// Execute push if decided
|
|
482
|
-
if (
|
|
483
|
-
console.log('');
|
|
618
|
+
// Execute tag push if decided
|
|
619
|
+
if (shouldPushTags) {
|
|
620
|
+
if (!isJSON) console.log('');
|
|
484
621
|
showInfo('Pushing tags to remote...');
|
|
485
622
|
|
|
486
623
|
const pushResult = pushTagsUtil(null, tagComparison.localNewer);
|
|
487
624
|
|
|
488
625
|
if (pushResult.success) {
|
|
626
|
+
pushedTags = tagComparison.localNewer;
|
|
489
627
|
showSuccess('✓ Tags pushed successfully');
|
|
490
628
|
logger.debug('create-pr', 'Tags pushed', {
|
|
491
629
|
pushed: tagComparison.localNewer
|
|
492
630
|
});
|
|
493
631
|
} else {
|
|
494
632
|
showError(`Failed to push some tags: ${pushResult.error}`);
|
|
495
|
-
if (pushResult.failed.length > 0) {
|
|
633
|
+
if (!isJSON && pushResult.failed.length > 0) {
|
|
496
634
|
console.log('Failed tags:');
|
|
497
635
|
pushResult.failed.forEach((f) => console.log(` - ${f.tag}: ${f.error}`));
|
|
498
636
|
}
|
|
499
637
|
|
|
638
|
+
// Decision #9: headless → ABORT
|
|
639
|
+
if (headless) {
|
|
640
|
+
const msg = `Failed to push tags: ${pushResult.error}`;
|
|
641
|
+
if (isJSON) { _emitErrorJSON(msg); return; }
|
|
642
|
+
fatal(msg);
|
|
643
|
+
}
|
|
644
|
+
|
|
500
645
|
const shouldContinue = await promptConfirmation(
|
|
501
646
|
'Continue creating PR despite push failure?',
|
|
502
647
|
false
|
|
@@ -508,13 +653,12 @@ export async function runCreatePr(args) {
|
|
|
508
653
|
}
|
|
509
654
|
}
|
|
510
655
|
|
|
511
|
-
console.log('');
|
|
656
|
+
if (!isJSON) console.log('');
|
|
512
657
|
} else if (userChoice !== 'c') {
|
|
513
|
-
// Auto-push failed or user chose to continue
|
|
514
658
|
logger.debug('create-pr', 'Tag push skipped', {
|
|
515
659
|
reason: 'user choice or condition'
|
|
516
660
|
});
|
|
517
|
-
console.log('');
|
|
661
|
+
if (!isJSON) console.log('');
|
|
518
662
|
}
|
|
519
663
|
} else {
|
|
520
664
|
logger.debug('create-pr', 'No unpushed tags found, continuing');
|
|
@@ -529,11 +673,17 @@ export async function runCreatePr(args) {
|
|
|
529
673
|
result: analysisResult,
|
|
530
674
|
error: engineError
|
|
531
675
|
} = await analyzeBranchForPR(baseBranch, {
|
|
532
|
-
hook: 'create-pr'
|
|
676
|
+
hook: 'create-pr',
|
|
677
|
+
headless,
|
|
678
|
+
costTracker
|
|
533
679
|
});
|
|
534
680
|
|
|
535
681
|
if (!engineSuccess) {
|
|
536
682
|
logger.error('create-pr', 'Failed to generate PR metadata', { error: engineError });
|
|
683
|
+
if (isJSON) {
|
|
684
|
+
_emitErrorJSON(engineError || 'Failed to generate PR metadata');
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
537
687
|
error(engineError || 'Failed to generate PR metadata');
|
|
538
688
|
return;
|
|
539
689
|
}
|
|
@@ -568,18 +718,25 @@ export async function runCreatePr(args) {
|
|
|
568
718
|
|
|
569
719
|
if (mergeStrategy === 'unknown') {
|
|
570
720
|
showWarning('Unknown branch pattern — cannot auto-detect merge strategy');
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
721
|
+
|
|
722
|
+
// Decision #10: headless → default to 'squash'
|
|
723
|
+
if (headless) {
|
|
724
|
+
mergeStrategy = 'squash';
|
|
725
|
+
logger.warning('create-pr', 'Defaulting to squash merge strategy (headless)');
|
|
726
|
+
} else {
|
|
727
|
+
if (!isJSON) console.log('');
|
|
728
|
+
const strategyChoice = await promptMenu(
|
|
729
|
+
'Select merge strategy:',
|
|
730
|
+
[
|
|
731
|
+
{ key: 's', label: 'Squash merge' },
|
|
732
|
+
{ key: 'm', label: 'Merge commit' },
|
|
733
|
+
{ key: 'k', label: 'Skip (no strategy label)' }
|
|
734
|
+
],
|
|
735
|
+
'k'
|
|
736
|
+
);
|
|
737
|
+
if (strategyChoice === 's') mergeStrategy = 'squash';
|
|
738
|
+
else if (strategyChoice === 'm') mergeStrategy = 'merge-commit';
|
|
739
|
+
}
|
|
583
740
|
}
|
|
584
741
|
|
|
585
742
|
// Step 9.5: Resolve labels
|
|
@@ -633,7 +790,7 @@ export async function runCreatePr(args) {
|
|
|
633
790
|
reviewers
|
|
634
791
|
};
|
|
635
792
|
|
|
636
|
-
showPRPreview(prData);
|
|
793
|
+
if (!isJSON) showPRPreview(prData);
|
|
637
794
|
|
|
638
795
|
let strategyLabel;
|
|
639
796
|
if (mergeStrategy === 'squash') {
|
|
@@ -646,14 +803,17 @@ export async function runCreatePr(args) {
|
|
|
646
803
|
showInfo(`Merge strategy: ${strategyLabel} (${currentBranch} → ${baseBranch})`);
|
|
647
804
|
|
|
648
805
|
// Step 12: Prompt for confirmation
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
806
|
+
// Decision #11: headless → always create
|
|
807
|
+
const action = headless
|
|
808
|
+
? 'c'
|
|
809
|
+
: await promptMenu(
|
|
810
|
+
'What would you like to do?',
|
|
811
|
+
[
|
|
812
|
+
{ key: 'c', label: 'Create PR' },
|
|
813
|
+
{ key: 'x', label: 'Cancel' }
|
|
814
|
+
],
|
|
815
|
+
'c'
|
|
816
|
+
);
|
|
657
817
|
|
|
658
818
|
if (action === 'x') {
|
|
659
819
|
showInfo('PR creation cancelled');
|
|
@@ -710,6 +870,29 @@ export async function runCreatePr(args) {
|
|
|
710
870
|
prNumber: result.number
|
|
711
871
|
}).catch(() => {});
|
|
712
872
|
|
|
873
|
+
if (isJSON) {
|
|
874
|
+
_emitJSON({
|
|
875
|
+
status: 'created',
|
|
876
|
+
prUrl: result.html_url,
|
|
877
|
+
prNumber: result.number,
|
|
878
|
+
title: prData.title,
|
|
879
|
+
body: prData.body,
|
|
880
|
+
head: prData.head,
|
|
881
|
+
base: prData.base,
|
|
882
|
+
mergeStrategy,
|
|
883
|
+
labels: prData.labels,
|
|
884
|
+
reviewers: prData.reviewers,
|
|
885
|
+
taskId: taskId || null,
|
|
886
|
+
pushed: branchWasPushed,
|
|
887
|
+
tagsPushed: pushedTags,
|
|
888
|
+
fileCount: filesArray.length,
|
|
889
|
+
durationMs: Date.now() - startTime,
|
|
890
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
891
|
+
});
|
|
892
|
+
process.exit(0);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
713
896
|
console.log('');
|
|
714
897
|
showSuccess('Pull request created successfully!');
|
|
715
898
|
console.log('');
|
|
@@ -724,6 +907,21 @@ export async function runCreatePr(args) {
|
|
|
724
907
|
}
|
|
725
908
|
} catch (apiError) {
|
|
726
909
|
logger.error('create-pr', 'Failed to create pull request', apiError);
|
|
910
|
+
|
|
911
|
+
if (isJSON) {
|
|
912
|
+
_emitJSON({
|
|
913
|
+
status: 'error',
|
|
914
|
+
error: apiError.message,
|
|
915
|
+
errorName: apiError.name || 'Error',
|
|
916
|
+
head: prData.head,
|
|
917
|
+
base: prData.base,
|
|
918
|
+
durationMs: Date.now() - startTime,
|
|
919
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
920
|
+
});
|
|
921
|
+
process.exit(1);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
727
925
|
showError('Failed to create pull request');
|
|
728
926
|
console.error('');
|
|
729
927
|
console.error(` ${apiError.message}`);
|
|
@@ -761,6 +959,19 @@ export async function runCreatePr(args) {
|
|
|
761
959
|
}
|
|
762
960
|
} catch (err) {
|
|
763
961
|
logger.error('create-pr', 'Error creating PR', err);
|
|
962
|
+
|
|
963
|
+
if (isJSON) {
|
|
964
|
+
_emitJSON({
|
|
965
|
+
status: 'error',
|
|
966
|
+
error: err.message,
|
|
967
|
+
errorName: err.name || 'Error',
|
|
968
|
+
durationMs: Date.now() - startTime,
|
|
969
|
+
costs: costTracker ? costTracker.toJSON() : null
|
|
970
|
+
});
|
|
971
|
+
process.exit(1);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
764
975
|
showError(`Error creating PR: ${err.message}`);
|
|
765
976
|
|
|
766
977
|
if (err.context) {
|