claude-git-hooks 2.51.2 → 2.66.1
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 +513 -30
- package/lib/commands/back-merge.js +91 -11
- package/lib/commands/bump-version.js +72 -76
- package/lib/commands/create-pr.js +124 -44
- package/lib/commands/create-release.js +198 -52
- package/lib/commands/help.js +13 -3
- package/lib/messages/library-warnings.js +147 -0
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/git-tag-manager.js +104 -0
- package/lib/utils/github-api.js +30 -0
- package/lib/utils/judge.js +2 -1
- package/lib/utils/linter-runner.js +6 -0
- package/lib/utils/version-manager.js +30 -17
- package/package.json +85 -83
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Workflow:
|
|
6
6
|
* 1. Validate preconditions (clean tree, on develop, up-to-date, no existing RC)
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
11
|
-
* 6.
|
|
12
|
-
* 7.
|
|
13
|
-
* 8.
|
|
14
|
-
* 9.
|
|
15
|
-
* 10.
|
|
16
|
-
* 11.
|
|
17
|
-
* 12.
|
|
18
|
-
* 13.
|
|
7
|
+
* 2. Library verification gate (non-blocking)
|
|
8
|
+
* 3. Discover version files — abort on mismatch
|
|
9
|
+
* 4. Calculate next version
|
|
10
|
+
* 5. [dry-run] Preview + return
|
|
11
|
+
* 6. Confirm with user
|
|
12
|
+
* 7. Create RC branch from develop
|
|
13
|
+
* 8. Bump version files
|
|
14
|
+
* 9. [if stale] Library regeneration (keeps tag accurate)
|
|
15
|
+
* 10. [optional] Generate CHANGELOG
|
|
16
|
+
* 11. Stage all + commit (--no-verify)
|
|
17
|
+
* 12. Create Git tag (skip if already exists)
|
|
18
|
+
* 13. Push RC branch (unless --skip-push)
|
|
19
|
+
* 14. Create release PR (unless --skip-push or push failed)
|
|
20
|
+
* 15. Deploy to shadow (unless --no-shadow or --skip-push)
|
|
21
|
+
* 16. Return to RC branch + display summary
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
import { execSync } from 'child_process';
|
|
@@ -57,6 +60,12 @@ import {
|
|
|
57
60
|
} from '../utils/interactive-ui.js';
|
|
58
61
|
import logger from '../utils/logger.js';
|
|
59
62
|
import { colors, error, checkGitRepo } from './helpers.js';
|
|
63
|
+
import {
|
|
64
|
+
CONSOLE_WARNING_TEMPLATE,
|
|
65
|
+
PR_BODY_SECTION_TEMPLATE,
|
|
66
|
+
PR_TAG_VALUE,
|
|
67
|
+
LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE
|
|
68
|
+
} from '../messages/library-warnings.js';
|
|
60
69
|
|
|
61
70
|
/** Source branch for RC creation */
|
|
62
71
|
const SOURCE_BRANCH = 'develop';
|
|
@@ -264,6 +273,51 @@ function restoreSnapshot(snapshot) {
|
|
|
264
273
|
});
|
|
265
274
|
}
|
|
266
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Emit a loud Library staleness warning to stderr.
|
|
278
|
+
* Content comes from the canonical template + verify result data.
|
|
279
|
+
* Formatting uses ANSI codes for visual emphasis on TTY stderr.
|
|
280
|
+
*
|
|
281
|
+
* @param {import('../../.library/librarian/index.js').VerifyResult} result
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
function _emitLibraryWarning(result) {
|
|
285
|
+
const y = colors.yellow;
|
|
286
|
+
const r = colors.reset;
|
|
287
|
+
const bar = `${y}${'='.repeat(63)}${r}`;
|
|
288
|
+
const content = CONSOLE_WARNING_TEMPLATE(result, { autoRegen: 'will-run' });
|
|
289
|
+
|
|
290
|
+
const lines = ['', bar, '', `${y}${content}${r}`, '', bar, ''];
|
|
291
|
+
|
|
292
|
+
process.stderr.write(lines.join('\n'));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Marker comment pair wrapping the staleness section in PR bodies */
|
|
296
|
+
const STALENESS_MARKER_OPEN = '<!-- LIBRARY_STALENESS_SECTION -->';
|
|
297
|
+
const STALENESS_MARKER_CLOSE = '<!-- /LIBRARY_STALENESS_SECTION -->';
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Build the release PR body, optionally appending the staleness section.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} nextVersion
|
|
303
|
+
* @param {import('../../.library/librarian/index.js').VerifyResult|null} verifyResult
|
|
304
|
+
* @param {'completed'|'failed'|null} regenOutcome - result of auto-regeneration
|
|
305
|
+
* @returns {string}
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
function _buildReleasePrBody(nextVersion, verifyResult, regenOutcome) {
|
|
309
|
+
let body = `## Release V${nextVersion}\n\nRelease-candidate branch created from \`${SOURCE_BRANCH}\`.`;
|
|
310
|
+
|
|
311
|
+
if (verifyResult && !verifyResult.clean) {
|
|
312
|
+
const section = PR_BODY_SECTION_TEMPLATE(verifyResult, {
|
|
313
|
+
autoRegen: regenOutcome || 'failed'
|
|
314
|
+
});
|
|
315
|
+
body += `\n\n${STALENESS_MARKER_OPEN}\n${section}\n${STALENESS_MARKER_CLOSE}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return body;
|
|
319
|
+
}
|
|
320
|
+
|
|
267
321
|
/**
|
|
268
322
|
* create-release command
|
|
269
323
|
* Creates a release-candidate branch from develop, bumps version, and deploys to shadow.
|
|
@@ -318,8 +372,25 @@ export async function runCreateRelease(args) {
|
|
|
318
372
|
showSuccess('Preconditions validated');
|
|
319
373
|
console.log('');
|
|
320
374
|
|
|
321
|
-
// Step 2:
|
|
322
|
-
|
|
375
|
+
// Step 2: Library verification gate — silent on clean, loud-warn on stale, never blocks
|
|
376
|
+
let verifyResult = null;
|
|
377
|
+
logger.debug('create-release', 'Step 2: Running Library verification gate');
|
|
378
|
+
try {
|
|
379
|
+
const { verify } = await import('../../.library/librarian/index.js');
|
|
380
|
+
verifyResult = await verify();
|
|
381
|
+
|
|
382
|
+
if (verifyResult.clean) {
|
|
383
|
+
logger.debug('create-release', 'Library is clean — no warning needed');
|
|
384
|
+
} else {
|
|
385
|
+
_emitLibraryWarning(verifyResult);
|
|
386
|
+
}
|
|
387
|
+
} catch (verifyErr) {
|
|
388
|
+
const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE} ${verifyErr.message}${colors.reset}\n\n`;
|
|
389
|
+
process.stderr.write(msg);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Step 3: Discover version files
|
|
393
|
+
logger.debug('create-release', 'Step 3: Discovering version files');
|
|
323
394
|
const discovery = discoverVersionFiles();
|
|
324
395
|
|
|
325
396
|
if (discovery.files.length === 0) {
|
|
@@ -356,7 +427,7 @@ export async function runCreateRelease(args) {
|
|
|
356
427
|
process.exit(1);
|
|
357
428
|
}
|
|
358
429
|
|
|
359
|
-
// Step
|
|
430
|
+
// Step 4: Calculate next version
|
|
360
431
|
const nextVersion = incrementVersion(currentVersion, options.bumpType);
|
|
361
432
|
const rcBranch = `${RC_PREFIX}/V${nextVersion}`;
|
|
362
433
|
const tagName = formatTagName(nextVersion);
|
|
@@ -366,13 +437,13 @@ export async function runCreateRelease(args) {
|
|
|
366
437
|
showInfo(`Branch: ${rcBranch}`);
|
|
367
438
|
console.log('');
|
|
368
439
|
|
|
369
|
-
// Step
|
|
440
|
+
// Step 5: Dry-run
|
|
370
441
|
if (options.dryRun) {
|
|
371
442
|
showDryRunPreview({ bumpType: options.bumpType, currentVersion, nextVersion, rcBranch, options });
|
|
372
443
|
return;
|
|
373
444
|
}
|
|
374
445
|
|
|
375
|
-
// Step
|
|
446
|
+
// Step 6: Confirm with user
|
|
376
447
|
const confirmed = await promptConfirmation(
|
|
377
448
|
`Create ${rcBranch} and bump version to ${nextVersion}?`,
|
|
378
449
|
true
|
|
@@ -388,30 +459,8 @@ export async function runCreateRelease(args) {
|
|
|
388
459
|
const config = await getConfig();
|
|
389
460
|
const selectedFiles = discovery.files.filter((f) => f.selected);
|
|
390
461
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
showInfo('📚 Checking library staleness...');
|
|
394
|
-
const { runAll, formatHuman } = await import('../../.library/tools/staleness.js');
|
|
395
|
-
const { getSourceDir, getBooksDir } = await import('../../.library/paths.js');
|
|
396
|
-
const { getRepoRoot } = await import('../utils/git-operations.js');
|
|
397
|
-
const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
|
|
398
|
-
const hasDrift = stalenessResult.stale.length > 0 ||
|
|
399
|
-
stalenessResult.orphan_books.length > 0 ||
|
|
400
|
-
stalenessResult.unbooked_sources.length > 0;
|
|
401
|
-
if (hasDrift) {
|
|
402
|
-
showError('📚 Library staleness detected — cannot create release with stale books');
|
|
403
|
-
process.stdout.write(formatHuman(stalenessResult));
|
|
404
|
-
showError('Run: npm run library:regenerate');
|
|
405
|
-
process.exit(1);
|
|
406
|
-
}
|
|
407
|
-
showSuccess('📚 Library books are current');
|
|
408
|
-
} catch (err) {
|
|
409
|
-
showWarning('📚 Library staleness check unavailable — .library/ tools not found');
|
|
410
|
-
logger.debug('create-release', 'Library staleness check skipped', { error: err.message });
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Step 6: Create RC branch from develop
|
|
414
|
-
logger.debug('create-release', 'Step 6: Creating RC branch', { rcBranch });
|
|
462
|
+
// Step 7: Create RC branch from develop
|
|
463
|
+
logger.debug('create-release', 'Step 7: Creating RC branch', { rcBranch });
|
|
415
464
|
showInfo(`Creating branch ${rcBranch} from ${SOURCE_BRANCH}...`);
|
|
416
465
|
checkoutBranch(rcBranch, { create: true, startPoint: SOURCE_BRANCH });
|
|
417
466
|
showSuccess(`✓ Branch created: ${rcBranch}`);
|
|
@@ -430,8 +479,8 @@ export async function runCreateRelease(args) {
|
|
|
430
479
|
}
|
|
431
480
|
});
|
|
432
481
|
|
|
433
|
-
// Step
|
|
434
|
-
logger.debug('create-release', 'Step
|
|
482
|
+
// Step 8: Bump version files
|
|
483
|
+
logger.debug('create-release', 'Step 8: Bumping version files');
|
|
435
484
|
showInfo('Updating version files...');
|
|
436
485
|
try {
|
|
437
486
|
updateVersionFiles(selectedFiles, nextVersion);
|
|
@@ -455,10 +504,41 @@ export async function runCreateRelease(args) {
|
|
|
455
504
|
process.exit(1);
|
|
456
505
|
}
|
|
457
506
|
|
|
458
|
-
// Step
|
|
507
|
+
// Step 9: Library regeneration (if stale — keeps tag accurate)
|
|
508
|
+
let libraryFiles = [];
|
|
509
|
+
let regenOutcome = null;
|
|
510
|
+
if (verifyResult && !verifyResult.clean) {
|
|
511
|
+
logger.debug('create-release', 'Step 9: Running Library regeneration');
|
|
512
|
+
showInfo('📚 Regenerating stale Library books...');
|
|
513
|
+
try {
|
|
514
|
+
const { createPrPipeline } = await import('../../.library/librarian/index.js');
|
|
515
|
+
const root = getRepoRoot();
|
|
516
|
+
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
517
|
+
|
|
518
|
+
libraryFiles = pipelineSummary.modifiedFiles.map(
|
|
519
|
+
(f) => path.join(root, f)
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
regenOutcome = 'completed';
|
|
523
|
+
if (libraryFiles.length > 0) {
|
|
524
|
+
showSuccess(`✓ Library regenerated (${libraryFiles.length} file(s))`);
|
|
525
|
+
} else {
|
|
526
|
+
showInfo('Library pipeline ran — no files changed');
|
|
527
|
+
}
|
|
528
|
+
} catch (regenErr) {
|
|
529
|
+
regenOutcome = 'failed';
|
|
530
|
+
showWarning(`Library regeneration failed: ${regenErr.message}`);
|
|
531
|
+
logger.debug('create-release', 'Library regen failed', {
|
|
532
|
+
error: regenErr.message
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
process.stdout.write('\n');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Step 10: Optional CHANGELOG
|
|
459
539
|
let selectedChangelogPath = null;
|
|
460
540
|
if (options.updateChangelog) {
|
|
461
|
-
logger.debug('create-release', 'Step
|
|
541
|
+
logger.debug('create-release', 'Step 10: Generating CHANGELOG');
|
|
462
542
|
showInfo('Generating CHANGELOG entry...');
|
|
463
543
|
const changelogResult = await generateChangelogEntry({
|
|
464
544
|
version: nextVersion,
|
|
@@ -484,6 +564,9 @@ export async function runCreateRelease(args) {
|
|
|
484
564
|
logger.debug('create-release', 'Staging and committing version bump');
|
|
485
565
|
showInfo('Staging and committing...');
|
|
486
566
|
const filesToStage = selectedFiles.map((f) => f.path);
|
|
567
|
+
if (libraryFiles.length > 0) {
|
|
568
|
+
filesToStage.push(...libraryFiles);
|
|
569
|
+
}
|
|
487
570
|
if (options.updateChangelog) {
|
|
488
571
|
const changelogPath =
|
|
489
572
|
selectedChangelogPath || path.join(getRepoRoot(), 'CHANGELOG.md');
|
|
@@ -518,8 +601,8 @@ export async function runCreateRelease(args) {
|
|
|
518
601
|
showSuccess('✓ Version bump committed');
|
|
519
602
|
console.log('');
|
|
520
603
|
|
|
521
|
-
// Step
|
|
522
|
-
logger.debug('create-release', 'Step
|
|
604
|
+
// Step 11: Create Git tag (skip silently if already exists)
|
|
605
|
+
logger.debug('create-release', 'Step 11: Creating Git tag', { tagName });
|
|
523
606
|
const tagAlreadyExists = await tagExists(tagName, 'local');
|
|
524
607
|
if (tagAlreadyExists) {
|
|
525
608
|
showWarning(`Tag ${tagName} already exists locally — skipping tag creation`);
|
|
@@ -533,7 +616,7 @@ export async function runCreateRelease(args) {
|
|
|
533
616
|
}
|
|
534
617
|
console.log('');
|
|
535
618
|
|
|
536
|
-
// Step
|
|
619
|
+
// Step 12: Push RC branch (unless --skip-push)
|
|
537
620
|
let pushStatus = 'skipped (--skip-push)';
|
|
538
621
|
if (!options.skipPush) {
|
|
539
622
|
showInfo(`Pushing ${rcBranch} to remote...`);
|
|
@@ -551,7 +634,66 @@ export async function runCreateRelease(args) {
|
|
|
551
634
|
console.log('');
|
|
552
635
|
}
|
|
553
636
|
|
|
554
|
-
// Step
|
|
637
|
+
// Step 13: Create release PR (unless --skip-push or push failed)
|
|
638
|
+
let prUrl = null;
|
|
639
|
+
if (!options.skipPush && pushStatus === 'pushed') {
|
|
640
|
+
logger.debug('create-release', 'Step 13: Creating release PR');
|
|
641
|
+
try {
|
|
642
|
+
const { createPullRequest, validateToken, findExistingPR } =
|
|
643
|
+
await import('../utils/github-api.js');
|
|
644
|
+
const { parseGitHubRepo } = await import('../utils/github-client.js');
|
|
645
|
+
|
|
646
|
+
const tokenValidation = await validateToken();
|
|
647
|
+
if (!tokenValidation.valid) {
|
|
648
|
+
showWarning(
|
|
649
|
+
`GitHub token invalid — release PR not created: ${tokenValidation.error}`
|
|
650
|
+
);
|
|
651
|
+
console.log('');
|
|
652
|
+
console.log('Create the PR manually:');
|
|
653
|
+
console.log(` gh pr create --base main --head ${rcBranch}`);
|
|
654
|
+
} else {
|
|
655
|
+
const repoInfo = parseGitHubRepo();
|
|
656
|
+
|
|
657
|
+
// Idempotency: skip if PR already exists for this head → main
|
|
658
|
+
const existingPR = await findExistingPR({
|
|
659
|
+
owner: repoInfo.owner,
|
|
660
|
+
repo: repoInfo.repo,
|
|
661
|
+
head: rcBranch,
|
|
662
|
+
base: 'main'
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (existingPR) {
|
|
666
|
+
showInfo(`Release PR already exists: ${existingPR.html_url}`);
|
|
667
|
+
prUrl = existingPR.html_url;
|
|
668
|
+
} else {
|
|
669
|
+
const prBody = _buildReleasePrBody(nextVersion, verifyResult, regenOutcome);
|
|
670
|
+
const labels = verifyResult && !verifyResult.clean
|
|
671
|
+
? [PR_TAG_VALUE]
|
|
672
|
+
: [];
|
|
673
|
+
|
|
674
|
+
const pr = await createPullRequest({
|
|
675
|
+
owner: repoInfo.owner,
|
|
676
|
+
repo: repoInfo.repo,
|
|
677
|
+
title: `Release V${nextVersion}`,
|
|
678
|
+
body: prBody,
|
|
679
|
+
head: rcBranch,
|
|
680
|
+
base: 'main',
|
|
681
|
+
labels
|
|
682
|
+
});
|
|
683
|
+
prUrl = pr.html_url;
|
|
684
|
+
showSuccess(`✓ Release PR created: ${prUrl}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (prErr) {
|
|
688
|
+
showWarning(`Release PR creation failed: ${prErr.message}`);
|
|
689
|
+
console.log('');
|
|
690
|
+
console.log('Create the PR manually:');
|
|
691
|
+
console.log(` gh pr create --base main --head ${rcBranch}`);
|
|
692
|
+
}
|
|
693
|
+
console.log('');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Step 14: Deploy to shadow (unless --no-shadow or --skip-push)
|
|
555
697
|
let shadowStatus = 'skipped';
|
|
556
698
|
if (options.noShadow) {
|
|
557
699
|
showInfo('Shadow deployment skipped (--no-shadow)');
|
|
@@ -573,7 +715,7 @@ export async function runCreateRelease(args) {
|
|
|
573
715
|
}
|
|
574
716
|
}
|
|
575
717
|
|
|
576
|
-
// Step
|
|
718
|
+
// Step 15: Ensure we land on RC branch
|
|
577
719
|
const currentBranchAfter = getCurrentBranch();
|
|
578
720
|
if (currentBranchAfter !== rcBranch) {
|
|
579
721
|
try {
|
|
@@ -583,7 +725,7 @@ export async function runCreateRelease(args) {
|
|
|
583
725
|
}
|
|
584
726
|
}
|
|
585
727
|
|
|
586
|
-
// Step
|
|
728
|
+
// Step 16: Summary
|
|
587
729
|
console.log('');
|
|
588
730
|
console.log(
|
|
589
731
|
`${colors.green}════════════════════════════════════════════════════${colors.reset}`
|
|
@@ -601,13 +743,17 @@ export async function runCreateRelease(args) {
|
|
|
601
743
|
`${colors.blue}Tag:${colors.reset} ${tagAlreadyExists ? `${tagName} (pre-existing, skipped)` : tagName}`
|
|
602
744
|
);
|
|
603
745
|
console.log(`${colors.blue}Push:${colors.reset} ${pushStatus}`);
|
|
746
|
+
console.log(
|
|
747
|
+
`${colors.blue}PR:${colors.reset} ${prUrl || 'skipped'}`
|
|
748
|
+
);
|
|
604
749
|
console.log(`${colors.blue}Shadow:${colors.reset} ${shadowStatus}`);
|
|
605
750
|
console.log('');
|
|
606
751
|
|
|
607
752
|
if (options.skipPush) {
|
|
608
753
|
console.log('Next steps:');
|
|
609
754
|
console.log(` 1. Push when ready: git push -u origin ${rcBranch}`);
|
|
610
|
-
console.log(` 2.
|
|
755
|
+
console.log(` 2. Create PR: gh pr create --base main --head ${rcBranch}`);
|
|
756
|
+
console.log(` 3. Sync shadow: claude-hooks shadow sync ${rcBranch}`);
|
|
611
757
|
} else {
|
|
612
758
|
console.log('Next steps:');
|
|
613
759
|
console.log(' 1. Verify shadow deployment is running');
|
package/lib/commands/help.js
CHANGED
|
@@ -18,8 +18,6 @@ import { fetchFileContent, fetchDirectoryListing, createIssue } from '../utils/g
|
|
|
18
18
|
import { promptMenu, promptEditField, promptConfirmation } from '../utils/interactive-ui.js';
|
|
19
19
|
import logger from '../utils/logger.js';
|
|
20
20
|
import { commands } from '../cli-metadata.js';
|
|
21
|
-
import { fetchLibraryContent as librarianFetch } from '../../.library/librarian/index.js';
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Get claude-hooks source repo coordinates from package.json
|
|
25
23
|
* Why: AI help must always fetch from the tool's own repo, not the user's current repo
|
|
@@ -192,10 +190,22 @@ const _readPackageFile = async (relativePath) => {
|
|
|
192
190
|
* Why: The catalog provides navigational context for the AI librarian (Pass 1).
|
|
193
191
|
* Delegates to the librarian module for directory discovery and routing.
|
|
194
192
|
*
|
|
193
|
+
* Uses dynamic import() because .library/ is not shipped in the npm package —
|
|
194
|
+
* it only exists in the source repo. When running from a global install,
|
|
195
|
+
* the import fails gracefully and the help command falls back to static help.
|
|
196
|
+
*
|
|
195
197
|
* @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
|
|
196
198
|
*/
|
|
197
199
|
const readLibraryCatalog = async () => {
|
|
198
|
-
|
|
200
|
+
let librarianFetch;
|
|
201
|
+
try {
|
|
202
|
+
const mod = await import('../../.library/librarian/index.js');
|
|
203
|
+
librarianFetch = mod.fetchLibraryContent;
|
|
204
|
+
} catch {
|
|
205
|
+
logger.debug('help - readLibraryCatalog', 'Library not available (expected in global install)');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
try {
|
|
200
210
|
const result = await librarianFetch(null, { repoRoot: _packageRoot, full: true });
|
|
201
211
|
if (result.catalog) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: library-warnings.js
|
|
3
|
+
* Purpose: Staleness-warning wording and rendering for Library verification
|
|
4
|
+
* gates in claude-hooks.
|
|
5
|
+
*
|
|
6
|
+
* These are consumer rendering functions — they take structured VerifyResult
|
|
7
|
+
* data from the Library and produce display-ready text (console) or Markdown
|
|
8
|
+
* (PR body). The Library produces the data; this module formats it.
|
|
9
|
+
*
|
|
10
|
+
* Constraints:
|
|
11
|
+
* - Pure functions of (VerifyResult, opts) — no I/O, no globals, no environment
|
|
12
|
+
* - Do NOT import from .library/ — lib/ must not depend on unshipped paths
|
|
13
|
+
*
|
|
14
|
+
* Related tickets:
|
|
15
|
+
* AUT-3767 — original placeholder (retired by AUT-3769)
|
|
16
|
+
* AUT-3769 — finalized wording + librarian messages module
|
|
17
|
+
* AUT-3770 — create-release integration (PR body, label, console)
|
|
18
|
+
* AUT-3738 — parent user story
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render an assertive console warning for Library staleness.
|
|
23
|
+
*
|
|
24
|
+
* Returns plain text with a marker and the list of stale books.
|
|
25
|
+
* The closing section varies by consumer context via `opts.autoRegen`:
|
|
26
|
+
* - `'will-run'` — create-release: regen runs in the same command
|
|
27
|
+
* - `'deferred'` — bump-version: regen runs later via create-pr
|
|
28
|
+
*
|
|
29
|
+
* Consumers apply visual formatting (box, color, stderr routing) on top.
|
|
30
|
+
*
|
|
31
|
+
* Returns an empty string when the Library is clean.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} verifyResult
|
|
34
|
+
* @param {boolean} verifyResult.clean
|
|
35
|
+
* @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
|
|
36
|
+
* @param {string[]} verifyResult.recommendedScripts
|
|
37
|
+
* @param {Object} opts
|
|
38
|
+
* @param {'will-run'|'deferred'} opts.autoRegen — controls the call-to-action
|
|
39
|
+
* @returns {string} Plain-text console warning, or empty string if clean
|
|
40
|
+
*/
|
|
41
|
+
export function CONSOLE_WARNING_TEMPLATE(verifyResult, opts = {}) {
|
|
42
|
+
if (verifyResult.clean) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = [];
|
|
47
|
+
|
|
48
|
+
lines.push('⚠️ Library is stale');
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('The following books are out of date:');
|
|
51
|
+
lines.push('');
|
|
52
|
+
|
|
53
|
+
for (const book of verifyResult.staleBooks) {
|
|
54
|
+
lines.push(` • ${book.path}`);
|
|
55
|
+
for (const reason of book.reasons) {
|
|
56
|
+
lines.push(` - ${reason}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lines.push('');
|
|
61
|
+
|
|
62
|
+
if (opts.autoRegen === 'will-run') {
|
|
63
|
+
lines.push('Auto-regeneration will run before the release is tagged.');
|
|
64
|
+
} else if (opts.autoRegen === 'deferred') {
|
|
65
|
+
lines.push('Library will be auto-regenerated when you run create-pr.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render a Markdown section for the PR body when Library is stale.
|
|
73
|
+
*
|
|
74
|
+
* The section heading is exactly `## ⚠️ Library is stale` — do not
|
|
75
|
+
* change this; downstream tooling may key on it. Includes stale-books
|
|
76
|
+
* list with reasons and a remediation section that varies by
|
|
77
|
+
* `opts.autoRegen`:
|
|
78
|
+
* - `'completed'` — auto-regen ran successfully; no scripts listed
|
|
79
|
+
* - `'failed'` — auto-regen failed; scripts listed as fallback
|
|
80
|
+
*
|
|
81
|
+
* The acknowledgement line is always present.
|
|
82
|
+
*
|
|
83
|
+
* Returns an empty string when the Library is clean.
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} verifyResult
|
|
86
|
+
* @param {boolean} verifyResult.clean
|
|
87
|
+
* @param {Array<{ path: string, reasons: string[] }>} verifyResult.staleBooks
|
|
88
|
+
* @param {string[]} verifyResult.recommendedScripts
|
|
89
|
+
* @param {Object} opts
|
|
90
|
+
* @param {'completed'|'failed'} opts.autoRegen — controls the remediation section
|
|
91
|
+
* @returns {string} Markdown section, or empty string if clean
|
|
92
|
+
*/
|
|
93
|
+
export function PR_BODY_SECTION_TEMPLATE(verifyResult, opts = {}) {
|
|
94
|
+
if (verifyResult.clean) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lines = [];
|
|
99
|
+
|
|
100
|
+
lines.push('## ⚠️ Library is stale');
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push('The following Library books are out of date:');
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
for (const book of verifyResult.staleBooks) {
|
|
106
|
+
const reasonText = book.reasons.join('; ');
|
|
107
|
+
lines.push(`- \`${book.path}\` — ${reasonText}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lines.push('');
|
|
111
|
+
|
|
112
|
+
if (opts.autoRegen === 'completed') {
|
|
113
|
+
lines.push('**Auto-regeneration was performed.** The books listed above were regenerated in the release commit.');
|
|
114
|
+
} else if (opts.autoRegen === 'failed') {
|
|
115
|
+
lines.push('**Recommended remediation scripts:**');
|
|
116
|
+
lines.push('');
|
|
117
|
+
for (const script of verifyResult.recommendedScripts) {
|
|
118
|
+
lines.push(`- \`${script}\``);
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('_Auto-regeneration was attempted but failed. Run the scripts manually._');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(
|
|
126
|
+
'_This release was tagged via claude-hooks. The Library lifecycle pipeline is integrated with `create-pr` and `back-merge` — staleness at release time indicates a path that bypassed the pipeline._'
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Tag value used to mark a PR or Linear issue as library-stale.
|
|
134
|
+
*
|
|
135
|
+
* Applied as a Linear label slug or a PR-title prefix by consumers.
|
|
136
|
+
*
|
|
137
|
+
* @type {string}
|
|
138
|
+
*/
|
|
139
|
+
export const PR_TAG_VALUE = 'library-stale';
|
|
140
|
+
|
|
141
|
+
export const LIBRARY_VERIFY_SKIPPED_WARNING =
|
|
142
|
+
'Library verification skipped due to an unexpected error. ' +
|
|
143
|
+
'The version bump will proceed normally.';
|
|
144
|
+
|
|
145
|
+
export const LIBRARY_VERIFY_SKIPPED_WARNING_RELEASE =
|
|
146
|
+
'Library verification skipped due to an unexpected error. ' +
|
|
147
|
+
'The release will proceed normally.';
|
|
@@ -502,10 +502,11 @@ async function verifySDKConnection() {
|
|
|
502
502
|
* @param {number} options.timeout - Timeout in milliseconds (default: 120000 = 2 minutes)
|
|
503
503
|
* @param {string} options.model - Claude model override (e.g., 'haiku', 'sonnet', 'opus')
|
|
504
504
|
* @param {boolean} options.headless - Use SDK instead of CLI (default: false)
|
|
505
|
+
* @param {boolean} options.print - Use CLI print mode (-p): text-only output, no tool use (default: false)
|
|
505
506
|
* @returns {Promise<string>} Claude's response
|
|
506
507
|
* @throws {ClaudeClientError} If execution fails or times out
|
|
507
508
|
*/
|
|
508
|
-
const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null } = {}) => {
|
|
509
|
+
const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = null, headless = false, maxTokens = null, costTracker = null, print = false } = {}) => {
|
|
509
510
|
// Headless mode: use Anthropic SDK directly (GH#133)
|
|
510
511
|
// Branch here (not in executeClaudeWithRetry) because analyzeCode calls
|
|
511
512
|
// executeClaude directly via withRetry — branching here covers all paths.
|
|
@@ -548,10 +549,14 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [], model = nu
|
|
|
548
549
|
// executed inside bash --login so that .profile/.bashrc set up the correct PATH.
|
|
549
550
|
// All CLI flags are embedded in the bash -c command string (no spaces in flag values).
|
|
550
551
|
const claudeParts = [loginShell.path];
|
|
552
|
+
if (print) claudeParts.push('-p');
|
|
551
553
|
if (allowedTools.length > 0) claudeParts.push(`--allowedTools ${allowedTools.join(',')}`);
|
|
552
554
|
if (model) claudeParts.push(`--model ${model}`);
|
|
553
555
|
finalArgs.push('bash', '-lc', claudeParts.join(' '));
|
|
554
556
|
} else {
|
|
557
|
+
if (print) {
|
|
558
|
+
finalArgs.push('-p');
|
|
559
|
+
}
|
|
555
560
|
if (allowedTools.length > 0) {
|
|
556
561
|
// Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
|
|
557
562
|
finalArgs.push('--allowedTools', allowedTools.join(','));
|