@eldrforge/commands-git 0.1.4 → 1.0.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/README.md +2 -0
- package/dist/index.js +1388 -758
- package/dist/index.js.map +1 -1
- package/dist/src/commands/commit.d.ts.map +1 -1
- package/dist/src/commands/precommit.d.ts +2 -3
- package/dist/src/commands/precommit.d.ts.map +1 -1
- package/dist/src/commands/pull.d.ts +6 -0
- package/dist/src/commands/pull.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/util/mcpLogger.d.ts +21 -0
- package/dist/src/util/mcpLogger.d.ts.map +1 -0
- package/package.json +11 -8
package/dist/index.js
CHANGED
|
@@ -3,13 +3,13 @@ import 'dotenv/config';
|
|
|
3
3
|
import shellescape from 'shell-escape';
|
|
4
4
|
import { getDryRunLogger, DEFAULT_MAX_DIFF_BYTES, DEFAULT_EXCLUDED_PATTERNS, Diff, Files, Log, DEFAULT_OUTPUT_DIRECTORY, sanitizeDirection, toAIConfig, createStorageAdapter, createLoggerAdapter, getOutputPath, getTimestampedResponseFilename, getTimestampedRequestFilename, filterContent, getTimestampedCommitFilename, improveContentWithLLM, getLogger, getTimestampedReviewNotesFilename, getTimestampedReviewFilename } from '@eldrforge/core';
|
|
5
5
|
import { ValidationError, ExternalDependencyError, CommandError, createStorage, checkForFileDependencies as checkForFileDependencies$1, logFileDependencyWarning, logFileDependencySuggestions, FileOperationError } from '@eldrforge/shared';
|
|
6
|
-
import { run, validateString, safeJsonParse, validatePackageJson, unstageAll, stageFiles, verifyStagedFiles, runSecure } from '@eldrforge/git-tools';
|
|
6
|
+
import { run, validateString, safeJsonParse, validatePackageJson, unstageAll, stageFiles, verifyStagedFiles, getCurrentBranch, runSecure, getGitStatusSummary } from '@eldrforge/git-tools';
|
|
7
7
|
import { getRecentClosedIssuesForCommit, handleIssueCreation, getReleaseNotesContent, getIssuesContent } from '@eldrforge/github-tools';
|
|
8
8
|
import { runAgenticCommit, requireTTY, generateReflectionReport, getUserChoice, STANDARD_CHOICES, getLLMFeedbackInEditor, editContentInEditor, createCompletionWithRetry, createCommitPrompt, createReviewPrompt, createCompletion } from '@eldrforge/ai-service';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import fs from 'fs/promises';
|
|
11
10
|
import os from 'os';
|
|
12
11
|
import { spawn } from 'child_process';
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
13
|
|
|
14
14
|
// Helper function to read context files
|
|
15
15
|
async function readContextFiles(contextFiles, logger) {
|
|
@@ -311,6 +311,41 @@ const saveCommitMessage = async (outputDirectory, summary, storage, logger)=>{
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
};
|
|
314
|
+
/**
|
|
315
|
+
* Deduplicate files across splits - each file can only be in one split
|
|
316
|
+
* Later splits lose files that were already claimed by earlier splits
|
|
317
|
+
* Returns filtered splits with empty splits removed
|
|
318
|
+
*/ function deduplicateSplits(splits, logger) {
|
|
319
|
+
const claimedFiles = new Set();
|
|
320
|
+
const result = [];
|
|
321
|
+
for (const split of splits){
|
|
322
|
+
// Find files in this split that haven't been claimed yet
|
|
323
|
+
const uniqueFiles = [];
|
|
324
|
+
const duplicates = [];
|
|
325
|
+
for (const file of split.files){
|
|
326
|
+
if (claimedFiles.has(file)) {
|
|
327
|
+
duplicates.push(file);
|
|
328
|
+
} else {
|
|
329
|
+
uniqueFiles.push(file);
|
|
330
|
+
claimedFiles.add(file);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Log if duplicates were found
|
|
334
|
+
if (duplicates.length > 0) {
|
|
335
|
+
logger.warn(`Removing duplicate files from split "${split.message.split('\n')[0]}": ${duplicates.join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
// Only include split if it has files
|
|
338
|
+
if (uniqueFiles.length > 0) {
|
|
339
|
+
result.push({
|
|
340
|
+
...split,
|
|
341
|
+
files: uniqueFiles
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
logger.warn(`Skipping empty split after deduplication: "${split.message.split('\n')[0]}"`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
314
349
|
/**
|
|
315
350
|
* Interactive review of a single split before committing
|
|
316
351
|
*/ async function reviewSplitInteractively(split, index, total, logger) {
|
|
@@ -516,11 +551,12 @@ const saveCommitMessage = async (outputDirectory, summary, storage, logger)=>{
|
|
|
516
551
|
lines.push('═'.repeat(80));
|
|
517
552
|
return lines.join('\n');
|
|
518
553
|
}
|
|
519
|
-
const executeInternal$
|
|
520
|
-
var _ref, _runConfig_excludedPatterns;
|
|
521
|
-
var _runConfig_commit, _runConfig_commit1, _runConfig_commit2, _runConfig_commit3, _runConfig_commit4, _runConfig_commit5, _runConfig_commit6, _aiConfig_commands_commit, _aiConfig_commands,
|
|
554
|
+
const executeInternal$3 = async (runConfig)=>{
|
|
555
|
+
var _ref, _ref1, _runConfig_excludedPatterns;
|
|
556
|
+
var _runConfig_commit, _runConfig_commit1, _runConfig_commit2, _runConfig_commit3, _runConfig_commit4, _runConfig_commit5, _runConfig_commit6, _runConfig_commit7, _aiConfig_commands_commit, _aiConfig_commands, _aiConfig_commands_commit1, _aiConfig_commands1, _aiConfig_commands_commit2, _aiConfig_commands2, _runConfig_commit8, _aiConfig_commands_commit3, _aiConfig_commands3, _runConfig_commit9, _runConfig_commit10, _runConfig_commit11, _runConfig_commit12, _runConfig_commit13, _runConfig_commit14, _runConfig_commit15;
|
|
522
557
|
const isDryRun = runConfig.dryRun || false;
|
|
523
558
|
const logger = getDryRunLogger(isDryRun);
|
|
559
|
+
logger.info('COMMIT_START: Starting commit message generation | Mode: %s', isDryRun ? 'dry-run' : 'live');
|
|
524
560
|
// Track if user explicitly chose to skip in interactive mode
|
|
525
561
|
let userSkippedCommit = false;
|
|
526
562
|
if ((_runConfig_commit = runConfig.commit) === null || _runConfig_commit === void 0 ? void 0 : _runConfig_commit.add) {
|
|
@@ -533,11 +569,14 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
533
569
|
}
|
|
534
570
|
}
|
|
535
571
|
// Determine cached state with single, clear logic
|
|
572
|
+
logger.info('COMMIT_CHECK_STAGED: Checking for staged changes | Action: Analyzing git status');
|
|
536
573
|
const cached = await determineCachedState(runConfig);
|
|
574
|
+
logger.info('COMMIT_STAGED_STATUS: Staged changes detected: %s | Cached: %s', cached ? 'yes' : 'no', cached);
|
|
537
575
|
// Validate sendit state early - now returns boolean instead of throwing
|
|
538
576
|
validateSenditState(runConfig, cached, isDryRun, logger);
|
|
577
|
+
logger.info('COMMIT_GENERATE_DIFF: Generating diff content | Max bytes: %d', (_ref = (_runConfig_commit1 = runConfig.commit) === null || _runConfig_commit1 === void 0 ? void 0 : _runConfig_commit1.maxDiffBytes) !== null && _ref !== void 0 ? _ref : DEFAULT_MAX_DIFF_BYTES);
|
|
539
578
|
let diffContent = '';
|
|
540
|
-
const maxDiffBytes = (
|
|
579
|
+
const maxDiffBytes = (_ref1 = (_runConfig_commit2 = runConfig.commit) === null || _runConfig_commit2 === void 0 ? void 0 : _runConfig_commit2.maxDiffBytes) !== null && _ref1 !== void 0 ? _ref1 : DEFAULT_MAX_DIFF_BYTES;
|
|
541
580
|
const options = {
|
|
542
581
|
cached,
|
|
543
582
|
excludedPatterns: (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS,
|
|
@@ -545,15 +584,16 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
545
584
|
};
|
|
546
585
|
const diff = await Diff.create(options);
|
|
547
586
|
diffContent = await diff.get();
|
|
587
|
+
logger.info('COMMIT_DIFF_GENERATED: Diff content generated | Size: %d bytes | Has changes: %s', diffContent.length, diffContent.trim().length > 0 ? 'yes' : 'no');
|
|
548
588
|
// Check if there are actually any changes in the diff
|
|
549
589
|
let hasActualChanges = diffContent.trim().length > 0;
|
|
550
590
|
// If no changes found with current patterns, check for critical excluded files
|
|
551
591
|
if (!hasActualChanges) {
|
|
552
592
|
const criticalChanges = await Diff.hasCriticalExcludedChanges();
|
|
553
593
|
if (criticalChanges.hasChanges) {
|
|
554
|
-
var
|
|
594
|
+
var _runConfig_commit16;
|
|
555
595
|
logger.info('CRITICAL_FILES_DETECTED: No changes with exclusion patterns, but critical files modified | Files: %s | Action: May need to include critical files', criticalChanges.files.join(', '));
|
|
556
|
-
if (((
|
|
596
|
+
if (((_runConfig_commit16 = runConfig.commit) === null || _runConfig_commit16 === void 0 ? void 0 : _runConfig_commit16.sendit) && !isDryRun) {
|
|
557
597
|
var _runConfig_excludedPatterns1;
|
|
558
598
|
// In sendit mode, automatically include critical files
|
|
559
599
|
logger.info('SENDIT_INCLUDING_CRITICAL: SendIt mode including critical files in diff | Purpose: Ensure all important changes are captured');
|
|
@@ -585,10 +625,10 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
585
625
|
}
|
|
586
626
|
}
|
|
587
627
|
} else {
|
|
588
|
-
var
|
|
628
|
+
var _runConfig_commit17;
|
|
589
629
|
// No changes at all - try fallback to file content for new repositories
|
|
590
630
|
logger.info('NO_CHANGES_DETECTED: No changes found in working directory | Status: clean | Action: Nothing to commit');
|
|
591
|
-
if (((
|
|
631
|
+
if (((_runConfig_commit17 = runConfig.commit) === null || _runConfig_commit17 === void 0 ? void 0 : _runConfig_commit17.sendit) && !isDryRun) {
|
|
592
632
|
logger.warn('No changes detected to commit. Skipping commit operation.');
|
|
593
633
|
return 'No changes to commit.';
|
|
594
634
|
} else {
|
|
@@ -607,8 +647,8 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
607
647
|
diffContent = fileContent;
|
|
608
648
|
hasActualChanges = true; // We have content to work with
|
|
609
649
|
} else {
|
|
610
|
-
var
|
|
611
|
-
if ((
|
|
650
|
+
var _runConfig_commit18;
|
|
651
|
+
if ((_runConfig_commit18 = runConfig.commit) === null || _runConfig_commit18 === void 0 ? void 0 : _runConfig_commit18.sendit) {
|
|
612
652
|
logger.info('COMMIT_SKIPPED: Skipping commit operation | Reason: No changes detected | Action: None');
|
|
613
653
|
return 'No changes to commit.';
|
|
614
654
|
} else {
|
|
@@ -619,7 +659,7 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
619
659
|
}
|
|
620
660
|
}
|
|
621
661
|
const logOptions = {
|
|
622
|
-
limit: (
|
|
662
|
+
limit: (_runConfig_commit3 = runConfig.commit) === null || _runConfig_commit3 === void 0 ? void 0 : _runConfig_commit3.messageLimit
|
|
623
663
|
};
|
|
624
664
|
const log = await Log.create(logOptions);
|
|
625
665
|
const logContext = await log.get();
|
|
@@ -654,7 +694,7 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
654
694
|
overridePaths: runConfig.discoveredConfigDirs || [],
|
|
655
695
|
overrides: runConfig.overrides || false
|
|
656
696
|
};
|
|
657
|
-
const userDirection = sanitizeDirection((
|
|
697
|
+
const userDirection = sanitizeDirection((_runConfig_commit4 = runConfig.commit) === null || _runConfig_commit4 === void 0 ? void 0 : _runConfig_commit4.direction);
|
|
658
698
|
if (userDirection) {
|
|
659
699
|
logger.debug('Using user direction: %s', userDirection);
|
|
660
700
|
}
|
|
@@ -663,10 +703,10 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
663
703
|
const aiStorageAdapter = createStorageAdapter(outputDirectory);
|
|
664
704
|
const aiLogger = createLoggerAdapter(isDryRun);
|
|
665
705
|
// Read context from files if provided
|
|
666
|
-
const contextFromFiles = await readContextFiles((
|
|
706
|
+
const contextFromFiles = await readContextFiles((_runConfig_commit5 = runConfig.commit) === null || _runConfig_commit5 === void 0 ? void 0 : _runConfig_commit5.contextFiles, logger);
|
|
667
707
|
// Combine file context with existing context
|
|
668
708
|
const combinedContext = [
|
|
669
|
-
(
|
|
709
|
+
(_runConfig_commit6 = runConfig.commit) === null || _runConfig_commit6 === void 0 ? void 0 : _runConfig_commit6.context,
|
|
670
710
|
contextFromFiles
|
|
671
711
|
].filter(Boolean).join('\n\n---\n\n');
|
|
672
712
|
// Define promptContext for use in interactive improvements
|
|
@@ -676,7 +716,7 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
676
716
|
directories: runConfig.contextDirectories
|
|
677
717
|
};
|
|
678
718
|
// Announce self-reflection if enabled
|
|
679
|
-
if ((
|
|
719
|
+
if ((_runConfig_commit7 = runConfig.commit) === null || _runConfig_commit7 === void 0 ? void 0 : _runConfig_commit7.selfReflection) {
|
|
680
720
|
logger.info('📊 Self-reflection enabled - detailed analysis will be generated');
|
|
681
721
|
}
|
|
682
722
|
// Get list of changed files
|
|
@@ -685,30 +725,31 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
685
725
|
const changedFiles = changedFilesOutput.split('\n').filter((f)=>f.trim().length > 0);
|
|
686
726
|
logger.debug('Changed files for analysis: %d files', changedFiles.length);
|
|
687
727
|
// Run agentic commit generation
|
|
728
|
+
logger.info('COMMIT_AI_GENERATION: Starting AI-powered commit message generation | Model: %s | Reasoning: %s | Files: %d', ((_aiConfig_commands = aiConfig.commands) === null || _aiConfig_commands === void 0 ? void 0 : (_aiConfig_commands_commit = _aiConfig_commands.commit) === null || _aiConfig_commands_commit === void 0 ? void 0 : _aiConfig_commands_commit.model) || aiConfig.model || 'gpt-4o-mini', ((_aiConfig_commands1 = aiConfig.commands) === null || _aiConfig_commands1 === void 0 ? void 0 : (_aiConfig_commands_commit1 = _aiConfig_commands1.commit) === null || _aiConfig_commands_commit1 === void 0 ? void 0 : _aiConfig_commands_commit1.reasoning) || aiConfig.reasoning || 'low', changedFiles.length);
|
|
688
729
|
const agenticResult = await runAgenticCommit({
|
|
689
730
|
changedFiles,
|
|
690
731
|
diffContent,
|
|
691
732
|
userDirection,
|
|
692
733
|
logContext,
|
|
693
|
-
model: ((
|
|
694
|
-
maxIterations: ((
|
|
734
|
+
model: ((_aiConfig_commands2 = aiConfig.commands) === null || _aiConfig_commands2 === void 0 ? void 0 : (_aiConfig_commands_commit2 = _aiConfig_commands2.commit) === null || _aiConfig_commands_commit2 === void 0 ? void 0 : _aiConfig_commands_commit2.model) || aiConfig.model,
|
|
735
|
+
maxIterations: ((_runConfig_commit8 = runConfig.commit) === null || _runConfig_commit8 === void 0 ? void 0 : _runConfig_commit8.maxAgenticIterations) || 10,
|
|
695
736
|
debug: runConfig.debug,
|
|
696
737
|
debugRequestFile: getOutputPath(outputDirectory, getTimestampedRequestFilename('commit')),
|
|
697
738
|
debugResponseFile: getOutputPath(outputDirectory, getTimestampedResponseFilename('commit')),
|
|
698
739
|
storage: aiStorageAdapter,
|
|
699
740
|
logger: aiLogger,
|
|
700
|
-
openaiReasoning: ((
|
|
741
|
+
openaiReasoning: ((_aiConfig_commands3 = aiConfig.commands) === null || _aiConfig_commands3 === void 0 ? void 0 : (_aiConfig_commands_commit3 = _aiConfig_commands3.commit) === null || _aiConfig_commands_commit3 === void 0 ? void 0 : _aiConfig_commands_commit3.reasoning) || aiConfig.reasoning
|
|
701
742
|
});
|
|
702
743
|
const iterations = agenticResult.iterations || 0;
|
|
703
744
|
const toolCalls = agenticResult.toolCallsExecuted || 0;
|
|
704
745
|
logger.info(`🔍 Analysis complete: ${iterations} iterations, ${toolCalls} tool calls`);
|
|
705
746
|
// Generate self-reflection output if enabled
|
|
706
|
-
if ((
|
|
747
|
+
if ((_runConfig_commit9 = runConfig.commit) === null || _runConfig_commit9 === void 0 ? void 0 : _runConfig_commit9.selfReflection) {
|
|
707
748
|
await generateSelfReflection(agenticResult, outputDirectory, storage, logger);
|
|
708
749
|
}
|
|
709
750
|
// Check for suggested splits
|
|
710
|
-
if (agenticResult.suggestedSplits.length > 1 && ((
|
|
711
|
-
var
|
|
751
|
+
if (agenticResult.suggestedSplits.length > 1 && ((_runConfig_commit10 = runConfig.commit) === null || _runConfig_commit10 === void 0 ? void 0 : _runConfig_commit10.allowCommitSplitting)) {
|
|
752
|
+
var _runConfig_commit19;
|
|
712
753
|
logger.info('\n📋 AI suggests splitting this into %d commits:', agenticResult.suggestedSplits.length);
|
|
713
754
|
for(let i = 0; i < agenticResult.suggestedSplits.length; i++){
|
|
714
755
|
const split = agenticResult.suggestedSplits[i];
|
|
@@ -718,19 +759,25 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
718
759
|
logger.info(' Message: %s', split.message);
|
|
719
760
|
}
|
|
720
761
|
// NEW: Check if auto-split is enabled (defaults to true if not specified)
|
|
721
|
-
const autoSplitEnabled = ((
|
|
762
|
+
const autoSplitEnabled = ((_runConfig_commit19 = runConfig.commit) === null || _runConfig_commit19 === void 0 ? void 0 : _runConfig_commit19.autoSplit) !== false; // Default to true
|
|
722
763
|
if (autoSplitEnabled) {
|
|
723
|
-
var
|
|
764
|
+
var _runConfig_commit20, _runConfig_commit21;
|
|
724
765
|
logger.info('\n🔄 Auto-split enabled - creating separate commits...\n');
|
|
766
|
+
// Deduplicate files across splits to prevent staging errors
|
|
767
|
+
// (AI sometimes suggests the same file in multiple splits)
|
|
768
|
+
const deduplicatedSplits = deduplicateSplits(agenticResult.suggestedSplits, logger);
|
|
769
|
+
if (deduplicatedSplits.length === 0) {
|
|
770
|
+
throw new CommandError('All splits were empty after deduplication - no files to commit', 'SPLIT_EMPTY', false);
|
|
771
|
+
}
|
|
725
772
|
const splitResult = await executeSplitCommits({
|
|
726
|
-
splits:
|
|
773
|
+
splits: deduplicatedSplits,
|
|
727
774
|
isDryRun,
|
|
728
|
-
interactive: !!(((
|
|
775
|
+
interactive: !!(((_runConfig_commit20 = runConfig.commit) === null || _runConfig_commit20 === void 0 ? void 0 : _runConfig_commit20.interactive) && !((_runConfig_commit21 = runConfig.commit) === null || _runConfig_commit21 === void 0 ? void 0 : _runConfig_commit21.sendit)),
|
|
729
776
|
logger});
|
|
730
777
|
if (splitResult.success) {
|
|
731
|
-
var
|
|
778
|
+
var _runConfig_commit22;
|
|
732
779
|
// Push if requested (all commits)
|
|
733
|
-
if (((
|
|
780
|
+
if (((_runConfig_commit22 = runConfig.commit) === null || _runConfig_commit22 === void 0 ? void 0 : _runConfig_commit22.push) && !isDryRun) {
|
|
734
781
|
await pushCommit(runConfig.commit.push, logger, isDryRun);
|
|
735
782
|
}
|
|
736
783
|
return formatSplitCommitSummary(splitResult);
|
|
@@ -755,13 +802,13 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
755
802
|
await saveCommitMessage(outputDirectory, summary, storage, logger);
|
|
756
803
|
// 🛡️ Universal Safety Check: Run before ANY commit operation
|
|
757
804
|
// This protects both direct commits (--sendit) and automated commits (publish, etc.)
|
|
758
|
-
const willCreateCommit = ((
|
|
759
|
-
if (willCreateCommit && !((
|
|
805
|
+
const willCreateCommit = ((_runConfig_commit11 = runConfig.commit) === null || _runConfig_commit11 === void 0 ? void 0 : _runConfig_commit11.sendit) && hasActualChanges && cached;
|
|
806
|
+
if (willCreateCommit && !((_runConfig_commit12 = runConfig.commit) === null || _runConfig_commit12 === void 0 ? void 0 : _runConfig_commit12.skipFileCheck) && !isDryRun) {
|
|
760
807
|
logger.debug('Checking for file: dependencies before commit operation...');
|
|
761
808
|
try {
|
|
762
809
|
const fileDependencyIssues = await checkForFileDependencies$1(storage, process.cwd());
|
|
763
810
|
if (fileDependencyIssues.length > 0) {
|
|
764
|
-
var
|
|
811
|
+
var _runConfig_commit23;
|
|
765
812
|
logger.error('🚫 COMMIT BLOCKED: Found file: dependencies that should not be committed!');
|
|
766
813
|
logger.error('');
|
|
767
814
|
logFileDependencyWarning(fileDependencyIssues, 'commit');
|
|
@@ -769,7 +816,7 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
769
816
|
logger.error('Generated commit message was:');
|
|
770
817
|
logger.error('%s', summary);
|
|
771
818
|
logger.error('');
|
|
772
|
-
if ((
|
|
819
|
+
if ((_runConfig_commit23 = runConfig.commit) === null || _runConfig_commit23 === void 0 ? void 0 : _runConfig_commit23.sendit) {
|
|
773
820
|
logger.error('To bypass this check, use: kodrdriv commit --skip-file-check --sendit');
|
|
774
821
|
} else {
|
|
775
822
|
logger.error('To bypass this check, add skipFileCheck: true to your commit configuration');
|
|
@@ -781,12 +828,12 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
781
828
|
logger.warn('Warning: Could not check for file: dependencies: %s', error.message);
|
|
782
829
|
logger.warn('Proceeding with commit...');
|
|
783
830
|
}
|
|
784
|
-
} else if (((
|
|
831
|
+
} else if (((_runConfig_commit13 = runConfig.commit) === null || _runConfig_commit13 === void 0 ? void 0 : _runConfig_commit13.skipFileCheck) && willCreateCommit) {
|
|
785
832
|
logger.warn('⚠️ Skipping file: dependency check as requested');
|
|
786
833
|
}
|
|
787
834
|
// Handle interactive mode
|
|
788
|
-
if (((
|
|
789
|
-
var
|
|
835
|
+
if (((_runConfig_commit14 = runConfig.commit) === null || _runConfig_commit14 === void 0 ? void 0 : _runConfig_commit14.interactive) && !isDryRun) {
|
|
836
|
+
var _runConfig_commit24;
|
|
790
837
|
requireTTY('Interactive mode requires a terminal. Use --sendit or --dry-run instead.');
|
|
791
838
|
const interactiveResult = await handleInteractiveCommitFeedback(summary, runConfig, promptConfig, promptContext, outputDirectory, storage, diffContent, hasActualChanges, cached);
|
|
792
839
|
if (interactiveResult.action === 'skip') {
|
|
@@ -796,23 +843,23 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
796
843
|
return interactiveResult.finalMessage;
|
|
797
844
|
}
|
|
798
845
|
// User chose to commit - check if sendit is enabled to determine what action to take
|
|
799
|
-
const senditEnabled = (
|
|
846
|
+
const senditEnabled = (_runConfig_commit24 = runConfig.commit) === null || _runConfig_commit24 === void 0 ? void 0 : _runConfig_commit24.sendit;
|
|
800
847
|
const willActuallyCommit = senditEnabled && hasActualChanges && cached;
|
|
801
848
|
if (willActuallyCommit) {
|
|
802
|
-
var
|
|
803
|
-
const commitAction = ((
|
|
849
|
+
var _runConfig_commit25;
|
|
850
|
+
const commitAction = ((_runConfig_commit25 = runConfig.commit) === null || _runConfig_commit25 === void 0 ? void 0 : _runConfig_commit25.amend) ? 'amending last commit' : 'committing';
|
|
804
851
|
logger.info('SENDIT_EXECUTING: SendIt enabled, executing commit action | Action: %s | Message Length: %d | Final Message: \n\n%s\n\n', commitAction.charAt(0).toUpperCase() + commitAction.slice(1), interactiveResult.finalMessage.length, interactiveResult.finalMessage);
|
|
805
852
|
try {
|
|
806
|
-
var
|
|
853
|
+
var _runConfig_commit26, _runConfig_commit27;
|
|
807
854
|
const validatedSummary = validateString(interactiveResult.finalMessage, 'commit summary');
|
|
808
855
|
const escapedSummary = shellescape([
|
|
809
856
|
validatedSummary
|
|
810
857
|
]);
|
|
811
|
-
const commitCommand = ((
|
|
858
|
+
const commitCommand = ((_runConfig_commit26 = runConfig.commit) === null || _runConfig_commit26 === void 0 ? void 0 : _runConfig_commit26.amend) ? `git commit --amend -m ${escapedSummary}` : `git commit -m ${escapedSummary}`;
|
|
812
859
|
await run(commitCommand);
|
|
813
860
|
logger.info('COMMIT_SUCCESS: Commit operation completed successfully | Status: committed | Action: Changes saved to repository');
|
|
814
861
|
// Push if requested
|
|
815
|
-
await pushCommit((
|
|
862
|
+
await pushCommit((_runConfig_commit27 = runConfig.commit) === null || _runConfig_commit27 === void 0 ? void 0 : _runConfig_commit27.push, logger, isDryRun);
|
|
816
863
|
} catch (error) {
|
|
817
864
|
logger.error('Failed to commit:', error);
|
|
818
865
|
throw new ExternalDependencyError('Failed to create commit', 'git', error);
|
|
@@ -835,32 +882,32 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
835
882
|
logger.debug('Skipping sendit logic because user chose to skip in interactive mode');
|
|
836
883
|
return summary;
|
|
837
884
|
}
|
|
838
|
-
if ((
|
|
885
|
+
if ((_runConfig_commit15 = runConfig.commit) === null || _runConfig_commit15 === void 0 ? void 0 : _runConfig_commit15.sendit) {
|
|
839
886
|
if (isDryRun) {
|
|
840
|
-
var
|
|
887
|
+
var _runConfig_commit28, _runConfig_commit29;
|
|
841
888
|
logger.info('Would commit with message: \n\n%s\n\n', summary);
|
|
842
|
-
const commitAction = ((
|
|
889
|
+
const commitAction = ((_runConfig_commit28 = runConfig.commit) === null || _runConfig_commit28 === void 0 ? void 0 : _runConfig_commit28.amend) ? 'git commit --amend -m <generated-message>' : 'git commit -m <generated-message>';
|
|
843
890
|
logger.info('Would execute: %s', commitAction);
|
|
844
891
|
// Show push command in dry run if requested
|
|
845
|
-
if ((
|
|
892
|
+
if ((_runConfig_commit29 = runConfig.commit) === null || _runConfig_commit29 === void 0 ? void 0 : _runConfig_commit29.push) {
|
|
846
893
|
const remote = typeof runConfig.commit.push === 'string' ? runConfig.commit.push : 'origin';
|
|
847
894
|
logger.info('Would push to %s with: git push %s', remote, remote);
|
|
848
895
|
}
|
|
849
896
|
} else if (hasActualChanges && cached) {
|
|
850
|
-
var
|
|
851
|
-
const commitAction = ((
|
|
897
|
+
var _runConfig_commit30;
|
|
898
|
+
const commitAction = ((_runConfig_commit30 = runConfig.commit) === null || _runConfig_commit30 === void 0 ? void 0 : _runConfig_commit30.amend) ? 'amending commit' : 'committing';
|
|
852
899
|
logger.info('SendIt mode enabled. %s with message: \n\n%s\n\n', commitAction.charAt(0).toUpperCase() + commitAction.slice(1), summary);
|
|
853
900
|
try {
|
|
854
|
-
var
|
|
901
|
+
var _runConfig_commit31, _runConfig_commit32;
|
|
855
902
|
const validatedSummary = validateString(summary, 'commit summary');
|
|
856
903
|
const escapedSummary = shellescape([
|
|
857
904
|
validatedSummary
|
|
858
905
|
]);
|
|
859
|
-
const commitCommand = ((
|
|
906
|
+
const commitCommand = ((_runConfig_commit31 = runConfig.commit) === null || _runConfig_commit31 === void 0 ? void 0 : _runConfig_commit31.amend) ? `git commit --amend -m ${escapedSummary}` : `git commit -m ${escapedSummary}`;
|
|
860
907
|
await run(commitCommand);
|
|
861
908
|
logger.info('Commit successful!');
|
|
862
909
|
// Push if requested
|
|
863
|
-
await pushCommit((
|
|
910
|
+
await pushCommit((_runConfig_commit32 = runConfig.commit) === null || _runConfig_commit32 === void 0 ? void 0 : _runConfig_commit32.push, logger, isDryRun);
|
|
864
911
|
} catch (error) {
|
|
865
912
|
logger.error('Failed to commit:', error);
|
|
866
913
|
throw new ExternalDependencyError('Failed to create commit', 'git', error);
|
|
@@ -876,9 +923,9 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
876
923
|
}
|
|
877
924
|
return summary;
|
|
878
925
|
};
|
|
879
|
-
const execute$
|
|
926
|
+
const execute$4 = async (runConfig)=>{
|
|
880
927
|
try {
|
|
881
|
-
return await executeInternal$
|
|
928
|
+
return await executeInternal$3(runConfig);
|
|
882
929
|
} catch (error) {
|
|
883
930
|
// Import getLogger for error handling
|
|
884
931
|
const { getLogger } = await import('@eldrforge/core');
|
|
@@ -898,547 +945,245 @@ const execute$3 = async (runConfig)=>{
|
|
|
898
945
|
}
|
|
899
946
|
};
|
|
900
947
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
try {
|
|
909
|
-
const content = await fs.readFile(cachePath, 'utf-8');
|
|
910
|
-
return JSON.parse(content);
|
|
911
|
-
} catch {
|
|
912
|
-
return {};
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* Save test cache to disk
|
|
917
|
-
*/ async function saveTestCache(packageDir, cache) {
|
|
918
|
-
const cachePath = path.join(packageDir, TEST_CACHE_FILE);
|
|
919
|
-
try {
|
|
920
|
-
await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
921
|
-
} catch (error) {
|
|
922
|
-
logger.debug(`Failed to save test cache: ${error.message}`);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Get the current git commit hash
|
|
927
|
-
*/ async function getCurrentCommitHash(packageDir) {
|
|
928
|
-
try {
|
|
929
|
-
const { stdout } = await runSecure('git', [
|
|
930
|
-
'rev-parse',
|
|
931
|
-
'HEAD'
|
|
932
|
-
], {
|
|
933
|
-
cwd: packageDir
|
|
948
|
+
function _define_property(obj, key, value) {
|
|
949
|
+
if (key in obj) {
|
|
950
|
+
Object.defineProperty(obj, key, {
|
|
951
|
+
value: value,
|
|
952
|
+
enumerable: true,
|
|
953
|
+
configurable: true,
|
|
954
|
+
writable: true
|
|
934
955
|
});
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
return null;
|
|
956
|
+
} else {
|
|
957
|
+
obj[key] = value;
|
|
938
958
|
}
|
|
959
|
+
return obj;
|
|
939
960
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return
|
|
945
|
-
changed: true,
|
|
946
|
-
reason: 'No previous test run recorded'
|
|
947
|
-
};
|
|
961
|
+
// Performance timing helper
|
|
962
|
+
class PerformanceTimer {
|
|
963
|
+
static start(logger, operation) {
|
|
964
|
+
logger.verbose(`⏱️ Starting: ${operation}`);
|
|
965
|
+
return new PerformanceTimer(logger);
|
|
948
966
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if (currentCommitHash !== lastCommitHash) {
|
|
960
|
-
return {
|
|
961
|
-
changed: true,
|
|
962
|
-
reason: `Commit hash changed: ${lastCommitHash.substring(0, 7)} -> ${currentCommitHash.substring(0, 7)}`
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
// Check if there are any uncommitted changes to source files
|
|
966
|
-
const { stdout } = await runSecure('git', [
|
|
967
|
-
'status',
|
|
968
|
-
'--porcelain'
|
|
969
|
-
], {
|
|
970
|
-
cwd: packageDir
|
|
971
|
-
});
|
|
972
|
-
const changedFiles = stdout.split('\n').filter((line)=>line.trim()).map((line)=>line.substring(3).trim()).filter((file)=>{
|
|
973
|
-
// Only consider source files, not build artifacts or config files
|
|
974
|
-
const ext = path.extname(file);
|
|
975
|
-
return(// TypeScript/JavaScript source files
|
|
976
|
-
[
|
|
977
|
-
'.ts',
|
|
978
|
-
'.tsx',
|
|
979
|
-
'.js',
|
|
980
|
-
'.jsx'
|
|
981
|
-
].includes(ext) || // Test files
|
|
982
|
-
file.includes('.test.') || file.includes('.spec.') || // Config files that affect build/test
|
|
983
|
-
[
|
|
984
|
-
'tsconfig.json',
|
|
985
|
-
'vite.config.ts',
|
|
986
|
-
'vitest.config.ts',
|
|
987
|
-
'package.json'
|
|
988
|
-
].includes(path.basename(file)));
|
|
989
|
-
});
|
|
990
|
-
if (changedFiles.length > 0) {
|
|
991
|
-
return {
|
|
992
|
-
changed: true,
|
|
993
|
-
reason: `Uncommitted changes in: ${changedFiles.slice(0, 3).join(', ')}${changedFiles.length > 3 ? '...' : ''}`
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
return {
|
|
997
|
-
changed: false,
|
|
998
|
-
reason: 'No source file changes detected'
|
|
999
|
-
};
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
logger.debug(`Error checking for source file changes: ${error.message}`);
|
|
1002
|
-
// Conservative: assume changed if we can't verify
|
|
1003
|
-
return {
|
|
1004
|
-
changed: true,
|
|
1005
|
-
reason: `Could not verify changes: ${error.message}`
|
|
1006
|
-
};
|
|
967
|
+
end(operation) {
|
|
968
|
+
const duration = Date.now() - this.startTime;
|
|
969
|
+
this.logger.verbose(`⏱️ Completed: ${operation} (${duration}ms)`);
|
|
970
|
+
return duration;
|
|
971
|
+
}
|
|
972
|
+
constructor(logger){
|
|
973
|
+
_define_property(this, "startTime", void 0);
|
|
974
|
+
_define_property(this, "logger", void 0);
|
|
975
|
+
this.logger = logger;
|
|
976
|
+
this.startTime = Date.now();
|
|
1007
977
|
}
|
|
1008
978
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
979
|
+
const EXCLUDED_DIRECTORIES = [
|
|
980
|
+
'node_modules',
|
|
981
|
+
'dist',
|
|
982
|
+
'build',
|
|
983
|
+
'coverage',
|
|
984
|
+
'.git',
|
|
985
|
+
'.next',
|
|
986
|
+
'.nuxt',
|
|
987
|
+
'out',
|
|
988
|
+
'public',
|
|
989
|
+
'static',
|
|
990
|
+
'assets'
|
|
991
|
+
];
|
|
992
|
+
// Batch read multiple package.json files in parallel
|
|
993
|
+
const batchReadPackageJsonFiles = async (packageJsonPaths, storage, rootDir)=>{
|
|
994
|
+
const logger = getLogger();
|
|
995
|
+
const timer = PerformanceTimer.start(logger, `Batch reading ${packageJsonPaths.length} package.json files`);
|
|
996
|
+
const readPromises = packageJsonPaths.map(async (packageJsonPath)=>{
|
|
997
|
+
try {
|
|
998
|
+
const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
|
|
999
|
+
const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
|
|
1000
|
+
const packageJson = validatePackageJson(parsed, packageJsonPath, false);
|
|
1001
|
+
const relativePath = path.relative(rootDir, path.dirname(packageJsonPath));
|
|
1018
1002
|
return {
|
|
1019
|
-
|
|
1020
|
-
|
|
1003
|
+
path: packageJsonPath,
|
|
1004
|
+
packageJson,
|
|
1005
|
+
relativePath: relativePath || '.'
|
|
1021
1006
|
};
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
logger.debug(`Skipped invalid package.json at ${packageJsonPath}: ${error.message}`);
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
const results = await Promise.all(readPromises);
|
|
1013
|
+
const validResults = results.filter((result)=>result !== null);
|
|
1014
|
+
timer.end(`Successfully read ${validResults.length}/${packageJsonPaths.length} package.json files`);
|
|
1015
|
+
return validResults;
|
|
1016
|
+
};
|
|
1017
|
+
// Optimized recursive package.json finder with parallel processing
|
|
1018
|
+
const findAllPackageJsonFiles = async (rootDir, storage)=>{
|
|
1019
|
+
const logger = getLogger();
|
|
1020
|
+
const timer = PerformanceTimer.start(logger, 'Optimized scanning for package.json files');
|
|
1021
|
+
const scanForPaths = async (currentDir, depth = 0)=>{
|
|
1022
|
+
// Prevent infinite recursion and overly deep scanning
|
|
1023
|
+
if (depth > 5) {
|
|
1024
|
+
return [];
|
|
1022
1025
|
}
|
|
1023
|
-
// Get dist directory modification time
|
|
1024
|
-
const distStats = await fs.stat(distPath);
|
|
1025
|
-
const distMtime = distStats.mtimeMs;
|
|
1026
|
-
// Use git to find source files that are newer than dist
|
|
1027
1026
|
try {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
'.jsx',
|
|
1043
|
-
'.json'
|
|
1044
|
-
].includes(ext)) {
|
|
1027
|
+
if (!await storage.exists(currentDir) || !await storage.isDirectory(currentDir)) {
|
|
1028
|
+
return [];
|
|
1029
|
+
}
|
|
1030
|
+
const items = await storage.listFiles(currentDir);
|
|
1031
|
+
const foundPaths = [];
|
|
1032
|
+
// Check for package.json in current directory
|
|
1033
|
+
if (items.includes('package.json')) {
|
|
1034
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
1035
|
+
foundPaths.push(packageJsonPath);
|
|
1036
|
+
}
|
|
1037
|
+
// Process subdirectories in parallel
|
|
1038
|
+
const subdirPromises = [];
|
|
1039
|
+
for (const item of items){
|
|
1040
|
+
if (EXCLUDED_DIRECTORIES.includes(item)) {
|
|
1045
1041
|
continue;
|
|
1046
1042
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1043
|
+
const itemPath = path.join(currentDir, item);
|
|
1044
|
+
subdirPromises.push((async ()=>{
|
|
1045
|
+
try {
|
|
1046
|
+
if (await storage.isDirectory(itemPath)) {
|
|
1047
|
+
return await scanForPaths(itemPath, depth + 1);
|
|
1048
|
+
}
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
logger.debug(`Skipped directory ${itemPath}: ${error.message}`);
|
|
1051
|
+
}
|
|
1052
|
+
return [];
|
|
1053
|
+
})());
|
|
1054
|
+
}
|
|
1055
|
+
if (subdirPromises.length > 0) {
|
|
1056
|
+
const subdirResults = await Promise.all(subdirPromises);
|
|
1057
|
+
for (const subdirPaths of subdirResults){
|
|
1058
|
+
foundPaths.push(...subdirPaths);
|
|
1062
1059
|
}
|
|
1063
1060
|
}
|
|
1064
|
-
return
|
|
1065
|
-
needed: false,
|
|
1066
|
-
reason: 'dist directory is up to date with source files'
|
|
1067
|
-
};
|
|
1061
|
+
return foundPaths;
|
|
1068
1062
|
} catch (error) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1063
|
+
logger.debug(`Failed to scan directory ${currentDir}: ${error.message}`);
|
|
1064
|
+
return [];
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
const pathsTimer = PerformanceTimer.start(logger, 'Finding all package.json paths');
|
|
1068
|
+
const allPaths = await scanForPaths(rootDir);
|
|
1069
|
+
pathsTimer.end(`Found ${allPaths.length} package.json file paths`);
|
|
1070
|
+
// Phase 2: Batch read all package.json files in parallel
|
|
1071
|
+
const packageJsonFiles = await batchReadPackageJsonFiles(allPaths, storage, rootDir);
|
|
1072
|
+
timer.end(`Found ${packageJsonFiles.length} valid package.json files`);
|
|
1073
|
+
return packageJsonFiles;
|
|
1074
|
+
};
|
|
1075
|
+
// Optimized package scanning with parallel processing
|
|
1076
|
+
const scanDirectoryForPackages = async (rootDir, storage)=>{
|
|
1077
|
+
const logger = getLogger();
|
|
1078
|
+
const timer = PerformanceTimer.start(logger, `Optimized package scanning: ${rootDir}`);
|
|
1079
|
+
const packageMap = new Map(); // packageName -> relativePath
|
|
1080
|
+
const absoluteRootDir = path.resolve(process.cwd(), rootDir);
|
|
1081
|
+
logger.verbose(`Scanning directory for packages: ${absoluteRootDir}`);
|
|
1082
|
+
try {
|
|
1083
|
+
// Quick existence and directory check
|
|
1084
|
+
const existsTimer = PerformanceTimer.start(logger, `Checking directory: ${absoluteRootDir}`);
|
|
1085
|
+
if (!await storage.exists(absoluteRootDir) || !await storage.isDirectory(absoluteRootDir)) {
|
|
1086
|
+
existsTimer.end(`Directory not found or not a directory: ${absoluteRootDir}`);
|
|
1087
|
+
timer.end(`Directory invalid: ${rootDir}`);
|
|
1088
|
+
return packageMap;
|
|
1089
|
+
}
|
|
1090
|
+
existsTimer.end(`Directory verified: ${absoluteRootDir}`);
|
|
1091
|
+
// Get all items and process in parallel
|
|
1092
|
+
const listTimer = PerformanceTimer.start(logger, `Listing contents: ${absoluteRootDir}`);
|
|
1093
|
+
const items = await storage.listFiles(absoluteRootDir);
|
|
1094
|
+
listTimer.end(`Listed ${items.length} items`);
|
|
1095
|
+
// Create batched promises for better performance
|
|
1096
|
+
const BATCH_SIZE = 10; // Process directories in batches to avoid overwhelming filesystem
|
|
1097
|
+
const batches = [];
|
|
1098
|
+
for(let i = 0; i < items.length; i += BATCH_SIZE){
|
|
1099
|
+
const batch = items.slice(i, i + BATCH_SIZE);
|
|
1100
|
+
batches.push(batch);
|
|
1101
|
+
}
|
|
1102
|
+
const processTimer = PerformanceTimer.start(logger, `Processing ${batches.length} batches of directories`);
|
|
1103
|
+
for (const batch of batches){
|
|
1104
|
+
const batchPromises = batch.map(async (item)=>{
|
|
1105
|
+
const itemPath = path.join(absoluteRootDir, item);
|
|
1077
1106
|
try {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1107
|
+
if (await storage.isDirectory(itemPath)) {
|
|
1108
|
+
const packageJsonPath = path.join(itemPath, 'package.json');
|
|
1109
|
+
if (await storage.exists(packageJsonPath)) {
|
|
1110
|
+
const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
|
|
1111
|
+
const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
|
|
1112
|
+
const packageJson = validatePackageJson(parsed, packageJsonPath);
|
|
1113
|
+
if (packageJson.name) {
|
|
1114
|
+
const relativePath = path.relative(process.cwd(), itemPath);
|
|
1115
|
+
return {
|
|
1116
|
+
name: packageJson.name,
|
|
1117
|
+
path: relativePath
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1084
1121
|
}
|
|
1085
|
-
} catch
|
|
1086
|
-
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
logger.debug(`Skipped ${itemPath}: ${error.message || error}`);
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1126
|
+
});
|
|
1127
|
+
const batchResults = await Promise.all(batchPromises);
|
|
1128
|
+
for (const result of batchResults){
|
|
1129
|
+
if (result) {
|
|
1130
|
+
packageMap.set(result.name, result.path);
|
|
1131
|
+
logger.debug(`Found package: ${result.name} at ${result.path}`);
|
|
1087
1132
|
}
|
|
1088
1133
|
}
|
|
1089
|
-
// Conservative: if we can't verify, assume clean is needed
|
|
1090
|
-
return {
|
|
1091
|
-
needed: true,
|
|
1092
|
-
reason: 'Could not verify dist freshness, cleaning to be safe'
|
|
1093
|
-
};
|
|
1094
1134
|
}
|
|
1135
|
+
processTimer.end(`Processed ${items.length} directories in ${batches.length} batches`);
|
|
1136
|
+
logger.verbose(`Found ${packageMap.size} packages in ${items.length} subdirectories`);
|
|
1095
1137
|
} catch (error) {
|
|
1096
|
-
logger.
|
|
1097
|
-
// Conservative: assume clean is needed if we can't check
|
|
1098
|
-
return {
|
|
1099
|
-
needed: true,
|
|
1100
|
-
reason: `Could not verify: ${error.message}`
|
|
1101
|
-
};
|
|
1138
|
+
logger.warn(`PERFORMANCE_DIR_READ_FAILED: Unable to read directory | Directory: ${absoluteRootDir} | Error: ${error}`);
|
|
1102
1139
|
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1140
|
+
timer.end(`Found ${packageMap.size} packages in: ${rootDir}`);
|
|
1141
|
+
return packageMap;
|
|
1142
|
+
};
|
|
1143
|
+
// Parallel scope processing for better performance
|
|
1144
|
+
const findPackagesByScope = async (dependencies, scopeRoots, storage)=>{
|
|
1145
|
+
const logger = getLogger();
|
|
1146
|
+
const timer = PerformanceTimer.start(logger, 'Finding packages by scope (optimized)');
|
|
1147
|
+
const workspacePackages = new Map();
|
|
1148
|
+
logger.silly(`Checking dependencies against scope roots: ${JSON.stringify(scopeRoots)}`);
|
|
1149
|
+
// Process all scopes in parallel for maximum performance
|
|
1150
|
+
const scopeTimer = PerformanceTimer.start(logger, 'Parallel scope scanning');
|
|
1151
|
+
const scopePromises = Object.entries(scopeRoots).map(async ([scope, rootDir])=>{
|
|
1152
|
+
logger.verbose(`Scanning scope ${scope} at root directory: ${rootDir}`);
|
|
1153
|
+
const scopePackages = await scanDirectoryForPackages(rootDir, storage);
|
|
1154
|
+
// Filter packages that match the scope
|
|
1155
|
+
const matchingPackages = [];
|
|
1156
|
+
for (const [packageName, packagePath] of scopePackages){
|
|
1157
|
+
if (packageName.startsWith(scope)) {
|
|
1158
|
+
matchingPackages.push([
|
|
1159
|
+
packageName,
|
|
1160
|
+
packagePath
|
|
1161
|
+
]);
|
|
1162
|
+
logger.debug(`Registered package: ${packageName} -> ${packagePath}`);
|
|
1163
|
+
}
|
|
1126
1164
|
}
|
|
1127
1165
|
return {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
};
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
logger.debug(`Error checking if test is needed: ${error.message}`);
|
|
1133
|
-
// Conservative: assume test is needed if we can't check
|
|
1134
|
-
return {
|
|
1135
|
-
needed: true,
|
|
1136
|
-
reason: `Could not verify: ${error.message}`
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Record that tests were run for this package
|
|
1142
|
-
*/ async function recordTestRun(packageDir) {
|
|
1143
|
-
try {
|
|
1144
|
-
const cache = await loadTestCache(packageDir);
|
|
1145
|
-
const cacheKey = packageDir;
|
|
1146
|
-
const commitHash = await getCurrentCommitHash(packageDir);
|
|
1147
|
-
cache[cacheKey] = {
|
|
1148
|
-
lastTestRun: Date.now(),
|
|
1149
|
-
lastCommitHash: commitHash || 'unknown'
|
|
1166
|
+
scope,
|
|
1167
|
+
packages: matchingPackages
|
|
1150
1168
|
};
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
* Returns the optimized command and information about what was skipped
|
|
1159
|
-
*/ async function optimizePrecommitCommand(packageDir, originalCommand, options = {}) {
|
|
1160
|
-
const { skipClean = true, skipTest = true } = options;
|
|
1161
|
-
// Parse the original command to extract individual scripts
|
|
1162
|
-
// Common patterns: "npm run precommit", "npm run clean && npm run build && npm run lint && npm run test"
|
|
1163
|
-
const isPrecommitScript = originalCommand.includes('precommit') || originalCommand.includes('pre-commit');
|
|
1164
|
-
let optimizedCommand = originalCommand;
|
|
1165
|
-
const skipped = {
|
|
1166
|
-
clean: false,
|
|
1167
|
-
test: false
|
|
1168
|
-
};
|
|
1169
|
-
const reasons = {};
|
|
1170
|
-
// If it's a precommit script, we need to check what it actually runs
|
|
1171
|
-
// For now, we'll optimize the common pattern: clean && build && lint && test
|
|
1172
|
-
if (isPrecommitScript || originalCommand.includes('clean')) {
|
|
1173
|
-
if (skipClean) {
|
|
1174
|
-
const cleanCheck = await isCleanNeeded(packageDir);
|
|
1175
|
-
if (!cleanCheck.needed) {
|
|
1176
|
-
// Remove clean from the command
|
|
1177
|
-
optimizedCommand = optimizedCommand.replace(/npm\s+run\s+clean\s+&&\s*/g, '').replace(/npm\s+run\s+clean\s+/g, '').replace(/\s*&&\s*npm\s+run\s+clean/g, '').trim();
|
|
1178
|
-
skipped.clean = true;
|
|
1179
|
-
reasons.clean = cleanCheck.reason;
|
|
1180
|
-
}
|
|
1169
|
+
});
|
|
1170
|
+
const allScopeResults = await Promise.all(scopePromises);
|
|
1171
|
+
// Aggregate all packages from all scopes
|
|
1172
|
+
const allPackages = new Map();
|
|
1173
|
+
for (const { scope, packages } of allScopeResults){
|
|
1174
|
+
for (const [packageName, packagePath] of packages){
|
|
1175
|
+
allPackages.set(packageName, packagePath);
|
|
1181
1176
|
}
|
|
1182
1177
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
// Clean up any double && or trailing &&
|
|
1195
|
-
optimizedCommand = optimizedCommand.replace(/\s*&&\s*&&/g, ' && ').replace(/&&\s*$/, '').trim();
|
|
1196
|
-
return {
|
|
1197
|
-
optimizedCommand,
|
|
1198
|
-
skipped,
|
|
1199
|
-
reasons
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function _define_property(obj, key, value) {
|
|
1204
|
-
if (key in obj) {
|
|
1205
|
-
Object.defineProperty(obj, key, {
|
|
1206
|
-
value: value,
|
|
1207
|
-
enumerable: true,
|
|
1208
|
-
configurable: true,
|
|
1209
|
-
writable: true
|
|
1210
|
-
});
|
|
1211
|
-
} else {
|
|
1212
|
-
obj[key] = value;
|
|
1213
|
-
}
|
|
1214
|
-
return obj;
|
|
1215
|
-
}
|
|
1216
|
-
// Performance timing helper
|
|
1217
|
-
class PerformanceTimer {
|
|
1218
|
-
static start(logger, operation) {
|
|
1219
|
-
logger.verbose(`⏱️ Starting: ${operation}`);
|
|
1220
|
-
return new PerformanceTimer(logger);
|
|
1221
|
-
}
|
|
1222
|
-
end(operation) {
|
|
1223
|
-
const duration = Date.now() - this.startTime;
|
|
1224
|
-
this.logger.verbose(`⏱️ Completed: ${operation} (${duration}ms)`);
|
|
1225
|
-
return duration;
|
|
1226
|
-
}
|
|
1227
|
-
constructor(logger){
|
|
1228
|
-
_define_property(this, "startTime", void 0);
|
|
1229
|
-
_define_property(this, "logger", void 0);
|
|
1230
|
-
this.logger = logger;
|
|
1231
|
-
this.startTime = Date.now();
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
const EXCLUDED_DIRECTORIES = [
|
|
1235
|
-
'node_modules',
|
|
1236
|
-
'dist',
|
|
1237
|
-
'build',
|
|
1238
|
-
'coverage',
|
|
1239
|
-
'.git',
|
|
1240
|
-
'.next',
|
|
1241
|
-
'.nuxt',
|
|
1242
|
-
'out',
|
|
1243
|
-
'public',
|
|
1244
|
-
'static',
|
|
1245
|
-
'assets'
|
|
1246
|
-
];
|
|
1247
|
-
// Batch read multiple package.json files in parallel
|
|
1248
|
-
const batchReadPackageJsonFiles = async (packageJsonPaths, storage, rootDir)=>{
|
|
1249
|
-
const logger = getLogger();
|
|
1250
|
-
const timer = PerformanceTimer.start(logger, `Batch reading ${packageJsonPaths.length} package.json files`);
|
|
1251
|
-
const readPromises = packageJsonPaths.map(async (packageJsonPath)=>{
|
|
1252
|
-
try {
|
|
1253
|
-
const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
|
|
1254
|
-
const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
|
|
1255
|
-
const packageJson = validatePackageJson(parsed, packageJsonPath, false);
|
|
1256
|
-
const relativePath = path.relative(rootDir, path.dirname(packageJsonPath));
|
|
1257
|
-
return {
|
|
1258
|
-
path: packageJsonPath,
|
|
1259
|
-
packageJson,
|
|
1260
|
-
relativePath: relativePath || '.'
|
|
1261
|
-
};
|
|
1262
|
-
} catch (error) {
|
|
1263
|
-
logger.debug(`Skipped invalid package.json at ${packageJsonPath}: ${error.message}`);
|
|
1264
|
-
return null;
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
const results = await Promise.all(readPromises);
|
|
1268
|
-
const validResults = results.filter((result)=>result !== null);
|
|
1269
|
-
timer.end(`Successfully read ${validResults.length}/${packageJsonPaths.length} package.json files`);
|
|
1270
|
-
return validResults;
|
|
1271
|
-
};
|
|
1272
|
-
// Optimized recursive package.json finder with parallel processing
|
|
1273
|
-
const findAllPackageJsonFiles = async (rootDir, storage)=>{
|
|
1274
|
-
const logger = getLogger();
|
|
1275
|
-
const timer = PerformanceTimer.start(logger, 'Optimized scanning for package.json files');
|
|
1276
|
-
const scanForPaths = async (currentDir, depth = 0)=>{
|
|
1277
|
-
// Prevent infinite recursion and overly deep scanning
|
|
1278
|
-
if (depth > 5) {
|
|
1279
|
-
return [];
|
|
1280
|
-
}
|
|
1281
|
-
try {
|
|
1282
|
-
if (!await storage.exists(currentDir) || !await storage.isDirectory(currentDir)) {
|
|
1283
|
-
return [];
|
|
1284
|
-
}
|
|
1285
|
-
const items = await storage.listFiles(currentDir);
|
|
1286
|
-
const foundPaths = [];
|
|
1287
|
-
// Check for package.json in current directory
|
|
1288
|
-
if (items.includes('package.json')) {
|
|
1289
|
-
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
1290
|
-
foundPaths.push(packageJsonPath);
|
|
1291
|
-
}
|
|
1292
|
-
// Process subdirectories in parallel
|
|
1293
|
-
const subdirPromises = [];
|
|
1294
|
-
for (const item of items){
|
|
1295
|
-
if (EXCLUDED_DIRECTORIES.includes(item)) {
|
|
1296
|
-
continue;
|
|
1297
|
-
}
|
|
1298
|
-
const itemPath = path.join(currentDir, item);
|
|
1299
|
-
subdirPromises.push((async ()=>{
|
|
1300
|
-
try {
|
|
1301
|
-
if (await storage.isDirectory(itemPath)) {
|
|
1302
|
-
return await scanForPaths(itemPath, depth + 1);
|
|
1303
|
-
}
|
|
1304
|
-
} catch (error) {
|
|
1305
|
-
logger.debug(`Skipped directory ${itemPath}: ${error.message}`);
|
|
1306
|
-
}
|
|
1307
|
-
return [];
|
|
1308
|
-
})());
|
|
1309
|
-
}
|
|
1310
|
-
if (subdirPromises.length > 0) {
|
|
1311
|
-
const subdirResults = await Promise.all(subdirPromises);
|
|
1312
|
-
for (const subdirPaths of subdirResults){
|
|
1313
|
-
foundPaths.push(...subdirPaths);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
return foundPaths;
|
|
1317
|
-
} catch (error) {
|
|
1318
|
-
logger.debug(`Failed to scan directory ${currentDir}: ${error.message}`);
|
|
1319
|
-
return [];
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
const pathsTimer = PerformanceTimer.start(logger, 'Finding all package.json paths');
|
|
1323
|
-
const allPaths = await scanForPaths(rootDir);
|
|
1324
|
-
pathsTimer.end(`Found ${allPaths.length} package.json file paths`);
|
|
1325
|
-
// Phase 2: Batch read all package.json files in parallel
|
|
1326
|
-
const packageJsonFiles = await batchReadPackageJsonFiles(allPaths, storage, rootDir);
|
|
1327
|
-
timer.end(`Found ${packageJsonFiles.length} valid package.json files`);
|
|
1328
|
-
return packageJsonFiles;
|
|
1329
|
-
};
|
|
1330
|
-
// Optimized package scanning with parallel processing
|
|
1331
|
-
const scanDirectoryForPackages = async (rootDir, storage)=>{
|
|
1332
|
-
const logger = getLogger();
|
|
1333
|
-
const timer = PerformanceTimer.start(logger, `Optimized package scanning: ${rootDir}`);
|
|
1334
|
-
const packageMap = new Map(); // packageName -> relativePath
|
|
1335
|
-
const absoluteRootDir = path.resolve(process.cwd(), rootDir);
|
|
1336
|
-
logger.verbose(`Scanning directory for packages: ${absoluteRootDir}`);
|
|
1337
|
-
try {
|
|
1338
|
-
// Quick existence and directory check
|
|
1339
|
-
const existsTimer = PerformanceTimer.start(logger, `Checking directory: ${absoluteRootDir}`);
|
|
1340
|
-
if (!await storage.exists(absoluteRootDir) || !await storage.isDirectory(absoluteRootDir)) {
|
|
1341
|
-
existsTimer.end(`Directory not found or not a directory: ${absoluteRootDir}`);
|
|
1342
|
-
timer.end(`Directory invalid: ${rootDir}`);
|
|
1343
|
-
return packageMap;
|
|
1344
|
-
}
|
|
1345
|
-
existsTimer.end(`Directory verified: ${absoluteRootDir}`);
|
|
1346
|
-
// Get all items and process in parallel
|
|
1347
|
-
const listTimer = PerformanceTimer.start(logger, `Listing contents: ${absoluteRootDir}`);
|
|
1348
|
-
const items = await storage.listFiles(absoluteRootDir);
|
|
1349
|
-
listTimer.end(`Listed ${items.length} items`);
|
|
1350
|
-
// Create batched promises for better performance
|
|
1351
|
-
const BATCH_SIZE = 10; // Process directories in batches to avoid overwhelming filesystem
|
|
1352
|
-
const batches = [];
|
|
1353
|
-
for(let i = 0; i < items.length; i += BATCH_SIZE){
|
|
1354
|
-
const batch = items.slice(i, i + BATCH_SIZE);
|
|
1355
|
-
batches.push(batch);
|
|
1356
|
-
}
|
|
1357
|
-
const processTimer = PerformanceTimer.start(logger, `Processing ${batches.length} batches of directories`);
|
|
1358
|
-
for (const batch of batches){
|
|
1359
|
-
const batchPromises = batch.map(async (item)=>{
|
|
1360
|
-
const itemPath = path.join(absoluteRootDir, item);
|
|
1361
|
-
try {
|
|
1362
|
-
if (await storage.isDirectory(itemPath)) {
|
|
1363
|
-
const packageJsonPath = path.join(itemPath, 'package.json');
|
|
1364
|
-
if (await storage.exists(packageJsonPath)) {
|
|
1365
|
-
const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
|
|
1366
|
-
const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
|
|
1367
|
-
const packageJson = validatePackageJson(parsed, packageJsonPath);
|
|
1368
|
-
if (packageJson.name) {
|
|
1369
|
-
const relativePath = path.relative(process.cwd(), itemPath);
|
|
1370
|
-
return {
|
|
1371
|
-
name: packageJson.name,
|
|
1372
|
-
path: relativePath
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
logger.debug(`Skipped ${itemPath}: ${error.message || error}`);
|
|
1379
|
-
}
|
|
1380
|
-
return null;
|
|
1381
|
-
});
|
|
1382
|
-
const batchResults = await Promise.all(batchPromises);
|
|
1383
|
-
for (const result of batchResults){
|
|
1384
|
-
if (result) {
|
|
1385
|
-
packageMap.set(result.name, result.path);
|
|
1386
|
-
logger.debug(`Found package: ${result.name} at ${result.path}`);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
processTimer.end(`Processed ${items.length} directories in ${batches.length} batches`);
|
|
1391
|
-
logger.verbose(`Found ${packageMap.size} packages in ${items.length} subdirectories`);
|
|
1392
|
-
} catch (error) {
|
|
1393
|
-
logger.warn(`PERFORMANCE_DIR_READ_FAILED: Unable to read directory | Directory: ${absoluteRootDir} | Error: ${error}`);
|
|
1394
|
-
}
|
|
1395
|
-
timer.end(`Found ${packageMap.size} packages in: ${rootDir}`);
|
|
1396
|
-
return packageMap;
|
|
1397
|
-
};
|
|
1398
|
-
// Parallel scope processing for better performance
|
|
1399
|
-
const findPackagesByScope = async (dependencies, scopeRoots, storage)=>{
|
|
1400
|
-
const logger = getLogger();
|
|
1401
|
-
const timer = PerformanceTimer.start(logger, 'Finding packages by scope (optimized)');
|
|
1402
|
-
const workspacePackages = new Map();
|
|
1403
|
-
logger.silly(`Checking dependencies against scope roots: ${JSON.stringify(scopeRoots)}`);
|
|
1404
|
-
// Process all scopes in parallel for maximum performance
|
|
1405
|
-
const scopeTimer = PerformanceTimer.start(logger, 'Parallel scope scanning');
|
|
1406
|
-
const scopePromises = Object.entries(scopeRoots).map(async ([scope, rootDir])=>{
|
|
1407
|
-
logger.verbose(`Scanning scope ${scope} at root directory: ${rootDir}`);
|
|
1408
|
-
const scopePackages = await scanDirectoryForPackages(rootDir, storage);
|
|
1409
|
-
// Filter packages that match the scope
|
|
1410
|
-
const matchingPackages = [];
|
|
1411
|
-
for (const [packageName, packagePath] of scopePackages){
|
|
1412
|
-
if (packageName.startsWith(scope)) {
|
|
1413
|
-
matchingPackages.push([
|
|
1414
|
-
packageName,
|
|
1415
|
-
packagePath
|
|
1416
|
-
]);
|
|
1417
|
-
logger.debug(`Registered package: ${packageName} -> ${packagePath}`);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
return {
|
|
1421
|
-
scope,
|
|
1422
|
-
packages: matchingPackages
|
|
1423
|
-
};
|
|
1424
|
-
});
|
|
1425
|
-
const allScopeResults = await Promise.all(scopePromises);
|
|
1426
|
-
// Aggregate all packages from all scopes
|
|
1427
|
-
const allPackages = new Map();
|
|
1428
|
-
for (const { scope, packages } of allScopeResults){
|
|
1429
|
-
for (const [packageName, packagePath] of packages){
|
|
1430
|
-
allPackages.set(packageName, packagePath);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
scopeTimer.end(`Scanned ${Object.keys(scopeRoots).length} scope roots, found ${allPackages.size} packages`);
|
|
1434
|
-
// Match dependencies to available packages
|
|
1435
|
-
const matchTimer = PerformanceTimer.start(logger, 'Matching dependencies to packages');
|
|
1436
|
-
for (const [depName, depVersion] of Object.entries(dependencies)){
|
|
1437
|
-
logger.debug(`Processing dependency: ${depName}@${depVersion}`);
|
|
1438
|
-
if (allPackages.has(depName)) {
|
|
1439
|
-
const packagePath = allPackages.get(depName);
|
|
1440
|
-
workspacePackages.set(depName, packagePath);
|
|
1441
|
-
logger.verbose(`Found sibling package: ${depName} at ${packagePath}`);
|
|
1178
|
+
scopeTimer.end(`Scanned ${Object.keys(scopeRoots).length} scope roots, found ${allPackages.size} packages`);
|
|
1179
|
+
// Match dependencies to available packages
|
|
1180
|
+
const matchTimer = PerformanceTimer.start(logger, 'Matching dependencies to packages');
|
|
1181
|
+
for (const [depName, depVersion] of Object.entries(dependencies)){
|
|
1182
|
+
logger.debug(`Processing dependency: ${depName}@${depVersion}`);
|
|
1183
|
+
if (allPackages.has(depName)) {
|
|
1184
|
+
const packagePath = allPackages.get(depName);
|
|
1185
|
+
workspacePackages.set(depName, packagePath);
|
|
1186
|
+
logger.verbose(`Found sibling package: ${depName} at ${packagePath}`);
|
|
1442
1187
|
}
|
|
1443
1188
|
}
|
|
1444
1189
|
matchTimer.end(`Matched ${workspacePackages.size} dependencies to workspace packages`);
|
|
@@ -1499,87 +1244,90 @@ const checkForFileDependencies = (packageJsonFiles)=>{
|
|
|
1499
1244
|
};
|
|
1500
1245
|
|
|
1501
1246
|
/**
|
|
1502
|
-
*
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1247
|
+
* Check if running in MCP server mode
|
|
1248
|
+
*/ const isMcpMode = ()=>process.env.KODRDRIV_MCP_SERVER === 'true';
|
|
1249
|
+
/**
|
|
1250
|
+
* Get an MCP-aware logger that suppresses info/warn/debug output in MCP mode.
|
|
1251
|
+
* Errors are always logged since they indicate problems that need attention.
|
|
1252
|
+
*/ const getMcpAwareLogger = ()=>{
|
|
1253
|
+
const coreLogger = getLogger();
|
|
1254
|
+
if (!isMcpMode()) {
|
|
1255
|
+
// In normal mode, just return the core logger
|
|
1256
|
+
return coreLogger;
|
|
1257
|
+
}
|
|
1258
|
+
// In MCP mode, wrap the logger to suppress non-error output
|
|
1259
|
+
return {
|
|
1260
|
+
info: (_message, ..._args)=>{},
|
|
1261
|
+
warn: (_message, ..._args)=>{},
|
|
1262
|
+
debug: (_message, ..._args)=>{},
|
|
1263
|
+
verbose: (_message, ..._args)=>{},
|
|
1264
|
+
silly: (_message, ..._args)=>{},
|
|
1265
|
+
// Always log errors - they indicate real problems
|
|
1266
|
+
error: (message, ...args)=>coreLogger.error(message, ...args)
|
|
1267
|
+
};
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Execute precommit checks by running the package's precommit script.
|
|
1272
|
+
* Expects the package to have a "precommit" script in package.json.
|
|
1273
|
+
*/ const execute$3 = async (runConfig)=>{
|
|
1274
|
+
var _runConfig_precommit, _packageJson_scripts;
|
|
1275
|
+
const logger = getMcpAwareLogger();
|
|
1507
1276
|
const isDryRun = runConfig.dryRun || false;
|
|
1508
1277
|
const packageDir = process.cwd();
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1278
|
+
const shouldFix = ((_runConfig_precommit = runConfig.precommit) === null || _runConfig_precommit === void 0 ? void 0 : _runConfig_precommit.fix) || false;
|
|
1279
|
+
// Verify precommit script exists
|
|
1280
|
+
const fs = await import('fs/promises');
|
|
1281
|
+
const packageJsonPath = path.join(packageDir, 'package.json');
|
|
1282
|
+
let packageName = packageDir;
|
|
1283
|
+
let packageJson;
|
|
1513
1284
|
try {
|
|
1514
|
-
var
|
|
1515
|
-
const fs = await import('fs/promises');
|
|
1516
|
-
const packageJsonPath = path.join(packageDir, 'package.json');
|
|
1285
|
+
var _packageJson_scripts1;
|
|
1517
1286
|
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if ((
|
|
1521
|
-
|
|
1522
|
-
// If it includes clean, we'll optimize it out
|
|
1523
|
-
// Otherwise, use the precommit script directly
|
|
1524
|
-
if (!precommitScript.includes('clean')) {
|
|
1525
|
-
commandToRun = `npm run precommit`;
|
|
1526
|
-
} else {
|
|
1527
|
-
// Use default command (lint -> build -> test) if precommit includes clean
|
|
1528
|
-
commandToRun = defaultCommand;
|
|
1529
|
-
}
|
|
1287
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
1288
|
+
packageName = packageJson.name || packageDir;
|
|
1289
|
+
if (!((_packageJson_scripts1 = packageJson.scripts) === null || _packageJson_scripts1 === void 0 ? void 0 : _packageJson_scripts1.precommit)) {
|
|
1290
|
+
throw new Error(`Package "${packageName}" is missing a "precommit" script in package.json`);
|
|
1530
1291
|
}
|
|
1531
1292
|
} catch (error) {
|
|
1532
|
-
|
|
1293
|
+
if (error.code === 'ENOENT') {
|
|
1294
|
+
throw new Error(`No package.json found at ${packageJsonPath}`);
|
|
1295
|
+
}
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
// If --fix is enabled, try to run lint --fix before precommit
|
|
1299
|
+
if (shouldFix && ((_packageJson_scripts = packageJson.scripts) === null || _packageJson_scripts === void 0 ? void 0 : _packageJson_scripts.lint)) {
|
|
1300
|
+
const lintFixCommand = 'npm run lint -- --fix';
|
|
1301
|
+
if (isDryRun) {
|
|
1302
|
+
logger.info(`DRY RUN: Would execute: ${lintFixCommand}`);
|
|
1303
|
+
} else {
|
|
1304
|
+
try {
|
|
1305
|
+
logger.info(`🔧 Running lint --fix before precommit checks: ${lintFixCommand}`);
|
|
1306
|
+
await run(lintFixCommand, {
|
|
1307
|
+
cwd: packageDir
|
|
1308
|
+
});
|
|
1309
|
+
logger.info(`✅ Lint fixes applied`);
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
// Log warning but continue with precommit - lint --fix may fail on some issues
|
|
1312
|
+
logger.warn(`⚠️ Lint --fix had issues (continuing with precommit): ${error.message}`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1533
1315
|
}
|
|
1316
|
+
const commandToRun = 'npm run precommit';
|
|
1534
1317
|
if (isDryRun) {
|
|
1535
1318
|
logger.info(`DRY RUN: Would execute: ${commandToRun}`);
|
|
1536
1319
|
return `DRY RUN: Would run precommit checks: ${commandToRun}`;
|
|
1537
1320
|
}
|
|
1538
|
-
//
|
|
1539
|
-
|
|
1540
|
-
let optimizationInfo = null;
|
|
1321
|
+
// Execute the precommit script
|
|
1322
|
+
const timer = PerformanceTimer.start(logger, 'Precommit checks');
|
|
1541
1323
|
try {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
optimizationInfo = {
|
|
1545
|
-
skipped: optimization.skipped,
|
|
1546
|
-
reasons: optimization.reasons
|
|
1547
|
-
};
|
|
1548
|
-
if (optimization.skipped.clean || optimization.skipped.test) {
|
|
1549
|
-
const skippedParts = [];
|
|
1550
|
-
if (optimization.skipped.clean) {
|
|
1551
|
-
skippedParts.push(`clean (${optimization.reasons.clean})`);
|
|
1552
|
-
}
|
|
1553
|
-
if (optimization.skipped.test) {
|
|
1554
|
-
skippedParts.push(`test (${optimization.reasons.test})`);
|
|
1555
|
-
}
|
|
1556
|
-
logger.info(`⚡ Optimized: Skipped ${skippedParts.join(', ')}`);
|
|
1557
|
-
if (runConfig.verbose || runConfig.debug) {
|
|
1558
|
-
logger.info(` Original: ${commandToRun}`);
|
|
1559
|
-
logger.info(` Optimized: ${optimizedCommand}`);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
} catch (error) {
|
|
1563
|
-
logger.debug(`Precommit optimization failed: ${error.message}`);
|
|
1564
|
-
}
|
|
1565
|
-
// Execute the optimized command
|
|
1566
|
-
const timer = PerformanceTimer.start(logger, 'Precommit checks');
|
|
1567
|
-
try {
|
|
1568
|
-
logger.info(`🔧 Running precommit checks: ${optimizedCommand}`);
|
|
1569
|
-
await run(optimizedCommand, {
|
|
1324
|
+
logger.info(`🔧 Running precommit checks: ${commandToRun}`);
|
|
1325
|
+
await run(commandToRun, {
|
|
1570
1326
|
cwd: packageDir
|
|
1571
1327
|
});
|
|
1572
1328
|
const duration = timer.end('Precommit checks');
|
|
1573
1329
|
const seconds = (duration / 1000).toFixed(1);
|
|
1574
1330
|
logger.info(`✅ Precommit checks passed (${seconds}s)`);
|
|
1575
|
-
// Record test run if tests were executed (not skipped)
|
|
1576
|
-
if (optimizedCommand.includes('test') && (!optimizationInfo || !optimizationInfo.skipped.test)) {
|
|
1577
|
-
try {
|
|
1578
|
-
await recordTestRun(packageDir);
|
|
1579
|
-
} catch (error) {
|
|
1580
|
-
logger.debug(`Failed to record test run: ${error.message}`);
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
1331
|
return `Precommit checks completed successfully in ${seconds}s`;
|
|
1584
1332
|
} catch (error) {
|
|
1585
1333
|
timer.end('Precommit checks');
|
|
@@ -1588,7 +1336,7 @@ const checkForFileDependencies = (packageJsonFiles)=>{
|
|
|
1588
1336
|
}
|
|
1589
1337
|
};
|
|
1590
1338
|
|
|
1591
|
-
const executeInternal$
|
|
1339
|
+
const executeInternal$2 = async (runConfig)=>{
|
|
1592
1340
|
const isDryRun = runConfig.dryRun || false;
|
|
1593
1341
|
const logger = getDryRunLogger(isDryRun);
|
|
1594
1342
|
const storage = createStorage();
|
|
@@ -1612,9 +1360,9 @@ const executeInternal$1 = async (runConfig)=>{
|
|
|
1612
1360
|
throw new FileOperationError('Failed to remove output directory', outputDirectory, error);
|
|
1613
1361
|
}
|
|
1614
1362
|
};
|
|
1615
|
-
const execute$
|
|
1363
|
+
const execute$2 = async (runConfig)=>{
|
|
1616
1364
|
try {
|
|
1617
|
-
await executeInternal$
|
|
1365
|
+
await executeInternal$2(runConfig);
|
|
1618
1366
|
} catch (error) {
|
|
1619
1367
|
const logger = getLogger();
|
|
1620
1368
|
if (error instanceof FileOperationError) {
|
|
@@ -2083,7 +1831,7 @@ const processSingleReview = async (reviewNote, runConfig, outputDirectory)=>{
|
|
|
2083
1831
|
}
|
|
2084
1832
|
return analysisResult;
|
|
2085
1833
|
};
|
|
2086
|
-
const executeInternal = async (runConfig)=>{
|
|
1834
|
+
const executeInternal$1 = async (runConfig)=>{
|
|
2087
1835
|
var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review4, _runConfig_review5, _runConfig_review6, _runConfig_review7, _runConfig_review8, _runConfig_review9, _runConfig_review10, _runConfig_review11, _runConfig_review12, _runConfig_review13, _runConfig_review14, _runConfig_review15, _runConfig_review16, _runConfig_review17, _runConfig_review18;
|
|
2088
1836
|
const logger = getLogger();
|
|
2089
1837
|
const isDryRun = runConfig.dryRun || false;
|
|
@@ -2204,143 +1952,1025 @@ const executeInternal = async (runConfig)=>{
|
|
|
2204
1952
|
runConfig.review.note = reviewNote;
|
|
2205
1953
|
}
|
|
2206
1954
|
} catch (error) {
|
|
2207
|
-
logger.error(`Failed to capture review note via editor: ${error.message}`);
|
|
2208
|
-
throw error;
|
|
2209
|
-
} finally{
|
|
2210
|
-
// Always clean up the temp file
|
|
2211
|
-
if (tmpFilePath) {
|
|
2212
|
-
await cleanupTempFile(tmpFilePath);
|
|
1955
|
+
logger.error(`Failed to capture review note via editor: ${error.message}`);
|
|
1956
|
+
throw error;
|
|
1957
|
+
} finally{
|
|
1958
|
+
// Always clean up the temp file
|
|
1959
|
+
if (tmpFilePath) {
|
|
1960
|
+
await cleanupTempFile(tmpFilePath);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
reviewFiles = [
|
|
1964
|
+
'editor input'
|
|
1965
|
+
];
|
|
1966
|
+
}
|
|
1967
|
+
if (!reviewNote || !reviewNote.trim()) {
|
|
1968
|
+
throw new ValidationError('No review note provided or captured');
|
|
1969
|
+
}
|
|
1970
|
+
logger.info('📝 Starting review analysis...');
|
|
1971
|
+
logger.debug('Review note: %s', reviewNote);
|
|
1972
|
+
logger.debug('Review note length: %d characters', reviewNote.length);
|
|
1973
|
+
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
1974
|
+
const storage = createStorage();
|
|
1975
|
+
await storage.ensureDirectory(outputDirectory);
|
|
1976
|
+
// Save timestamped copy of review notes to output directory
|
|
1977
|
+
try {
|
|
1978
|
+
const reviewNotesFilename = getTimestampedReviewNotesFilename();
|
|
1979
|
+
const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
|
|
1980
|
+
const reviewNotesContent = `# Review Notes\n\n${reviewNote}\n\n`;
|
|
1981
|
+
await safeWriteFile(reviewNotesPath, reviewNotesContent);
|
|
1982
|
+
logger.debug('Saved timestamped review notes: %s', reviewNotesPath);
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
logger.warn('Failed to save review notes: %s', error.message);
|
|
1985
|
+
}
|
|
1986
|
+
// Phase 1: File selection (only for directory mode)
|
|
1987
|
+
let selectedFiles;
|
|
1988
|
+
if ((_runConfig_review16 = runConfig.review) === null || _runConfig_review16 === void 0 ? void 0 : _runConfig_review16.directory) {
|
|
1989
|
+
var _runConfig_review26;
|
|
1990
|
+
selectedFiles = await selectFilesForProcessing(reviewFiles, ((_runConfig_review26 = runConfig.review) === null || _runConfig_review26 === void 0 ? void 0 : _runConfig_review26.sendit) || false);
|
|
1991
|
+
} else {
|
|
1992
|
+
// For single note mode, just use the note directly
|
|
1993
|
+
selectedFiles = [
|
|
1994
|
+
'single note'
|
|
1995
|
+
];
|
|
1996
|
+
}
|
|
1997
|
+
// Phase 2: Process selected files in order
|
|
1998
|
+
logger.info(`\n📝 Starting analysis phase...`);
|
|
1999
|
+
const results = [];
|
|
2000
|
+
const processedFiles = [];
|
|
2001
|
+
if ((_runConfig_review17 = runConfig.review) === null || _runConfig_review17 === void 0 ? void 0 : _runConfig_review17.directory) {
|
|
2002
|
+
// Directory mode: process each selected file
|
|
2003
|
+
for(let i = 0; i < selectedFiles.length; i++){
|
|
2004
|
+
const filePath = selectedFiles[i];
|
|
2005
|
+
try {
|
|
2006
|
+
logger.info(`📝 Processing file ${i + 1}/${selectedFiles.length}: ${filePath}`);
|
|
2007
|
+
const fileNote = await readReviewNoteFromFile(filePath);
|
|
2008
|
+
const fileResult = await processSingleReview(fileNote, runConfig, outputDirectory);
|
|
2009
|
+
results.push(fileResult);
|
|
2010
|
+
processedFiles.push(filePath);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
// Check if this is a critical error that should be propagated
|
|
2013
|
+
if (error.message.includes('Too many context gathering errors')) {
|
|
2014
|
+
throw error; // Propagate critical context errors
|
|
2015
|
+
}
|
|
2016
|
+
logger.warn(`Failed to process file ${filePath}: ${error.message}`);
|
|
2017
|
+
// Continue with other files for non-critical errors
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
// Single note mode: process the note directly
|
|
2022
|
+
try {
|
|
2023
|
+
logger.info(`📝 Processing single review note`);
|
|
2024
|
+
const fileResult = await processSingleReview(reviewNote, runConfig, outputDirectory);
|
|
2025
|
+
results.push(fileResult);
|
|
2026
|
+
processedFiles.push('single note');
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
logger.warn(`Failed to process review note: ${error.message}`);
|
|
2029
|
+
throw error; // Re-throw for single note mode since there's only one item
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (results.length === 0) {
|
|
2033
|
+
throw new ValidationError('No files were processed successfully');
|
|
2034
|
+
}
|
|
2035
|
+
// Combine results if we processed multiple files
|
|
2036
|
+
let analysisResult;
|
|
2037
|
+
if (results.length === 1) {
|
|
2038
|
+
analysisResult = results[0];
|
|
2039
|
+
} else {
|
|
2040
|
+
logger.info(`✅ Successfully processed ${results.length} review files`);
|
|
2041
|
+
// Create a combined summary
|
|
2042
|
+
const totalIssues = results.reduce((sum, result)=>sum + result.totalIssues, 0);
|
|
2043
|
+
const allIssues = results.flatMap((result)=>result.issues || []);
|
|
2044
|
+
analysisResult = {
|
|
2045
|
+
summary: `Combined analysis of ${results.length} review files. Total issues found: ${totalIssues}`,
|
|
2046
|
+
totalIssues,
|
|
2047
|
+
issues: allIssues
|
|
2048
|
+
};
|
|
2049
|
+
// Save combined results
|
|
2050
|
+
try {
|
|
2051
|
+
const combinedFilename = getTimestampedReviewFilename();
|
|
2052
|
+
const combinedPath = getOutputPath(outputDirectory, combinedFilename);
|
|
2053
|
+
const combinedContent = `# Combined Review Analysis Result\n\n` + `## Summary\n${analysisResult.summary}\n\n` + `## Total Issues Found\n${totalIssues}\n\n` + `## Files Processed\n${processedFiles.join('\n')}\n\n` + `## Issues\n\n${JSON.stringify(allIssues, null, 2)}\n\n` + `---\n\n*Combined analysis completed at ${new Date().toISOString()}*`;
|
|
2054
|
+
await safeWriteFile(combinedPath, combinedContent);
|
|
2055
|
+
logger.debug('Saved combined review analysis: %s', combinedPath);
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
logger.warn('Failed to save combined review analysis: %s', error.message);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
// Handle GitHub issue creation using the issues module
|
|
2061
|
+
const senditMode = ((_runConfig_review18 = runConfig.review) === null || _runConfig_review18 === void 0 ? void 0 : _runConfig_review18.sendit) || false;
|
|
2062
|
+
return await handleIssueCreation(analysisResult, senditMode);
|
|
2063
|
+
};
|
|
2064
|
+
const execute$1 = async (runConfig)=>{
|
|
2065
|
+
try {
|
|
2066
|
+
return await executeInternal$1(runConfig);
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
const logger = getLogger();
|
|
2069
|
+
if (error instanceof ValidationError) {
|
|
2070
|
+
logger.error(`review failed: ${error.message}`);
|
|
2071
|
+
throw error;
|
|
2072
|
+
}
|
|
2073
|
+
if (error instanceof FileOperationError) {
|
|
2074
|
+
logger.error(`review failed: ${error.message}`);
|
|
2075
|
+
if (error.cause && typeof error.cause === 'object' && 'message' in error.cause) {
|
|
2076
|
+
logger.debug(`Caused by: ${error.cause.message}`);
|
|
2077
|
+
}
|
|
2078
|
+
throw error;
|
|
2079
|
+
}
|
|
2080
|
+
if (error instanceof CommandError) {
|
|
2081
|
+
logger.error(`review failed: ${error.message}`);
|
|
2082
|
+
if (error.cause && typeof error.cause === 'object' && 'message' in error.cause) {
|
|
2083
|
+
logger.debug(`Caused by: ${error.cause.message}`);
|
|
2084
|
+
}
|
|
2085
|
+
throw error;
|
|
2086
|
+
}
|
|
2087
|
+
// Unexpected errors
|
|
2088
|
+
logger.error(`review encountered unexpected error: ${error.message}`);
|
|
2089
|
+
throw error;
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
// Patterns for files that can be auto-resolved
|
|
2094
|
+
const AUTO_RESOLVABLE_PATTERNS = {
|
|
2095
|
+
// Package lock files - just regenerate
|
|
2096
|
+
packageLock: /^package-lock\.json$/,
|
|
2097
|
+
yarnLock: /^yarn\.lock$/,
|
|
2098
|
+
pnpmLock: /^pnpm-lock\.yaml$/,
|
|
2099
|
+
// Generated files - take theirs and regenerate
|
|
2100
|
+
dist: /^dist\//,
|
|
2101
|
+
coverage: /^coverage\//,
|
|
2102
|
+
nodeModules: /^node_modules\//,
|
|
2103
|
+
// Build artifacts
|
|
2104
|
+
buildOutput: /\.(js\.map|d\.ts)$/
|
|
2105
|
+
};
|
|
2106
|
+
/**
|
|
2107
|
+
* Check if a file can be auto-resolved
|
|
2108
|
+
*/ function canAutoResolve(filename) {
|
|
2109
|
+
if (AUTO_RESOLVABLE_PATTERNS.packageLock.test(filename)) {
|
|
2110
|
+
return {
|
|
2111
|
+
canResolve: true,
|
|
2112
|
+
strategy: 'regenerate-lock'
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
if (AUTO_RESOLVABLE_PATTERNS.yarnLock.test(filename)) {
|
|
2116
|
+
return {
|
|
2117
|
+
canResolve: true,
|
|
2118
|
+
strategy: 'regenerate-lock'
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
if (AUTO_RESOLVABLE_PATTERNS.pnpmLock.test(filename)) {
|
|
2122
|
+
return {
|
|
2123
|
+
canResolve: true,
|
|
2124
|
+
strategy: 'regenerate-lock'
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
if (AUTO_RESOLVABLE_PATTERNS.dist.test(filename)) {
|
|
2128
|
+
return {
|
|
2129
|
+
canResolve: true,
|
|
2130
|
+
strategy: 'take-theirs-regenerate'
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
if (AUTO_RESOLVABLE_PATTERNS.coverage.test(filename)) {
|
|
2134
|
+
return {
|
|
2135
|
+
canResolve: true,
|
|
2136
|
+
strategy: 'take-theirs'
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
if (AUTO_RESOLVABLE_PATTERNS.nodeModules.test(filename)) {
|
|
2140
|
+
return {
|
|
2141
|
+
canResolve: true,
|
|
2142
|
+
strategy: 'take-theirs'
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
if (AUTO_RESOLVABLE_PATTERNS.buildOutput.test(filename)) {
|
|
2146
|
+
return {
|
|
2147
|
+
canResolve: true,
|
|
2148
|
+
strategy: 'take-theirs-regenerate'
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
return {
|
|
2152
|
+
canResolve: false,
|
|
2153
|
+
strategy: 'manual'
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Check if this is a package.json version conflict that can be auto-resolved
|
|
2158
|
+
*/ async function tryResolvePackageJsonConflict(filepath, logger) {
|
|
2159
|
+
const storage = createStorage();
|
|
2160
|
+
try {
|
|
2161
|
+
const content = await storage.readFile(filepath, 'utf-8');
|
|
2162
|
+
// Check if this is actually a conflict file
|
|
2163
|
+
if (!content.includes('<<<<<<<') || !content.includes('>>>>>>>')) {
|
|
2164
|
+
return {
|
|
2165
|
+
resolved: false,
|
|
2166
|
+
error: 'Not a conflict file'
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
// Try to parse ours and theirs versions
|
|
2170
|
+
const oursMatch = content.match(/<<<<<<< .*?\n([\s\S]*?)=======\n/);
|
|
2171
|
+
const theirsMatch = content.match(/=======\n([\s\S]*?)>>>>>>> /);
|
|
2172
|
+
if (!oursMatch || !theirsMatch) {
|
|
2173
|
+
return {
|
|
2174
|
+
resolved: false,
|
|
2175
|
+
error: 'Cannot parse conflict markers'
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
// For package.json, if only version differs, take the higher version
|
|
2179
|
+
// This is a simplified heuristic - real conflicts may need more logic
|
|
2180
|
+
const oursPart = oursMatch[1];
|
|
2181
|
+
const theirsPart = theirsMatch[1];
|
|
2182
|
+
// Check if this is just a version conflict
|
|
2183
|
+
const versionPattern = /"version":\s*"([^"]+)"/;
|
|
2184
|
+
const oursVersion = oursPart.match(versionPattern);
|
|
2185
|
+
const theirsVersion = theirsPart.match(versionPattern);
|
|
2186
|
+
if (oursVersion && theirsVersion) {
|
|
2187
|
+
// Both have versions - take higher one
|
|
2188
|
+
const semver = await import('semver');
|
|
2189
|
+
const higher = semver.gt(oursVersion[1].replace(/-.*$/, ''), theirsVersion[1].replace(/-.*$/, '')) ? oursVersion[1] : theirsVersion[1];
|
|
2190
|
+
// Replace the conflicted version with the higher one
|
|
2191
|
+
let resolvedContent = content;
|
|
2192
|
+
// Simple approach: take theirs but use higher version
|
|
2193
|
+
// Remove conflict markers and use theirs as base
|
|
2194
|
+
resolvedContent = content.replace(/<<<<<<< .*?\n[\s\S]*?=======\n([\s\S]*?)>>>>>>> .*?\n/g, '$1');
|
|
2195
|
+
// Update version to higher one
|
|
2196
|
+
resolvedContent = resolvedContent.replace(/"version":\s*"[^"]+"/, `"version": "${higher}"`);
|
|
2197
|
+
await storage.writeFile(filepath, resolvedContent, 'utf-8');
|
|
2198
|
+
logger.info(`PULL_RESOLVED_VERSION: Auto-resolved version conflict | File: ${filepath} | Version: ${higher}`);
|
|
2199
|
+
return {
|
|
2200
|
+
resolved: true
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
return {
|
|
2204
|
+
resolved: false,
|
|
2205
|
+
error: 'Complex conflict - not just version'
|
|
2206
|
+
};
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
return {
|
|
2209
|
+
resolved: false,
|
|
2210
|
+
error: error.message
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Resolve a single conflict file
|
|
2216
|
+
*/ async function resolveConflict(filepath, strategy, logger, isDryRun) {
|
|
2217
|
+
if (isDryRun) {
|
|
2218
|
+
logger.info(`PULL_RESOLVE_DRY_RUN: Would resolve conflict | File: ${filepath} | Strategy: ${strategy}`);
|
|
2219
|
+
return {
|
|
2220
|
+
file: filepath,
|
|
2221
|
+
resolved: true,
|
|
2222
|
+
strategy
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
try {
|
|
2226
|
+
switch(strategy){
|
|
2227
|
+
case 'regenerate-lock':
|
|
2228
|
+
{
|
|
2229
|
+
// Accept theirs and regenerate
|
|
2230
|
+
await runSecure('git', [
|
|
2231
|
+
'checkout',
|
|
2232
|
+
'--theirs',
|
|
2233
|
+
filepath
|
|
2234
|
+
]);
|
|
2235
|
+
await runSecure('git', [
|
|
2236
|
+
'add',
|
|
2237
|
+
filepath
|
|
2238
|
+
]);
|
|
2239
|
+
logger.info(`PULL_CONFLICT_RESOLVED: Accepted remote lock file | File: ${filepath} | Strategy: ${strategy} | Note: Will regenerate after pull`);
|
|
2240
|
+
return {
|
|
2241
|
+
file: filepath,
|
|
2242
|
+
resolved: true,
|
|
2243
|
+
strategy
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
case 'take-theirs':
|
|
2247
|
+
case 'take-theirs-regenerate':
|
|
2248
|
+
{
|
|
2249
|
+
await runSecure('git', [
|
|
2250
|
+
'checkout',
|
|
2251
|
+
'--theirs',
|
|
2252
|
+
filepath
|
|
2253
|
+
]);
|
|
2254
|
+
await runSecure('git', [
|
|
2255
|
+
'add',
|
|
2256
|
+
filepath
|
|
2257
|
+
]);
|
|
2258
|
+
logger.info(`PULL_CONFLICT_RESOLVED: Accepted remote version | File: ${filepath} | Strategy: ${strategy}`);
|
|
2259
|
+
return {
|
|
2260
|
+
file: filepath,
|
|
2261
|
+
resolved: true,
|
|
2262
|
+
strategy
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
case 'version-bump':
|
|
2266
|
+
{
|
|
2267
|
+
const result = await tryResolvePackageJsonConflict(filepath, logger);
|
|
2268
|
+
if (result.resolved) {
|
|
2269
|
+
await runSecure('git', [
|
|
2270
|
+
'add',
|
|
2271
|
+
filepath
|
|
2272
|
+
]);
|
|
2273
|
+
return {
|
|
2274
|
+
file: filepath,
|
|
2275
|
+
resolved: true,
|
|
2276
|
+
strategy
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
return {
|
|
2280
|
+
file: filepath,
|
|
2281
|
+
resolved: false,
|
|
2282
|
+
strategy,
|
|
2283
|
+
error: result.error
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
default:
|
|
2287
|
+
return {
|
|
2288
|
+
file: filepath,
|
|
2289
|
+
resolved: false,
|
|
2290
|
+
strategy,
|
|
2291
|
+
error: 'Unknown resolution strategy'
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
} catch (error) {
|
|
2295
|
+
return {
|
|
2296
|
+
file: filepath,
|
|
2297
|
+
resolved: false,
|
|
2298
|
+
strategy,
|
|
2299
|
+
error: error.message
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Get list of conflicted files
|
|
2305
|
+
*/ async function getConflictedFiles() {
|
|
2306
|
+
try {
|
|
2307
|
+
const { stdout } = await runSecure('git', [
|
|
2308
|
+
'diff',
|
|
2309
|
+
'--name-only',
|
|
2310
|
+
'--diff-filter=U'
|
|
2311
|
+
]);
|
|
2312
|
+
return stdout.trim().split('\n').filter((f)=>f.trim());
|
|
2313
|
+
} catch {
|
|
2314
|
+
return [];
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Try to auto-resolve all conflicts
|
|
2319
|
+
*/ async function autoResolveConflicts(logger, isDryRun) {
|
|
2320
|
+
const conflictedFiles = await getConflictedFiles();
|
|
2321
|
+
const resolved = [];
|
|
2322
|
+
const manual = [];
|
|
2323
|
+
for (const file of conflictedFiles){
|
|
2324
|
+
const { canResolve, strategy } = canAutoResolve(file);
|
|
2325
|
+
// Special handling for package.json
|
|
2326
|
+
if (file === 'package.json' || file.endsWith('/package.json')) {
|
|
2327
|
+
const result = await resolveConflict(file, 'version-bump', logger, isDryRun);
|
|
2328
|
+
if (result.resolved) {
|
|
2329
|
+
resolved.push(file);
|
|
2330
|
+
} else {
|
|
2331
|
+
manual.push(file);
|
|
2332
|
+
}
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
if (canResolve) {
|
|
2336
|
+
const result = await resolveConflict(file, strategy, logger, isDryRun);
|
|
2337
|
+
if (result.resolved) {
|
|
2338
|
+
resolved.push(file);
|
|
2339
|
+
} else {
|
|
2340
|
+
manual.push(file);
|
|
2341
|
+
}
|
|
2342
|
+
} else {
|
|
2343
|
+
manual.push(file);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
resolved,
|
|
2348
|
+
manual
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Stash local changes if any
|
|
2353
|
+
*/ async function stashIfNeeded(logger, isDryRun) {
|
|
2354
|
+
const status = await getGitStatusSummary();
|
|
2355
|
+
if (status.hasUncommittedChanges || status.hasUnstagedFiles) {
|
|
2356
|
+
const changeCount = status.uncommittedCount + status.unstagedCount;
|
|
2357
|
+
logger.info(`PULL_STASHING: Stashing ${changeCount} local changes before pull | Staged: ${status.uncommittedCount} | Unstaged: ${status.unstagedCount}`);
|
|
2358
|
+
if (!isDryRun) {
|
|
2359
|
+
await runSecure('git', [
|
|
2360
|
+
'stash',
|
|
2361
|
+
'push',
|
|
2362
|
+
'-m',
|
|
2363
|
+
`kodrdriv-pull-auto-stash-${Date.now()}`
|
|
2364
|
+
]);
|
|
2365
|
+
}
|
|
2366
|
+
return true;
|
|
2367
|
+
}
|
|
2368
|
+
return false;
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Apply stash if we created one
|
|
2372
|
+
*/ async function applyStashIfNeeded(didStash, logger, isDryRun) {
|
|
2373
|
+
if (!didStash) return false;
|
|
2374
|
+
logger.info('PULL_STASH_POP: Restoring stashed changes');
|
|
2375
|
+
if (!isDryRun) {
|
|
2376
|
+
try {
|
|
2377
|
+
await runSecure('git', [
|
|
2378
|
+
'stash',
|
|
2379
|
+
'pop'
|
|
2380
|
+
]);
|
|
2381
|
+
return true;
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
logger.warn(`PULL_STASH_CONFLICT: Stash pop had conflicts | Error: ${error.message} | Action: Stash preserved, manual intervention needed`);
|
|
2384
|
+
// Don't fail - user can manually resolve stash conflicts
|
|
2385
|
+
return false;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return true;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Regenerate lock files after pull
|
|
2392
|
+
*/ async function regenerateLockFiles(resolvedFiles, logger, isDryRun) {
|
|
2393
|
+
const needsRegenerate = resolvedFiles.some((f)=>f === 'package-lock.json' || f.endsWith('/package-lock.json'));
|
|
2394
|
+
if (needsRegenerate) {
|
|
2395
|
+
logger.info('PULL_REGENERATE_LOCK: Regenerating package-lock.json');
|
|
2396
|
+
if (!isDryRun) {
|
|
2397
|
+
try {
|
|
2398
|
+
await run('npm install');
|
|
2399
|
+
logger.info('PULL_REGENERATE_SUCCESS: Lock file regenerated successfully');
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
logger.warn(`PULL_REGENERATE_FAILED: Failed to regenerate lock file | Error: ${error.message}`);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Main pull execution
|
|
2408
|
+
*/ async function executePull(remote, branch, logger, isDryRun) {
|
|
2409
|
+
const currentBranch = await getCurrentBranch();
|
|
2410
|
+
const targetBranch = branch || currentBranch;
|
|
2411
|
+
logger.info(`PULL_STARTING: Pulling changes | Remote: ${remote} | Branch: ${targetBranch} | Current: ${currentBranch}`);
|
|
2412
|
+
// Step 1: Stash any local changes
|
|
2413
|
+
const didStash = await stashIfNeeded(logger, isDryRun);
|
|
2414
|
+
// Step 2: Fetch first to see what's coming
|
|
2415
|
+
logger.info(`PULL_FETCH: Fetching from ${remote}`);
|
|
2416
|
+
if (!isDryRun) {
|
|
2417
|
+
try {
|
|
2418
|
+
await runSecure('git', [
|
|
2419
|
+
'fetch',
|
|
2420
|
+
remote,
|
|
2421
|
+
targetBranch
|
|
2422
|
+
]);
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
logger.error(`PULL_FETCH_FAILED: Failed to fetch | Error: ${error.message}`);
|
|
2425
|
+
if (didStash) await applyStashIfNeeded(true, logger, isDryRun);
|
|
2426
|
+
return {
|
|
2427
|
+
success: false,
|
|
2428
|
+
hadConflicts: false,
|
|
2429
|
+
autoResolved: [],
|
|
2430
|
+
manualRequired: [],
|
|
2431
|
+
stashApplied: didStash,
|
|
2432
|
+
strategy: 'failed',
|
|
2433
|
+
message: `Fetch failed: ${error.message}`
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
// Step 3: Try fast-forward first
|
|
2438
|
+
logger.info('PULL_STRATEGY: Attempting fast-forward merge');
|
|
2439
|
+
if (!isDryRun) {
|
|
2440
|
+
try {
|
|
2441
|
+
await runSecure('git', [
|
|
2442
|
+
'merge',
|
|
2443
|
+
'--ff-only',
|
|
2444
|
+
`${remote}/${targetBranch}`
|
|
2445
|
+
]);
|
|
2446
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2447
|
+
logger.info('PULL_SUCCESS: Fast-forward merge successful');
|
|
2448
|
+
return {
|
|
2449
|
+
success: true,
|
|
2450
|
+
hadConflicts: false,
|
|
2451
|
+
autoResolved: [],
|
|
2452
|
+
manualRequired: [],
|
|
2453
|
+
stashApplied: didStash,
|
|
2454
|
+
strategy: 'fast-forward',
|
|
2455
|
+
message: 'Fast-forward merge successful'
|
|
2456
|
+
};
|
|
2457
|
+
} catch {
|
|
2458
|
+
logger.info('PULL_FF_FAILED: Fast-forward not possible, trying rebase');
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
// Step 4: Try rebase
|
|
2462
|
+
logger.info('PULL_STRATEGY: Attempting rebase');
|
|
2463
|
+
if (!isDryRun) {
|
|
2464
|
+
try {
|
|
2465
|
+
await runSecure('git', [
|
|
2466
|
+
'rebase',
|
|
2467
|
+
`${remote}/${targetBranch}`
|
|
2468
|
+
]);
|
|
2469
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2470
|
+
logger.info('PULL_SUCCESS: Rebase successful');
|
|
2471
|
+
return {
|
|
2472
|
+
success: true,
|
|
2473
|
+
hadConflicts: false,
|
|
2474
|
+
autoResolved: [],
|
|
2475
|
+
manualRequired: [],
|
|
2476
|
+
stashApplied: didStash,
|
|
2477
|
+
strategy: 'rebase',
|
|
2478
|
+
message: 'Rebase successful'
|
|
2479
|
+
};
|
|
2480
|
+
} catch {
|
|
2481
|
+
// Check if rebase is in progress with conflicts
|
|
2482
|
+
const conflictedFiles = await getConflictedFiles();
|
|
2483
|
+
if (conflictedFiles.length > 0) {
|
|
2484
|
+
logger.info(`PULL_CONFLICTS: Rebase has ${conflictedFiles.length} conflicts, attempting auto-resolution`);
|
|
2485
|
+
// Step 5: Try to auto-resolve conflicts
|
|
2486
|
+
const { resolved, manual } = await autoResolveConflicts(logger, isDryRun);
|
|
2487
|
+
if (manual.length === 0) {
|
|
2488
|
+
// All conflicts resolved, continue rebase
|
|
2489
|
+
logger.info('PULL_ALL_RESOLVED: All conflicts auto-resolved, continuing rebase');
|
|
2490
|
+
try {
|
|
2491
|
+
await runSecure('git', [
|
|
2492
|
+
'rebase',
|
|
2493
|
+
'--continue'
|
|
2494
|
+
]);
|
|
2495
|
+
await regenerateLockFiles(resolved, logger, isDryRun);
|
|
2496
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2497
|
+
return {
|
|
2498
|
+
success: true,
|
|
2499
|
+
hadConflicts: true,
|
|
2500
|
+
autoResolved: resolved,
|
|
2501
|
+
manualRequired: [],
|
|
2502
|
+
stashApplied: didStash,
|
|
2503
|
+
strategy: 'rebase',
|
|
2504
|
+
message: `Rebase successful with ${resolved.length} auto-resolved conflicts`
|
|
2505
|
+
};
|
|
2506
|
+
} catch (continueError) {
|
|
2507
|
+
logger.warn(`PULL_CONTINUE_FAILED: Rebase continue failed | Error: ${continueError.message}`);
|
|
2508
|
+
}
|
|
2509
|
+
} else {
|
|
2510
|
+
// Some conflicts need manual resolution
|
|
2511
|
+
logger.warn(`PULL_MANUAL_REQUIRED: ${manual.length} conflicts require manual resolution`);
|
|
2512
|
+
logger.warn('PULL_MANUAL_FILES: Files needing manual resolution:');
|
|
2513
|
+
manual.forEach((f)=>logger.warn(` - ${f}`));
|
|
2514
|
+
logger.info('PULL_HINT: After resolving conflicts manually, run: git rebase --continue');
|
|
2515
|
+
// Keep rebase in progress so user can finish
|
|
2516
|
+
return {
|
|
2517
|
+
success: false,
|
|
2518
|
+
hadConflicts: true,
|
|
2519
|
+
autoResolved: resolved,
|
|
2520
|
+
manualRequired: manual,
|
|
2521
|
+
stashApplied: false,
|
|
2522
|
+
strategy: 'rebase',
|
|
2523
|
+
message: `Rebase paused: ${manual.length} files need manual conflict resolution`
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
// Rebase failed for other reason, abort and try merge
|
|
2528
|
+
logger.info('PULL_REBASE_ABORT: Rebase failed, aborting and trying merge');
|
|
2529
|
+
try {
|
|
2530
|
+
await runSecure('git', [
|
|
2531
|
+
'rebase',
|
|
2532
|
+
'--abort'
|
|
2533
|
+
]);
|
|
2534
|
+
} catch {
|
|
2535
|
+
// Ignore abort errors
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
// Step 6: Fall back to regular merge
|
|
2541
|
+
logger.info('PULL_STRATEGY: Attempting merge');
|
|
2542
|
+
if (!isDryRun) {
|
|
2543
|
+
try {
|
|
2544
|
+
await runSecure('git', [
|
|
2545
|
+
'merge',
|
|
2546
|
+
`${remote}/${targetBranch}`
|
|
2547
|
+
]);
|
|
2548
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2549
|
+
logger.info('PULL_SUCCESS: Merge successful');
|
|
2550
|
+
return {
|
|
2551
|
+
success: true,
|
|
2552
|
+
hadConflicts: false,
|
|
2553
|
+
autoResolved: [],
|
|
2554
|
+
manualRequired: [],
|
|
2555
|
+
stashApplied: didStash,
|
|
2556
|
+
strategy: 'merge',
|
|
2557
|
+
message: 'Merge successful'
|
|
2558
|
+
};
|
|
2559
|
+
} catch {
|
|
2560
|
+
// Check for merge conflicts
|
|
2561
|
+
const conflictedFiles = await getConflictedFiles();
|
|
2562
|
+
if (conflictedFiles.length > 0) {
|
|
2563
|
+
logger.info(`PULL_CONFLICTS: Merge has ${conflictedFiles.length} conflicts, attempting auto-resolution`);
|
|
2564
|
+
const { resolved, manual } = await autoResolveConflicts(logger, isDryRun);
|
|
2565
|
+
if (manual.length === 0) {
|
|
2566
|
+
// All conflicts resolved, commit the merge
|
|
2567
|
+
logger.info('PULL_ALL_RESOLVED: All conflicts auto-resolved, completing merge');
|
|
2568
|
+
try {
|
|
2569
|
+
await runSecure('git', [
|
|
2570
|
+
'commit',
|
|
2571
|
+
'-m',
|
|
2572
|
+
`Merge ${remote}/${targetBranch} (auto-resolved by kodrdriv)`
|
|
2573
|
+
]);
|
|
2574
|
+
await regenerateLockFiles(resolved, logger, isDryRun);
|
|
2575
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2576
|
+
return {
|
|
2577
|
+
success: true,
|
|
2578
|
+
hadConflicts: true,
|
|
2579
|
+
autoResolved: resolved,
|
|
2580
|
+
manualRequired: [],
|
|
2581
|
+
stashApplied: didStash,
|
|
2582
|
+
strategy: 'merge',
|
|
2583
|
+
message: `Merge successful with ${resolved.length} auto-resolved conflicts`
|
|
2584
|
+
};
|
|
2585
|
+
} catch (commitError) {
|
|
2586
|
+
logger.error(`PULL_COMMIT_FAILED: Merge commit failed | Error: ${commitError.message}`);
|
|
2587
|
+
}
|
|
2588
|
+
} else {
|
|
2589
|
+
logger.warn(`PULL_MANUAL_REQUIRED: ${manual.length} conflicts require manual resolution`);
|
|
2590
|
+
manual.forEach((f)=>logger.warn(` - ${f}`));
|
|
2591
|
+
logger.info('PULL_HINT: After resolving conflicts manually, run: git commit');
|
|
2592
|
+
return {
|
|
2593
|
+
success: false,
|
|
2594
|
+
hadConflicts: true,
|
|
2595
|
+
autoResolved: resolved,
|
|
2596
|
+
manualRequired: manual,
|
|
2597
|
+
stashApplied: false,
|
|
2598
|
+
strategy: 'merge',
|
|
2599
|
+
message: `Merge paused: ${manual.length} files need manual conflict resolution`
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
// If we got here, something went wrong
|
|
2606
|
+
if (didStash) {
|
|
2607
|
+
logger.warn('PULL_STASH_PRESERVED: Local changes still stashed, use "git stash pop" to restore');
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
success: false,
|
|
2611
|
+
hadConflicts: false,
|
|
2612
|
+
autoResolved: [],
|
|
2613
|
+
manualRequired: [],
|
|
2614
|
+
stashApplied: false,
|
|
2615
|
+
strategy: 'failed',
|
|
2616
|
+
message: 'Pull failed - unable to merge or rebase'
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* Internal execution
|
|
2621
|
+
*/ const executeInternal = async (runConfig)=>{
|
|
2622
|
+
const isDryRun = runConfig.dryRun || false;
|
|
2623
|
+
const logger = getDryRunLogger(isDryRun);
|
|
2624
|
+
// Get pull configuration
|
|
2625
|
+
const pullConfig = runConfig.pull || {};
|
|
2626
|
+
const remote = pullConfig.remote || 'origin';
|
|
2627
|
+
const branch = pullConfig.branch;
|
|
2628
|
+
// Execute pull
|
|
2629
|
+
const result = await executePull(remote, branch, logger, isDryRun);
|
|
2630
|
+
// Format output
|
|
2631
|
+
const lines = [];
|
|
2632
|
+
lines.push('');
|
|
2633
|
+
lines.push('═'.repeat(60));
|
|
2634
|
+
lines.push(result.success ? '✅ PULL COMPLETE' : '⚠️ PULL NEEDS ATTENTION');
|
|
2635
|
+
lines.push('═'.repeat(60));
|
|
2636
|
+
lines.push('');
|
|
2637
|
+
lines.push(`Strategy: ${result.strategy}`);
|
|
2638
|
+
lines.push(`Message: ${result.message}`);
|
|
2639
|
+
if (result.hadConflicts) {
|
|
2640
|
+
lines.push('');
|
|
2641
|
+
lines.push(`Conflicts detected: ${result.autoResolved.length + result.manualRequired.length}`);
|
|
2642
|
+
if (result.autoResolved.length > 0) {
|
|
2643
|
+
lines.push(`✓ Auto-resolved: ${result.autoResolved.length}`);
|
|
2644
|
+
result.autoResolved.forEach((f)=>lines.push(` - ${f}`));
|
|
2645
|
+
}
|
|
2646
|
+
if (result.manualRequired.length > 0) {
|
|
2647
|
+
lines.push(`✗ Manual resolution needed: ${result.manualRequired.length}`);
|
|
2648
|
+
result.manualRequired.forEach((f)=>lines.push(` - ${f}`));
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (result.stashApplied) {
|
|
2652
|
+
lines.push('');
|
|
2653
|
+
lines.push('ℹ️ Local changes have been restored from stash');
|
|
2654
|
+
}
|
|
2655
|
+
lines.push('');
|
|
2656
|
+
lines.push('═'.repeat(60));
|
|
2657
|
+
const output = lines.join('\n');
|
|
2658
|
+
logger.info(output);
|
|
2659
|
+
return output;
|
|
2660
|
+
};
|
|
2661
|
+
/**
|
|
2662
|
+
* Execute pull command
|
|
2663
|
+
*/ const execute = async (runConfig)=>{
|
|
2664
|
+
try {
|
|
2665
|
+
return await executeInternal(runConfig);
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
const logger = getLogger();
|
|
2668
|
+
logger.error(`PULL_COMMAND_FAILED: Pull command failed | Error: ${error.message}`);
|
|
2669
|
+
throw error;
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
const logger = getLogger();
|
|
2674
|
+
// Cache file to store test run timestamps per package
|
|
2675
|
+
const TEST_CACHE_FILE = '.kodrdriv-test-cache.json';
|
|
2676
|
+
/**
|
|
2677
|
+
* Load test cache from disk
|
|
2678
|
+
*/ async function loadTestCache(packageDir) {
|
|
2679
|
+
const cachePath = path.join(packageDir, TEST_CACHE_FILE);
|
|
2680
|
+
try {
|
|
2681
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
2682
|
+
return JSON.parse(content);
|
|
2683
|
+
} catch {
|
|
2684
|
+
return {};
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Save test cache to disk
|
|
2689
|
+
*/ async function saveTestCache(packageDir, cache) {
|
|
2690
|
+
const cachePath = path.join(packageDir, TEST_CACHE_FILE);
|
|
2691
|
+
try {
|
|
2692
|
+
await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
logger.debug(`Failed to save test cache: ${error.message}`);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Get the current git commit hash
|
|
2699
|
+
*/ async function getCurrentCommitHash(packageDir) {
|
|
2700
|
+
try {
|
|
2701
|
+
const { stdout } = await runSecure('git', [
|
|
2702
|
+
'rev-parse',
|
|
2703
|
+
'HEAD'
|
|
2704
|
+
], {
|
|
2705
|
+
cwd: packageDir
|
|
2706
|
+
});
|
|
2707
|
+
return stdout.trim();
|
|
2708
|
+
} catch {
|
|
2709
|
+
return null;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Check if source files have changed since the last test run
|
|
2714
|
+
*/ async function hasSourceFilesChanged(packageDir, lastCommitHash) {
|
|
2715
|
+
if (!lastCommitHash) {
|
|
2716
|
+
return {
|
|
2717
|
+
changed: true,
|
|
2718
|
+
reason: 'No previous test run recorded'
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
try {
|
|
2722
|
+
// Get current commit hash
|
|
2723
|
+
const currentCommitHash = await getCurrentCommitHash(packageDir);
|
|
2724
|
+
if (!currentCommitHash) {
|
|
2725
|
+
return {
|
|
2726
|
+
changed: true,
|
|
2727
|
+
reason: 'Not in a git repository'
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
// If commit hash changed, files definitely changed
|
|
2731
|
+
if (currentCommitHash !== lastCommitHash) {
|
|
2732
|
+
return {
|
|
2733
|
+
changed: true,
|
|
2734
|
+
reason: `Commit hash changed: ${lastCommitHash.substring(0, 7)} -> ${currentCommitHash.substring(0, 7)}`
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
// Check if there are any uncommitted changes to source files
|
|
2738
|
+
const { stdout } = await runSecure('git', [
|
|
2739
|
+
'status',
|
|
2740
|
+
'--porcelain'
|
|
2741
|
+
], {
|
|
2742
|
+
cwd: packageDir
|
|
2743
|
+
});
|
|
2744
|
+
const changedFiles = stdout.split('\n').filter((line)=>line.trim()).map((line)=>line.substring(3).trim()).filter((file)=>{
|
|
2745
|
+
// Only consider source files, not build artifacts or config files
|
|
2746
|
+
const ext = path.extname(file);
|
|
2747
|
+
return(// TypeScript/JavaScript source files
|
|
2748
|
+
[
|
|
2749
|
+
'.ts',
|
|
2750
|
+
'.tsx',
|
|
2751
|
+
'.js',
|
|
2752
|
+
'.jsx'
|
|
2753
|
+
].includes(ext) || // Test files
|
|
2754
|
+
file.includes('.test.') || file.includes('.spec.') || // Config files that affect build/test
|
|
2755
|
+
[
|
|
2756
|
+
'tsconfig.json',
|
|
2757
|
+
'vite.config.ts',
|
|
2758
|
+
'vitest.config.ts',
|
|
2759
|
+
'package.json'
|
|
2760
|
+
].includes(path.basename(file)));
|
|
2761
|
+
});
|
|
2762
|
+
if (changedFiles.length > 0) {
|
|
2763
|
+
return {
|
|
2764
|
+
changed: true,
|
|
2765
|
+
reason: `Uncommitted changes in: ${changedFiles.slice(0, 3).join(', ')}${changedFiles.length > 3 ? '...' : ''}`
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
return {
|
|
2769
|
+
changed: false,
|
|
2770
|
+
reason: 'No source file changes detected'
|
|
2771
|
+
};
|
|
2772
|
+
} catch (error) {
|
|
2773
|
+
logger.debug(`Error checking for source file changes: ${error.message}`);
|
|
2774
|
+
// Conservative: assume changed if we can't verify
|
|
2775
|
+
return {
|
|
2776
|
+
changed: true,
|
|
2777
|
+
reason: `Could not verify changes: ${error.message}`
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* Check if dist directory needs to be cleaned (is outdated compared to source files)
|
|
2783
|
+
*/ async function isCleanNeeded(packageDir) {
|
|
2784
|
+
const storage = createStorage();
|
|
2785
|
+
const distPath = path.join(packageDir, 'dist');
|
|
2786
|
+
try {
|
|
2787
|
+
// Check if dist directory exists
|
|
2788
|
+
const distExists = await storage.exists('dist');
|
|
2789
|
+
if (!distExists) {
|
|
2790
|
+
return {
|
|
2791
|
+
needed: false,
|
|
2792
|
+
reason: 'dist directory does not exist'
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
// Get dist directory modification time
|
|
2796
|
+
const distStats = await fs.stat(distPath);
|
|
2797
|
+
const distMtime = distStats.mtimeMs;
|
|
2798
|
+
// Use git to find source files that are newer than dist
|
|
2799
|
+
try {
|
|
2800
|
+
// Get all tracked source files
|
|
2801
|
+
const { stdout: trackedFiles } = await runSecure('git', [
|
|
2802
|
+
'ls-files'
|
|
2803
|
+
], {
|
|
2804
|
+
cwd: packageDir
|
|
2805
|
+
});
|
|
2806
|
+
const files = trackedFiles.split('\n').filter(Boolean);
|
|
2807
|
+
// Check if any source files are newer than dist
|
|
2808
|
+
for (const file of files){
|
|
2809
|
+
const ext = path.extname(file);
|
|
2810
|
+
if (![
|
|
2811
|
+
'.ts',
|
|
2812
|
+
'.tsx',
|
|
2813
|
+
'.js',
|
|
2814
|
+
'.jsx',
|
|
2815
|
+
'.json'
|
|
2816
|
+
].includes(ext)) {
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
// Skip dist files
|
|
2820
|
+
if (file.startsWith('dist/')) {
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
try {
|
|
2824
|
+
const filePath = path.join(packageDir, file);
|
|
2825
|
+
const fileStats = await fs.stat(filePath);
|
|
2826
|
+
if (fileStats.mtimeMs > distMtime) {
|
|
2827
|
+
return {
|
|
2828
|
+
needed: true,
|
|
2829
|
+
reason: `${file} is newer than dist directory`
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
} catch {
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
needed: false,
|
|
2838
|
+
reason: 'dist directory is up to date with source files'
|
|
2839
|
+
};
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
// If git check fails, fall back to checking common source directories
|
|
2842
|
+
logger.debug(`Git-based check failed, using fallback: ${error.message}`);
|
|
2843
|
+
const sourceDirs = [
|
|
2844
|
+
'src',
|
|
2845
|
+
'tests'
|
|
2846
|
+
];
|
|
2847
|
+
for (const dir of sourceDirs){
|
|
2848
|
+
const dirPath = path.join(packageDir, dir);
|
|
2849
|
+
try {
|
|
2850
|
+
const dirStats = await fs.stat(dirPath);
|
|
2851
|
+
if (dirStats.mtimeMs > distMtime) {
|
|
2852
|
+
return {
|
|
2853
|
+
needed: true,
|
|
2854
|
+
reason: `${dir} directory is newer than dist`
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
} catch {
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2213
2860
|
}
|
|
2861
|
+
// Conservative: if we can't verify, assume clean is needed
|
|
2862
|
+
return {
|
|
2863
|
+
needed: true,
|
|
2864
|
+
reason: 'Could not verify dist freshness, cleaning to be safe'
|
|
2865
|
+
};
|
|
2214
2866
|
}
|
|
2215
|
-
reviewFiles = [
|
|
2216
|
-
'editor input'
|
|
2217
|
-
];
|
|
2218
|
-
}
|
|
2219
|
-
if (!reviewNote || !reviewNote.trim()) {
|
|
2220
|
-
throw new ValidationError('No review note provided or captured');
|
|
2221
|
-
}
|
|
2222
|
-
logger.info('📝 Starting review analysis...');
|
|
2223
|
-
logger.debug('Review note: %s', reviewNote);
|
|
2224
|
-
logger.debug('Review note length: %d characters', reviewNote.length);
|
|
2225
|
-
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
2226
|
-
const storage = createStorage();
|
|
2227
|
-
await storage.ensureDirectory(outputDirectory);
|
|
2228
|
-
// Save timestamped copy of review notes to output directory
|
|
2229
|
-
try {
|
|
2230
|
-
const reviewNotesFilename = getTimestampedReviewNotesFilename();
|
|
2231
|
-
const reviewNotesPath = getOutputPath(outputDirectory, reviewNotesFilename);
|
|
2232
|
-
const reviewNotesContent = `# Review Notes\n\n${reviewNote}\n\n`;
|
|
2233
|
-
await safeWriteFile(reviewNotesPath, reviewNotesContent);
|
|
2234
|
-
logger.debug('Saved timestamped review notes: %s', reviewNotesPath);
|
|
2235
2867
|
} catch (error) {
|
|
2236
|
-
logger.
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
selectedFiles = await selectFilesForProcessing(reviewFiles, ((_runConfig_review26 = runConfig.review) === null || _runConfig_review26 === void 0 ? void 0 : _runConfig_review26.sendit) || false);
|
|
2243
|
-
} else {
|
|
2244
|
-
// For single note mode, just use the note directly
|
|
2245
|
-
selectedFiles = [
|
|
2246
|
-
'single note'
|
|
2247
|
-
];
|
|
2868
|
+
logger.debug(`Error checking if clean is needed: ${error.message}`);
|
|
2869
|
+
// Conservative: assume clean is needed if we can't check
|
|
2870
|
+
return {
|
|
2871
|
+
needed: true,
|
|
2872
|
+
reason: `Could not verify: ${error.message}`
|
|
2873
|
+
};
|
|
2248
2874
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
//
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
}
|
|
2264
|
-
// Check if this is a critical error that should be propagated
|
|
2265
|
-
if (error.message.includes('Too many context gathering errors')) {
|
|
2266
|
-
throw error; // Propagate critical context errors
|
|
2267
|
-
}
|
|
2268
|
-
logger.warn(`Failed to process file ${filePath}: ${error.message}`);
|
|
2269
|
-
// Continue with other files for non-critical errors
|
|
2270
|
-
}
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Check if tests need to be run (source files changed since last test run)
|
|
2878
|
+
*/ async function isTestNeeded(packageDir) {
|
|
2879
|
+
try {
|
|
2880
|
+
// Load test cache
|
|
2881
|
+
const cache = await loadTestCache(packageDir);
|
|
2882
|
+
const cacheKey = packageDir;
|
|
2883
|
+
// Check if we have a cached test run for this package
|
|
2884
|
+
const cached = cache[cacheKey];
|
|
2885
|
+
if (!cached) {
|
|
2886
|
+
return {
|
|
2887
|
+
needed: true,
|
|
2888
|
+
reason: 'No previous test run recorded'
|
|
2889
|
+
};
|
|
2271
2890
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
} catch (error) {
|
|
2280
|
-
logger.warn(`Failed to process review note: ${error.message}`);
|
|
2281
|
-
throw error; // Re-throw for single note mode since there's only one item
|
|
2891
|
+
// Check if source files have changed since last test run
|
|
2892
|
+
const changeCheck = await hasSourceFilesChanged(packageDir, cached.lastCommitHash);
|
|
2893
|
+
if (changeCheck.changed) {
|
|
2894
|
+
return {
|
|
2895
|
+
needed: true,
|
|
2896
|
+
reason: changeCheck.reason
|
|
2897
|
+
};
|
|
2282
2898
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
// Create a combined summary
|
|
2294
|
-
const totalIssues = results.reduce((sum, result)=>sum + result.totalIssues, 0);
|
|
2295
|
-
const allIssues = results.flatMap((result)=>result.issues || []);
|
|
2296
|
-
analysisResult = {
|
|
2297
|
-
summary: `Combined analysis of ${results.length} review files. Total issues found: ${totalIssues}`,
|
|
2298
|
-
totalIssues,
|
|
2299
|
-
issues: allIssues
|
|
2899
|
+
return {
|
|
2900
|
+
needed: false,
|
|
2901
|
+
reason: 'No source file changes since last test run'
|
|
2902
|
+
};
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
logger.debug(`Error checking if test is needed: ${error.message}`);
|
|
2905
|
+
// Conservative: assume test is needed if we can't check
|
|
2906
|
+
return {
|
|
2907
|
+
needed: true,
|
|
2908
|
+
reason: `Could not verify: ${error.message}`
|
|
2300
2909
|
};
|
|
2301
|
-
// Save combined results
|
|
2302
|
-
try {
|
|
2303
|
-
const combinedFilename = getTimestampedReviewFilename();
|
|
2304
|
-
const combinedPath = getOutputPath(outputDirectory, combinedFilename);
|
|
2305
|
-
const combinedContent = `# Combined Review Analysis Result\n\n` + `## Summary\n${analysisResult.summary}\n\n` + `## Total Issues Found\n${totalIssues}\n\n` + `## Files Processed\n${processedFiles.join('\n')}\n\n` + `## Issues\n\n${JSON.stringify(allIssues, null, 2)}\n\n` + `---\n\n*Combined analysis completed at ${new Date().toISOString()}*`;
|
|
2306
|
-
await safeWriteFile(combinedPath, combinedContent);
|
|
2307
|
-
logger.debug('Saved combined review analysis: %s', combinedPath);
|
|
2308
|
-
} catch (error) {
|
|
2309
|
-
logger.warn('Failed to save combined review analysis: %s', error.message);
|
|
2310
|
-
}
|
|
2311
2910
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
const execute = async (runConfig)=>{
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Record that tests were run for this package
|
|
2914
|
+
*/ async function recordTestRun(packageDir) {
|
|
2317
2915
|
try {
|
|
2318
|
-
|
|
2916
|
+
const cache = await loadTestCache(packageDir);
|
|
2917
|
+
const cacheKey = packageDir;
|
|
2918
|
+
const commitHash = await getCurrentCommitHash(packageDir);
|
|
2919
|
+
cache[cacheKey] = {
|
|
2920
|
+
lastTestRun: Date.now(),
|
|
2921
|
+
lastCommitHash: commitHash || 'unknown'
|
|
2922
|
+
};
|
|
2923
|
+
await saveTestCache(packageDir, cache);
|
|
2319
2924
|
} catch (error) {
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2925
|
+
logger.debug(`Failed to record test run: ${error.message}`);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Optimize a precommit command by skipping unnecessary steps
|
|
2930
|
+
* Returns the optimized command and information about what was skipped
|
|
2931
|
+
*/ async function optimizePrecommitCommand(packageDir, originalCommand, options = {}) {
|
|
2932
|
+
const { skipClean = true, skipTest = true } = options;
|
|
2933
|
+
// Parse the original command to extract individual scripts
|
|
2934
|
+
// Common patterns: "npm run precommit", "npm run clean && npm run build && npm run lint && npm run test"
|
|
2935
|
+
const isPrecommitScript = originalCommand.includes('precommit') || originalCommand.includes('pre-commit');
|
|
2936
|
+
let optimizedCommand = originalCommand;
|
|
2937
|
+
const skipped = {
|
|
2938
|
+
clean: false,
|
|
2939
|
+
test: false
|
|
2940
|
+
};
|
|
2941
|
+
const reasons = {};
|
|
2942
|
+
// If it's a precommit script, we need to check what it actually runs
|
|
2943
|
+
// For now, we'll optimize the common pattern: clean && build && lint && test
|
|
2944
|
+
if (isPrecommitScript || originalCommand.includes('clean')) {
|
|
2945
|
+
if (skipClean) {
|
|
2946
|
+
const cleanCheck = await isCleanNeeded(packageDir);
|
|
2947
|
+
if (!cleanCheck.needed) {
|
|
2948
|
+
// Remove clean from the command
|
|
2949
|
+
optimizedCommand = optimizedCommand.replace(/npm\s+run\s+clean\s+&&\s*/g, '').replace(/npm\s+run\s+clean\s+/g, '').replace(/\s*&&\s*npm\s+run\s+clean/g, '').trim();
|
|
2950
|
+
skipped.clean = true;
|
|
2951
|
+
reasons.clean = cleanCheck.reason;
|
|
2329
2952
|
}
|
|
2330
|
-
throw error;
|
|
2331
2953
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2954
|
+
}
|
|
2955
|
+
if (isPrecommitScript || originalCommand.includes('test')) {
|
|
2956
|
+
if (skipTest) {
|
|
2957
|
+
const testCheck = await isTestNeeded(packageDir);
|
|
2958
|
+
if (!testCheck.needed) {
|
|
2959
|
+
// Remove test from the command
|
|
2960
|
+
optimizedCommand = optimizedCommand.replace(/\s*&&\s*npm\s+run\s+test\s*/g, '').replace(/\s*&&\s*npm\s+run\s+test$/g, '').replace(/npm\s+run\s+test\s+&&\s*/g, '').trim();
|
|
2961
|
+
skipped.test = true;
|
|
2962
|
+
reasons.test = testCheck.reason;
|
|
2336
2963
|
}
|
|
2337
|
-
throw error;
|
|
2338
2964
|
}
|
|
2339
|
-
// Unexpected errors
|
|
2340
|
-
logger.error(`review encountered unexpected error: ${error.message}`);
|
|
2341
|
-
throw error;
|
|
2342
2965
|
}
|
|
2343
|
-
|
|
2966
|
+
// Clean up any double && or trailing &&
|
|
2967
|
+
optimizedCommand = optimizedCommand.replace(/\s*&&\s*&&/g, ' && ').replace(/&&\s*$/, '').trim();
|
|
2968
|
+
return {
|
|
2969
|
+
optimizedCommand,
|
|
2970
|
+
skipped,
|
|
2971
|
+
reasons
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2344
2974
|
|
|
2345
|
-
export { PerformanceTimer, batchReadPackageJsonFiles, checkForFileDependencies, execute$
|
|
2975
|
+
export { PerformanceTimer, batchReadPackageJsonFiles, checkForFileDependencies, execute$2 as clean, collectAllDependencies, execute$4 as commit, findAllPackageJsonFiles, findPackagesByScope, isCleanNeeded, isTestNeeded, optimizePrecommitCommand, execute$3 as precommit, execute as pull, recordTestRun, execute$1 as review, scanDirectoryForPackages };
|
|
2346
2976
|
//# sourceMappingURL=index.js.map
|