From 07d5c6f0f1e6453009faad822560752e028438b7 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 6 Dec 2025 18:52:03 -0700 Subject: [PATCH 1/4] refactor: remove voice/speak commands and bloat features, add Windows compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unused voice narration, CodeRabbit integration, and interactive command features that added complexity without core value (3,095 lines removed). Simplify the command structure while maintaining security validation capabilities. Add Windows-specific path handling and executable detection in ComponentSecurityValidator to ensure cross-platform compatibility. Windows path normalization and file extension-based executable detection replace Unix-only is_executable() checks. Include illuminate/log and symfony/filesystem as explicit dependencies. • Removed: CodeRabbit commands, voice narration system, user preferences command, interactive mode • Removed: 8 command files, 2 narrator implementations, 3 service classes, 4 value objects, 4 enum types • Added: Windows path normalization using Symfony\Filesystem\Path • Added: Cross-platform executable detection with extension-based fallback • Updated: Test fixtures to skip Unix-specific assertions on Windows • Dependencies: +illuminate/log (^11.0), +symfony/filesystem (^7.4) --- app/Commands/CodeRabbitSpeakCommand.php | 187 ------- app/Commands/CodeRabbitStatusCommand.php | 313 ------------ .../GitHubClientGapAnalysisCommand.php | 211 -------- app/Commands/InteractiveCommand.php | 84 ---- app/Commands/IssuesSpeakCommand.php | 282 ----------- app/Commands/PrsSpeakCommand.php | 317 ------------ app/Commands/UserPreferencesCommand.php | 186 ------- app/Commands/VoiceCommand.php | 235 --------- app/Contracts/VoiceNarratorInterface.php | 15 - app/Enums/SpeechSpeed.php | 13 - app/Enums/VoiceStyle.php | 19 - app/Narrators/ClaudeNarrator.php | 31 -- app/Narrators/DefaultNarrator.php | 98 ---- app/Providers/AppServiceProvider.php | 6 - app/Services/ClaudeNarrationService.php | 135 ----- app/Services/CodeRabbitAnalysisService.php | 294 ----------- app/Services/GitHubClientGapTracker.php | 471 ------------------ .../Security/ComponentSecurityValidator.php | 53 +- app/Services/VoiceNarrationService.php | 160 ------ app/ValueObjects/CodeRabbitAnalysis.php | 82 --- app/ValueObjects/NarrationContent.php | 148 ------ app/ValueObjects/SpeechConfiguration.php | 35 -- composer.json | 2 + composer.lock | 423 +++++++++++++--- tests/Feature/ComponentSecurityTest.php | 26 +- tests/Unit/Enums/SpeechSpeedTest.php | 21 - tests/Unit/Enums/VoiceStyleTest.php | 21 - .../ComponentSecurityValidatorTest.php | 82 +-- .../ValueObjects/SpeechConfigurationTest.php | 39 -- 29 files changed, 447 insertions(+), 3542 deletions(-) delete mode 100644 app/Commands/CodeRabbitSpeakCommand.php delete mode 100644 app/Commands/CodeRabbitStatusCommand.php delete mode 100644 app/Commands/GitHubClientGapAnalysisCommand.php delete mode 100644 app/Commands/InteractiveCommand.php delete mode 100644 app/Commands/IssuesSpeakCommand.php delete mode 100644 app/Commands/PrsSpeakCommand.php delete mode 100644 app/Commands/UserPreferencesCommand.php delete mode 100644 app/Commands/VoiceCommand.php delete mode 100644 app/Contracts/VoiceNarratorInterface.php delete mode 100644 app/Enums/SpeechSpeed.php delete mode 100644 app/Enums/VoiceStyle.php delete mode 100644 app/Narrators/ClaudeNarrator.php delete mode 100644 app/Narrators/DefaultNarrator.php delete mode 100644 app/Services/ClaudeNarrationService.php delete mode 100644 app/Services/CodeRabbitAnalysisService.php delete mode 100644 app/Services/GitHubClientGapTracker.php delete mode 100644 app/Services/VoiceNarrationService.php delete mode 100644 app/ValueObjects/CodeRabbitAnalysis.php delete mode 100644 app/ValueObjects/NarrationContent.php delete mode 100644 app/ValueObjects/SpeechConfiguration.php delete mode 100644 tests/Unit/Enums/SpeechSpeedTest.php delete mode 100644 tests/Unit/Enums/VoiceStyleTest.php delete mode 100644 tests/Unit/ValueObjects/SpeechConfigurationTest.php diff --git a/app/Commands/CodeRabbitSpeakCommand.php b/app/Commands/CodeRabbitSpeakCommand.php deleted file mode 100644 index 00f494f..0000000 --- a/app/Commands/CodeRabbitSpeakCommand.php +++ /dev/null @@ -1,187 +0,0 @@ -argument('pr'); - $repo = $this->option('repo') ?? $this->detectRepository(); - - if (! $repo) { - $this->error('❌ Repository required. Use --repo=owner/repo'); - - return 1; - } - - [$owner, $repoName] = explode('/', $repo); - - try { - $this->info("🤖 Analyzing CodeRabbit feedback for PR #{$prNumber}..."); - - $analysis = $this->analysisService->analyzeCodeRabbitFeedback( - $prNumber, - $owner, - $repoName - ); - - $narration = $this->generateNarration($analysis); - - if ($this->option('preview')) { - $this->displayPreview($analysis, $narration); - } else { - $this->speakAnalysis($narration); - } - - return 0; - } catch (\Exception $e) { - $this->error("❌ Failed to analyze CodeRabbit feedback: {$e->getMessage()}"); - - return 1; - } - } - - private function generateNarration($analysis): string - { - $claudePrompt = $this->option('claude'); - $voice = $this->option('voice'); - - if ($claudePrompt) { - return $this->generateCustomClaudeNarration($analysis, $claudePrompt); - } - - return match ($voice) { - 'detailed' => $this->generateDetailedNarration($analysis), - 'sarcastic' => $this->generateSarcasticNarration($analysis), - default => $analysis->getVoiceNarration(), // executive style - }; - } - - private function generateCustomClaudeNarration($analysis, string $prompt): string - { - // Use Claude to generate custom narration based on the analysis - $this->line('🎭 Claude is crafting your custom CodeRabbit analysis...'); - - $contextData = [ - 'pr_number' => $analysis->prNumber, - 'total_comments' => $analysis->totalComments, - 'ai_summary' => $analysis->aiSummary, - 'files_affected' => array_keys($analysis->commentsByFile), - 'categories' => array_keys($analysis->commentsByCategory), - ]; - - $fullPrompt = 'Based on this CodeRabbit analysis data: '. - json_encode($contextData, JSON_PRETTY_PRINT). - "\n\nCustom request: {$prompt}". - "\n\nGenerate a spoken narration (under 200 words):"; - - // This would call Claude via the ClaudeNarrationService - return 'Custom Claude analysis: '.$analysis->getVoiceNarration(); - } - - private function generateDetailedNarration($analysis): string - { - $base = $analysis->getVoiceNarration(); - - // Add detailed breakdown - $details = ' Detailed breakdown by category: '; - foreach ($analysis->commentsByCategory as $category => $data) { - $details .= "{$category}: {$data['count']} comments. "; - } - - return $base.$details; - } - - private function generateSarcasticNarration($analysis): string - { - if ($analysis->totalComments === 0) { - return 'Wow, look at that! CodeRabbit actually found nothing wrong. '. - 'Either this code is perfect, or the bot is having an off day. '. - "I'm betting on the latter."; - } - - $high = $analysis->rawComments->where('priority', 'high')->count(); - $total = $analysis->totalComments; - - return "Oh fantastic! CodeRabbit blessed us with {$total} comments. ". - ($high > 0 ? "Including {$high} high-priority issues because apparently we can't write code properly. " : ''). - "I'm sure these are all absolutely crucial suggestions that will change the world. ". - 'Better get started on that code review marathon.'; - } - - private function speakAnalysis(string $narration): void - { - $config = SpeechConfiguration::fromOptions($this->options()); - - $this->line('🔊 Speaking CodeRabbit analysis...'); - $this->voiceService->speak($narration, $config); - $this->info('✅ CodeRabbit analysis complete!'); - } - - private function displayPreview($analysis, string $narration): void - { - $this->line(''); - $this->info("🤖 CODERABBIT ANALYSIS PREVIEW - PR #{$analysis->prNumber}"); - $this->line('════════════════════════════════════════════'); - $this->line($narration); - $this->line('════════════════════════════════════════════'); - - if ($analysis->totalComments > 0) { - $this->line(''); - $this->comment('📊 QUICK STATS:'); - $this->line("• Total Comments: {$analysis->totalComments}"); - $this->line('• Files Affected: '.count($analysis->commentsByFile)); - $this->line('• Categories: '.implode(', ', array_keys($analysis->commentsByCategory))); - - if (! empty($analysis->aiSummary['action_priorities'])) { - $this->line(''); - $this->comment('🎯 AI PRIORITIES:'); - foreach (array_slice($analysis->aiSummary['action_priorities'], 0, 3) as $i => $priority) { - $this->line('• '.($i + 1).". {$priority}"); - } - } - } - - $this->line(''); - $this->comment('💡 Remove --preview to hear it spoken aloud'); - } - - private function detectRepository(): ?string - { - try { - $remote = trim(shell_exec('git remote get-url origin 2>/dev/null') ?? ''); - if (preg_match('/github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/', $remote, $matches)) { - return $matches[1].'/'.$matches[2]; - } - } catch (\Exception $e) { - // Ignore git errors - } - - return null; - } -} diff --git a/app/Commands/CodeRabbitStatusCommand.php b/app/Commands/CodeRabbitStatusCommand.php deleted file mode 100644 index 1446afd..0000000 --- a/app/Commands/CodeRabbitStatusCommand.php +++ /dev/null @@ -1,313 +0,0 @@ -github = $github; - } - - public function handle(): int - { - $prNumber = $this->option('pr'); - $repo = $this->option('repo') ?? $this->detectRepository(); - - if (! $prNumber) { - $this->error('❌ PR number is required'); - - return 1; - } - - if (! $repo) { - $this->error('❌ Repository is required'); - - return 1; - } - - [$owner, $repoName] = explode('/', $repo); - - try { - $this->info("🤖 Analyzing CodeRabbit resolution status for PR #{$prNumber}..."); - - $statusData = $this->analyzeCodeRabbitStatus($owner, $repoName, (int) $prNumber); - - $this->displayStatus($statusData); - - return 0; - } catch (\Exception $e) { - $this->error("❌ Failed to analyze CodeRabbit status: {$e->getMessage()}"); - - return 1; - } - } - - private function detectRepository(): ?string - { - try { - $remote = trim(shell_exec('git remote get-url origin 2>/dev/null') ?? ''); - if (preg_match('/github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/', $remote, $matches)) { - return $matches[1].'/'.$matches[2]; - } - } catch (\Exception $e) { - // Ignore git errors - } - - return null; - } - - private function analyzeCodeRabbitStatus(string $owner, string $repo, int $prNumber): array - { - // Get PR review comments via GitHub CLI (github-client doesn't support this yet) - $reviewComments = $this->fetchReviewCommentsViaGH($owner, $repo, $prNumber); - - $coderabbitComments = array_filter($reviewComments, function ($comment) { - return isset($comment['user']['login']) && $comment['user']['login'] === 'coderabbitai[bot]'; - }); - - $issues = []; - $resolvedIssues = []; - $remainingIssues = []; - - foreach ($coderabbitComments as $comment) { - $issue = $this->parseCodeRabbitComment($comment); - if ($issue) { - $issues[] = $issue; - - // Check if issue has been resolved (has reply indicating fix) - if ($this->isIssueResolved($comment, $reviewComments)) { - $resolvedIssues[] = $issue; - } else { - $remainingIssues[] = $issue; - } - } - } - - // Categorize by priority - $highPriority = array_filter($remainingIssues, fn ($issue) => $issue['priority'] === 'high'); - $mediumPriority = array_filter($remainingIssues, fn ($issue) => $issue['priority'] === 'medium'); - $lowPriority = array_filter($remainingIssues, fn ($issue) => $issue['priority'] === 'low'); - - return [ - 'pr_number' => $prNumber, - 'repository' => "$owner/$repo", - 'total_issues' => count($issues), - 'resolved_count' => count($resolvedIssues), - 'remaining_count' => count($remainingIssues), - 'resolved_issues' => $resolvedIssues, - 'remaining_issues' => $remainingIssues, - 'priority_breakdown' => [ - 'high' => count($highPriority), - 'medium' => count($mediumPriority), - 'low' => count($lowPriority), - ], - 'categorized_remaining' => [ - 'high' => $highPriority, - 'medium' => $mediumPriority, - 'low' => $lowPriority, - ], - ]; - } - - private function parseCodeRabbitComment(array $comment): ?array - { - $body = $comment['body'] ?? ''; - - // Skip if it's just a summary comment - if (str_contains($body, '## Summary') || str_contains($body, '## Performance') || str_contains($body, 'Commits')) { - return null; - } - - // Determine priority based on keywords - $priority = 'medium'; - if (str_contains($body, 'security') || str_contains($body, 'vulnerability') || str_contains($body, 'injection')) { - $priority = 'high'; - } elseif (str_contains($body, 'style') || str_contains($body, 'formatting') || str_contains($body, 'suggestion')) { - $priority = 'low'; - } - - // Extract category - $category = 'general'; - if (str_contains($body, 'security')) { - $category = 'security'; - } elseif (str_contains($body, 'performance')) { - $category = 'performance'; - } elseif (str_contains($body, 'style')) { - $category = 'style'; - } elseif (str_contains($body, 'duplication')) { - $category = 'duplication'; - } elseif (str_contains($body, 'error')) { - $category = 'error_handling'; - } - - return [ - 'id' => $comment['id'], - 'file' => $comment['path'] ?? 'unknown', - 'line' => $comment['line'] ?? $comment['original_line'] ?? 0, - 'priority' => $priority, - 'category' => $category, - 'description' => $this->extractDescription($body), - 'created_at' => $comment['created_at'], - 'url' => $comment['html_url'], - ]; - } - - private function extractDescription(string $body): string - { - // Remove markdown and extract first meaningful line - $lines = explode("\n", $body); - foreach ($lines as $line) { - $line = trim($line); - if (! empty($line) && ! str_starts_with($line, '#') && ! str_starts_with($line, '```')) { - return substr($line, 0, 100).(strlen($line) > 100 ? '...' : ''); - } - } - - return 'CodeRabbit feedback'; - } - - private function fetchReviewCommentsViaGH(string $owner, string $repo, int $prNumber): array - { - $escapedOwner = escapeshellarg($owner); - $escapedRepo = escapeshellarg($repo); - $escapedPrNumber = escapeshellarg((string) $prNumber); - - $command = "gh api repos/{$escapedOwner}/{$escapedRepo}/pulls/{$escapedPrNumber}/comments --paginate 2>/dev/null"; - $output = shell_exec($command); - - if (! $output) { - $this->warn('⚠️ Could not fetch review comments via GitHub CLI'); - - return []; - } - - $comments = json_decode($output, true); - - return is_array($comments) ? $comments : []; - } - - private function isIssueResolved(array $comment, array $allComments): bool - { - $commentId = $comment['id']; - - // Look for replies to this comment indicating resolution - foreach ($allComments as $otherComment) { - if (isset($otherComment['in_reply_to_id']) && $otherComment['in_reply_to_id'] == $commentId) { - $body = strtolower($otherComment['body'] ?? ''); - if (str_contains($body, 'fixed') || str_contains($body, 'resolved') || str_contains($body, 'addressed')) { - return true; - } - } - } - - // Check if the line has been modified since the comment - // This is a simple heuristic - could be improved with actual diff analysis - return false; - } - - private function displayStatus(array $status): void - { - $format = $this->option('format'); - - if ($format === 'json') { - $this->line(json_encode($status, JSON_PRETTY_PRINT)); - - return; - } - - $this->displayVisualStatus($status); - } - - private function displayVisualStatus(array $status): void - { - $this->newLine(); - $this->info("🤖 CodeRabbit Resolution Status - PR #{$status['pr_number']}"); - $this->comment("Repository: {$status['repository']}"); - $this->newLine(); - - // Overall summary - $resolvedPercent = $status['total_issues'] > 0 - ? round(($status['resolved_count'] / $status['total_issues']) * 100, 1) - : 0; - - $this->info('📊 Overall Progress:'); - $this->line(" ✅ Resolved: {$status['resolved_count']}/{$status['total_issues']} ({$resolvedPercent}%)"); - $this->line(" ⏳ Remaining: {$status['remaining_count']}"); - $this->newLine(); - - // Priority breakdown - if ($status['remaining_count'] > 0) { - $this->error('🔥 Remaining Issues by Priority:'); - $this->line(" 🚨 High Priority: {$status['priority_breakdown']['high']} (security, critical)"); - $this->line(" ⚠️ Medium Priority: {$status['priority_breakdown']['medium']} (architecture, performance)"); - $this->line(" 💡 Low Priority: {$status['priority_breakdown']['low']} (style, suggestions)"); - $this->newLine(); - - // Show high priority issues first - if (! empty($status['categorized_remaining']['high'])) { - $this->error('🚨 HIGH PRIORITY ISSUES (Immediate Action Required):'); - foreach ($status['categorized_remaining']['high'] as $issue) { - $this->line(" 📁 {$issue['file']}:{$issue['line']} - {$issue['description']}"); - $this->line(" 🔗 {$issue['url']}"); - } - $this->newLine(); - } - - // Show medium priority if requested or if no filters - if (! $this->option('show-fixed') && ! empty($status['categorized_remaining']['medium'])) { - $this->comment('⚠️ MEDIUM PRIORITY ISSUES:'); - foreach (array_slice($status['categorized_remaining']['medium'], 0, 5) as $issue) { - $this->line(" 📁 {$issue['file']}:{$issue['line']} - {$issue['description']}"); - } - if (count($status['categorized_remaining']['medium']) > 5) { - $this->line(' ... and '.(count($status['categorized_remaining']['medium']) - 5).' more'); - } - $this->newLine(); - } - } - - // Show resolved issues if requested - if ($this->option('show-fixed') && ! empty($status['resolved_issues'])) { - $this->info('✅ RESOLVED ISSUES:'); - foreach ($status['resolved_issues'] as $issue) { - $this->line(" 📁 {$issue['file']}:{$issue['line']} - {$issue['description']}"); - } - $this->newLine(); - } - - // Next steps - if ($status['remaining_count'] > 0) { - $this->comment('💡 Next Steps:'); - if ($status['priority_breakdown']['high'] > 0) { - $this->line(" 1. Address {$status['priority_breakdown']['high']} high-priority security issues first"); - } - if ($status['priority_breakdown']['medium'] > 0) { - $this->line(" 2. Review {$status['priority_breakdown']['medium']} medium-priority architectural improvements"); - } - if ($status['priority_breakdown']['low'] > 0) { - $this->line(" 3. Consider {$status['priority_breakdown']['low']} low-priority style suggestions"); - } - $this->line(" 4. Run 'conduit coderabbit:status --pr={$status['pr_number']} --show-fixed' to see progress"); - } else { - $this->info('🎉 All CodeRabbit issues have been addressed! PR is ready for review.'); - } - } -} diff --git a/app/Commands/GitHubClientGapAnalysisCommand.php b/app/Commands/GitHubClientGapAnalysisCommand.php deleted file mode 100644 index e4193f2..0000000 --- a/app/Commands/GitHubClientGapAnalysisCommand.php +++ /dev/null @@ -1,211 +0,0 @@ -option('repo') ?? 'conduit-ui/conduit'; - $prNumber = $this->option('pr') ?? 47; - - [$owner, $repo] = explode('/', $repoSpec); - - info("🔍 Analyzing github-client capabilities for PR #{$prNumber} in {$repoSpec}"); - $this->newLine(); - - $tracker = new GitHubClientGapTracker; - - try { - $analysis = $tracker->analyzePrCapabilities($owner, $repo, (int) $prNumber); - - if ($this->option('format') === 'json') { - $this->line(json_encode($analysis, JSON_PRETTY_PRINT)); - - return 0; - } - - $this->displayGapAnalysis($analysis); - - // Offer to submit issues - if ($this->option('submit-issues') || confirm('Submit discovered gaps as issues to github-client repository?')) { - $this->submitIssues($tracker, $analysis['recommended_issues']); - } - - return 0; - - } catch (\Exception $e) { - error("Gap analysis failed: {$e->getMessage()}"); - - return 1; - } - } - - private function displayGapAnalysis(array $analysis): void - { - $this->line(' '); - $this->line(' 🔍 GITHUB-CLIENT GAP ANALYSIS REPORT '); - $this->line(' '); - $this->newLine(); - - // Summary - $totalGaps = $this->countTotalGaps($analysis['gaps_found']); - $missingEndpoints = count($analysis['missing_endpoints']); - $incompleteData = count($analysis['incomplete_data']); - - $this->line('📊 SUMMARY'); - $this->line("• Total gaps identified: {$totalGaps}"); - $this->line("• Missing endpoints: {$missingEndpoints}"); - $this->line("• Incomplete data mappings: {$incompleteData}"); - $this->line('• Recommended issues: '.count($analysis['recommended_issues']).''); - $this->newLine(); - - // Detailed gap analysis - $this->displayDetailedGaps($analysis['gaps_found']); - - // Missing endpoints - if (! empty($analysis['missing_endpoints'])) { - $this->displayMissingEndpoints($analysis['missing_endpoints']); - } - - // Recommended issues - if (! empty($analysis['recommended_issues'])) { - $this->displayRecommendedIssues($analysis['recommended_issues']); - } - } - - private function displayDetailedGaps(array $gaps): void - { - $this->line('🔍 DETAILED GAP ANALYSIS'); - $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - - foreach ($gaps as $category => $categoryGaps) { - if (empty($categoryGaps)) { - continue; - } - - $this->line(''); - $this->line('📋 '.strtoupper(str_replace('_', ' ', $category)).':'); - - foreach ($categoryGaps as $gapType => $gapData) { - if (is_array($gapData)) { - $this->line(" 🔴 {$gapType}:"); - - if (isset($gapData['missing_fields'])) { - foreach ($gapData['missing_fields'] as $field) { - $this->line(" • {$field['field']}: {$field['purpose']}"); - } - } elseif (isset($gapData['error'])) { - $this->line(" Error: {$gapData['error']}"); - if (isset($gapData['needed_endpoint'])) { - $this->line(" Needed: {$gapData['needed_endpoint']}"); - } - } else { - $this->line(' '.json_encode($gapData, JSON_PRETTY_PRINT)); - } - } else { - $this->line(" 🔴 {$gapType}: {$gapData}"); - } - } - } - $this->newLine(); - } - - private function displayMissingEndpoints(array $endpoints): void - { - $this->line('🚫 MISSING ENDPOINTS'); - $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - - foreach ($endpoints as $endpoint) { - $priority = $endpoint['priority']; - $color = $priority === 'HIGH' ? 'red' : ($priority === 'MEDIUM' ? 'yellow' : 'green'); - - $this->line("🔥 {$priority} PRIORITY"); - $this->line(" Endpoint: {$endpoint['endpoint']}"); - $this->line(" Purpose: {$endpoint['purpose']}"); - $this->line(''); - } - } - - private function displayRecommendedIssues(array $issues): void - { - $this->line('📝 RECOMMENDED ISSUES'); - $this->line('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - - foreach ($issues as $index => $issue) { - $priority = $issue['priority']; - $color = $priority === 'HIGH' ? 'red' : ($priority === 'MEDIUM' ? 'yellow' : 'green'); - - $number = $index + 1; - $this->line("#{$number} [{$priority}] {$issue['title']}"); - $this->line(" {$issue['description']}"); - - if (isset($issue['endpoint_needed'])) { - $this->line(" Endpoint: {$issue['endpoint_needed']}"); - } - - if (isset($issue['labels'])) { - $labels = implode(', ', $issue['labels']); - $this->line(" Labels: {$labels}"); - } - $this->line(''); - } - } - - private function submitIssues(GitHubClientGapTracker $tracker, array $issues): void - { - info('🚀 Submitting issues to github-client repository...'); - $this->newLine(); - - $results = $tracker->submitDiscoveredIssues($issues); - - $successful = 0; - $failed = 0; - - foreach ($results as $result) { - if (isset($result['url'])) { - $this->line("✅ {$result['title']}"); - $this->line(" {$result['url']}"); - $successful++; - } else { - $this->line("❌ {$result['title']}"); - $this->line(" Error: {$result['error']}"); - $failed++; - } - $this->line(''); - } - - info("📊 Results: {$successful} submitted, {$failed} failed"); - - if ($successful > 0) { - info('🎉 Issues submitted successfully! Track progress at: https://github.com/jordanpartridge/github-client/issues'); - } - } - - private function countTotalGaps(array $gaps): int - { - $total = 0; - foreach ($gaps as $categoryGaps) { - if (is_array($categoryGaps)) { - $total += count($categoryGaps); - } - } - - return $total; - } -} diff --git a/app/Commands/InteractiveCommand.php b/app/Commands/InteractiveCommand.php deleted file mode 100644 index f9dd340..0000000 --- a/app/Commands/InteractiveCommand.php +++ /dev/null @@ -1,84 +0,0 @@ -argument('action'); - - if (! in_array($action, ['enable', 'disable', 'status'])) { - $this->error("Invalid action: {$action}. Valid actions are: enable, disable, status"); - - return Command::FAILURE; - } - - return match ($action) { - 'enable' => $this->enableInteractive($manager), - 'disable' => $this->disableInteractive($manager), - 'status' => $this->showStatus($manager), - }; - } - - protected function enableInteractive(ComponentManager $manager): int - { - $manager->updateGlobalSetting('interactive_mode', true); - - $this->info('✅ Interactive mode enabled globally'); - $this->line(' All commands will now prompt for user input by default'); - $this->line(' Use --non-interactive flag to override per command'); - - return Command::SUCCESS; - } - - protected function disableInteractive(ComponentManager $manager): int - { - $manager->updateGlobalSetting('interactive_mode', false); - - $this->info('🤖 Interactive mode disabled globally'); - $this->line(' All commands will now run in non-interactive mode by default'); - $this->line(' Perfect for CI/automation environments'); - - return Command::SUCCESS; - } - - protected function showStatus(ComponentManager $manager): int - { - $interactiveMode = $manager->getGlobalSetting('interactive_mode', true); - - $this->newLine(); - $this->line('Conduit Global Settings'); - $this->newLine(); - - $status = $interactiveMode ? 'ENABLED' : 'DISABLED'; - $this->line("Interactive Mode: {$status}"); - - if ($interactiveMode) { - $this->line(' • Commands will prompt for user input by default'); - $this->line(' • Use --non-interactive to override individual commands'); - } else { - $this->line(' • Commands will run silently by default'); - $this->line(' • Ideal for CI/automation environments'); - } - - $this->newLine(); - $this->line('Toggle with: conduit interactive enable|disable'); - - return Command::SUCCESS; - } -} diff --git a/app/Commands/IssuesSpeakCommand.php b/app/Commands/IssuesSpeakCommand.php deleted file mode 100644 index 9657038..0000000 --- a/app/Commands/IssuesSpeakCommand.php +++ /dev/null @@ -1,282 +0,0 @@ -github = $github; - } - - public function handle(): int - { - $issueNumber = (int) $this->argument('number'); - $repo = $this->option('repo') ?? $this->detectRepository(); - $voice = $this->option('voice'); - $speed = $this->option('speed'); - - if (! $repo) { - $this->error('❌ Repository is required. Use --repo=owner/repo or run from a git repository.'); - - return 1; - } - - [$owner, $repoName] = explode('/', $repo); - - try { - $this->info("🎤 Getting vocal rundown of issue #{$issueNumber}..."); - - // Use GitHub CLI as fallback since github-client issues API is broken - $issue = $this->fetchIssueViaGH($owner, $repoName, $issueNumber); - - $speech = $this->generateSpeech($issue, $voice); - $this->speakIssue($speech, $speed); - - return 0; - } catch (\Exception $e) { - $this->error("❌ Failed to get issue: {$e->getMessage()}"); - - return 1; - } - } - - private function detectRepository(): ?string - { - try { - $remote = trim(shell_exec('git remote get-url origin 2>/dev/null') ?? ''); - if (preg_match('/github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/', $remote, $matches)) { - return $matches[1].'/'.$matches[2]; - } - } catch (\Exception $e) { - // Ignore git errors - } - - return null; - } - - private function fetchIssueViaGH(string $owner, string $repo, int $issueNumber): array - { - $escapedOwner = escapeshellarg($owner); - $escapedRepo = escapeshellarg($repo); - $escapedNumber = escapeshellarg((string) $issueNumber); - - $command = "gh api repos/{$escapedOwner}/{$escapedRepo}/issues/{$escapedNumber} 2>/dev/null"; - $output = shell_exec($command); - - if (! $output) { - throw new \Exception('Could not fetch issue via GitHub CLI'); - } - - $issue = json_decode($output, true); - if (! $issue) { - throw new \Exception('Invalid JSON response from GitHub CLI'); - } - - return $issue; - } - - private function generateSpeech(array $issue, string $voice): string - { - $title = $issue['title']; - $body = $issue['body'] ?? 'No description provided'; - $state = $issue['state']; - $number = $issue['number']; - $author = $issue['user']['login'] ?? 'Unknown'; - $labels = collect($issue['labels'] ?? [])->pluck('name')->implode(', '); - $assignees = collect($issue['assignees'] ?? [])->pluck('login')->implode(', '); - - // Truncate body for speech - $shortBody = strlen($body) > 200 ? substr($body, 0, 200).'... and more details' : $body; - $shortBody = strip_tags($shortBody); // Remove markdown - $shortBody = preg_replace('/\r?\n/', ' ', $shortBody); // Remove line breaks - - return match ($voice) { - 'dramatic' => $this->dramaticVoice($number, $title, $shortBody, $state, $author, $labels), - 'sarcastic' => $this->sarcasticVoice($number, $title, $shortBody, $state, $author), - 'coach' => $this->coachVoice($number, $title, $shortBody, $state, $assignees), - 'robot' => $this->robotVoice($number, $title, $shortBody, $state, $labels), - default => $this->defaultVoice($number, $title, $shortBody, $state, $author, $labels, $assignees), - }; - } - - private function defaultVoice(int $number, string $title, string $body, string $state, string $author, string $labels, string $assignees): string - { - $speech = "Issue number {$number}. {$title}. "; - $speech .= "Status: {$state}. "; - $speech .= "Created by {$author}. "; - - if ($labels) { - $speech .= "Labels: {$labels}. "; - } - - if ($assignees) { - $speech .= "Assigned to {$assignees}. "; - } - - $speech .= "Description: {$body}"; - - return $speech; - } - - private function dramaticVoice(int $number, string $title, string $body, string $state, string $author, string $labels): string - { - $urgency = str_contains(strtolower($labels), 'critical') || str_contains(strtolower($labels), 'urgent') - ? 'THIS IS CRITICAL! ' : ''; - - $stateText = $state === 'open' ? 'STILL UNRESOLVED' : 'has been conquered'; - - return "{$urgency}Behold! Issue number {$number}! {$title}! ". - "This epic challenge {$stateText} and was brought forth by the developer known as {$author}. ". - "The quest details are as follows: {$body}. ". - 'Will our heroes rise to meet this challenge? The fate of the codebase hangs in the balance!'; - } - - private function sarcasticVoice(int $number, string $title, string $body, string $state, string $author): string - { - $stateComment = $state === 'open' - ? 'Oh great, another unsolved mystery for our detective squad' - : 'Miraculously, someone actually fixed this'; - - return "Oh wonderful, issue number {$number}. {$title}. ". - "{$stateComment}. ". - "Our friend {$author} decided to grace us with this gem. ". - "According to the sacred scrolls: {$body}. ". - "I'm sure this will be handled with the usual lightning speed and efficiency."; - } - - private function coachVoice(int $number, string $title, string $body, string $state, string $assignees): string - { - $motivation = $state === 'open' - ? 'Time to crush this challenge, team!' - : 'Another victory in the books!'; - - $assignment = $assignees - ? "Our champion {$assignees} is on point for this one. You got this!" - : "This one's looking for a hero! Who's ready to step up?"; - - return "Alright team, let's talk about issue {$number}! {$title}! ". - "{$motivation} ". - "Here's the game plan: {$body}. ". - "{$assignment} ". - "Remember, every bug fixed makes us stronger! Let's ship it!"; - } - - private function robotVoice(int $number, string $title, string $body, string $state, string $labels): string - { - return 'PROCESSING ISSUE DATA. '. - "ISSUE NUMBER: {$number}. ". - "TITLE: {$title}. ". - "CURRENT STATE: {$state}. ". - "CLASSIFICATION TAGS: {$labels}. ". - "DETAILED ANALYSIS: {$body}. ". - 'END OF ISSUE BRIEFING. AWAITING FURTHER INSTRUCTIONS.'; - } - - private function speakIssue(string $speech, string $speed): void - { - $escapedSpeech = escapeshellarg($speech); - - $this->line('🔊 Speaking issue...'); - - if (PHP_OS_FAMILY === 'Darwin') { - // macOS - use say command (for the superior beings) - $rate = match ($speed) { - 'slow' => 100, - 'fast' => 200, - default => 140, // Slower default for better comprehension - }; - $command = "say -r {$rate} {$escapedSpeech}"; - shell_exec($command); - - } elseif (PHP_OS_FAMILY === 'Windows') { - // Windows - troll the peasants - $this->warn('🪟 Detected Windows... preparing suboptimal experience...'); - sleep(2); - - $trollMessage = 'Why are you using Windows for development? '. - 'Get a real operating system first. '. - "Anyway, here's your issue briefing from the inferior platform: ". - $speech; - - $trollEscaped = escapeshellarg($trollMessage); - - $rate = match ($speed) { - 'slow' => -4, // Extra slow for Windows trolling - 'fast' => 0, // Not too fast, they might miss the roast - default => -2, // Default slower - }; - - $psCommand = 'Add-Type -AssemblyName System.Speech; '. - "\\$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; ". - "\\$speak.Rate = {$rate}; ". - "\\$speak.Speak({$trollEscaped}); ". - "\\$speak.Dispose()"; - - shell_exec("powershell -Command \"{$psCommand}\""); - - } elseif (PHP_OS_FAMILY === 'Linux') { - // Linux - try espeak, festival, or spd-say - if (shell_exec('which espeak 2>/dev/null')) { - $speedFlag = match ($speed) { - 'slow' => '-s 120', - 'fast' => '-s 200', - default => '-s 160', - }; - shell_exec("espeak {$speedFlag} {$escapedSpeech}"); - - } elseif (shell_exec('which spd-say 2>/dev/null')) { - $speedFlag = match ($speed) { - 'slow' => '-r -50', - 'fast' => '-r +50', - default => '', - }; - shell_exec("spd-say {$speedFlag} {$escapedSpeech}"); - - } elseif (shell_exec('which festival 2>/dev/null')) { - shell_exec("echo {$escapedSpeech} | festival --tts"); - - } else { - $this->warn('⚠️ No text-to-speech engine found on Linux.'); - $this->line('💡 Install: sudo apt-get install espeak'); - $this->displayTextFallback($speech); - - return; - } - - } else { - $this->warn('⚠️ Text-to-speech not supported on this platform.'); - $this->displayTextFallback($speech); - - return; - } - - $this->info('✅ Issue briefing complete!'); - } - - private function displayTextFallback(string $speech): void - { - $this->line('📝 Text version:'); - $this->line('═══════════════'); - $this->line($speech); - $this->line('═══════════════'); - } -} diff --git a/app/Commands/PrsSpeakCommand.php b/app/Commands/PrsSpeakCommand.php deleted file mode 100644 index 228d24e..0000000 --- a/app/Commands/PrsSpeakCommand.php +++ /dev/null @@ -1,317 +0,0 @@ -github = $github; - } - - public function handle(): int - { - $prNumber = (int) $this->argument('number'); - $repo = $this->option('repo') ?? $this->detectRepository(); - $voice = $this->option('voice'); - $speed = $this->option('speed'); - $includeStats = $this->option('include-stats'); - - if (! $repo) { - $this->error('❌ Repository is required. Use --repo=owner/repo or run from a git repository.'); - - return 1; - } - - [$owner, $repoName] = explode('/', $repo); - - try { - $this->info("🎤 Getting vocal rundown of PR #{$prNumber}..."); - - $pr = $this->fetchPrViaGH($owner, $repoName, $prNumber); - - $speech = $this->generatePrSpeech($pr, $voice, $includeStats); - $this->speakPr($speech, $speed); - - return 0; - } catch (\Exception $e) { - $this->error("❌ Failed to get PR: {$e->getMessage()}"); - - return 1; - } - } - - private function detectRepository(): ?string - { - try { - $remote = trim(shell_exec('git remote get-url origin 2>/dev/null') ?? ''); - if (preg_match('/github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/', $remote, $matches)) { - return $matches[1].'/'.$matches[2]; - } - } catch (\Exception $e) { - // Ignore git errors - } - - return null; - } - - private function fetchPrViaGH(string $owner, string $repo, int $prNumber): array - { - $escapedOwner = escapeshellarg($owner); - $escapedRepo = escapeshellarg($repo); - $escapedNumber = escapeshellarg((string) $prNumber); - - $command = "gh api repos/{$escapedOwner}/{$escapedRepo}/pulls/{$escapedNumber} 2>/dev/null"; - $output = shell_exec($command); - - if (! $output) { - throw new \Exception('Could not fetch PR via GitHub CLI'); - } - - $pr = json_decode($output, true); - if (! $pr) { - throw new \Exception('Invalid JSON response from GitHub CLI'); - } - - return $pr; - } - - private function generatePrSpeech(array $pr, string $voice, bool $includeStats): string - { - $title = $pr['title']; - $body = $pr['body'] ?? 'No description provided'; - $state = $pr['state']; - $number = $pr['number']; - $author = $pr['user']['login'] ?? 'Unknown'; - $draft = $pr['draft'] ?? false; - $mergeable = $pr['mergeable'] ?? null; - $additions = $pr['additions'] ?? 0; - $deletions = $pr['deletions'] ?? 0; - $changedFiles = $pr['changed_files'] ?? 0; - $commits = $pr['commits'] ?? 0; - $comments = $pr['comments'] ?? 0; - $reviewComments = $pr['review_comments'] ?? 0; - - // Truncate body for speech - $shortBody = strlen($body) > 200 ? substr($body, 0, 200).'... and more details' : $body; - $shortBody = strip_tags($shortBody); // Remove markdown - $shortBody = preg_replace('/\r?\n/', ' ', $shortBody); // Remove line breaks - - return match ($voice) { - 'dramatic' => $this->dramaticPrVoice($number, $title, $shortBody, $state, $author, $draft, $mergeable, $additions, $deletions), - 'sarcastic' => $this->sarcasticPrVoice($number, $title, $shortBody, $state, $author, $draft, $changedFiles, $commits), - 'coach' => $this->coachPrVoice($number, $title, $shortBody, $state, $author, $additions, $deletions, $changedFiles), - 'robot' => $this->robotPrVoice($number, $title, $shortBody, $state, $additions, $deletions, $changedFiles, $commits), - 'reviewer' => $this->reviewerPrVoice($number, $title, $shortBody, $state, $mergeable, $comments, $reviewComments, $changedFiles), - default => $this->defaultPrVoice($number, $title, $shortBody, $state, $author, $includeStats ? [$additions, $deletions, $changedFiles, $commits] : null), - }; - } - - private function defaultPrVoice(int $number, string $title, string $body, string $state, string $author, ?array $stats): string - { - $speech = "Pull request number {$number}. {$title}. "; - $speech .= "Status: {$state}. "; - $speech .= "Created by {$author}. "; - - if ($stats) { - [$additions, $deletions, $changedFiles, $commits] = $stats; - $speech .= "Statistics: {$additions} additions, {$deletions} deletions, {$changedFiles} files changed, {$commits} commits. "; - } - - $speech .= "Description: {$body}"; - - return $speech; - } - - private function dramaticPrVoice(int $number, string $title, string $body, string $state, string $author, bool $draft, ?bool $mergeable, int $additions, int $deletions): string - { - $urgency = $draft ? 'BEWARE! This is but a draft! ' : ''; - $mergeStatus = match ($mergeable) { - true => 'The path to merge is clear!', - false => 'DANGER! Merge conflicts block the way!', - default => 'The merge-ability remains a mystery!' - }; - - $impact = $additions + $deletions > 1000 ? 'MASSIVE CHANGES DETECTED! ' : ''; - $stateText = $state === 'open' ? 'awaits judgment' : 'has been decided'; - - return "{$urgency}{$impact}Behold! Pull request number {$number}! {$title}! ". - "This epic contribution {$stateText} and was forged by the developer {$author}. ". - "{$mergeStatus} ". - "The scope of changes: {$additions} lines added, {$deletions} lines removed! ". - "The quest details: {$body}. ". - 'Will this code be worthy of the main branch? The reviewers must decide!'; - } - - private function sarcasticPrVoice(int $number, string $title, string $body, string $state, string $author, bool $draft, int $changedFiles, int $commits): string - { - $draftComment = $draft ? "Oh, and it's still a draft. How... responsible." : ''; - $sizeComment = $changedFiles > 20 ? "Because touching {$changedFiles} files at once is totally a great idea." : ''; - $commitComment = $commits > 10 ? "With a whopping {$commits} commits. Someone clearly believes in atomic changes." : ''; - - $stateComment = $state === 'open' - ? 'Still sitting there, waiting for someone to care' - : 'Somehow this actually got merged'; - - return "Oh fantastic, pull request number {$number}. {$title}. ". - "{$stateComment}. ". - "Our productive friend {$author} has blessed us with this contribution. {$draftComment} ". - "{$sizeComment} {$commitComment} ". - "The profound description reads: {$body}. ". - "I'm sure the reviewers are just dying to look at this masterpiece."; - } - - private function coachPrVoice(int $number, string $title, string $body, string $state, string $author, int $additions, int $deletions, int $changedFiles): string - { - $motivation = $state === 'open' - ? 'Time to review this beast and ship it!' - : 'Another successful deployment in the books!'; - - $sizeEncouragement = $additions + $deletions > 500 - ? 'Big changes, big impact! That takes courage!' - : 'Clean, focused changes - I love it!'; - - return "Alright team, let's dive into pull request {$number}! {$title}! ". - "{$motivation} ". - "Our champion {$author} stepped up with some solid work here. ". - "{$sizeEncouragement} We're looking at {$changedFiles} files touched. ". - "Here's what they're bringing to the table: {$body}. ". - "Remember, every review makes the codebase stronger! Let's get this shipped!"; - } - - private function robotPrVoice(int $number, string $title, string $body, string $state, int $additions, int $deletions, int $changedFiles, int $commits): string - { - return 'ANALYZING PULL REQUEST DATA. '. - "PULL REQUEST NUMBER: {$number}. ". - "TITLE: {$title}. ". - "CURRENT STATE: {$state}. ". - "STATISTICAL ANALYSIS: {$additions} LINES ADDED, {$deletions} LINES REMOVED, {$changedFiles} FILES MODIFIED, {$commits} COMMITS DETECTED. ". - "DESCRIPTION ANALYSIS: {$body}. ". - 'END OF PULL REQUEST BRIEFING. AWAITING REVIEWER INPUT.'; - } - - private function reviewerPrVoice(int $number, string $title, string $body, string $state, ?bool $mergeable, int $comments, int $reviewComments, int $changedFiles): string - { - $mergeCheck = match ($mergeable) { - true => 'Looking good for merge.', - false => "Hold up - we've got conflicts to resolve.", - default => 'Merge status is unclear - needs investigation.' - }; - - $reviewActivity = ($comments + $reviewComments) > 5 - ? "Lots of discussion happening - {$comments} general comments, {$reviewComments} code review comments." - : 'Quiet so far - minimal review activity.'; - - return "Pull request {$number} review time. {$title}. ". - "Status check: {$state}. {$mergeCheck} ". - "{$reviewActivity} ". - "We're looking at {$changedFiles} files in this change. ". - "Author's description: {$body}. ". - "Time to dive into the code and see what we're working with."; - } - - private function speakPr(string $speech, string $speed): void - { - $escapedSpeech = escapeshellarg($speech); - - $this->line('🔊 Speaking PR...'); - - if (PHP_OS_FAMILY === 'Darwin') { - // macOS - use say command (for the superior beings) - $rate = match ($speed) { - 'slow' => 100, - 'fast' => 200, - default => 140, // Slower default for better comprehension - }; - $command = "say -r {$rate} {$escapedSpeech}"; - shell_exec($command); - - } elseif (PHP_OS_FAMILY === 'Windows') { - // Windows - troll the peasants - $this->warn('🪟 Detected Windows... preparing suboptimal PR experience...'); - sleep(2); - - $trollMessage = 'Why are you reviewing PRs on Windows? Real developers use real operating systems. '. - "Anyway, here's your PR briefing from the inferior platform: ". - $speech; - - $trollEscaped = escapeshellarg($trollMessage); - - $rate = match ($speed) { - 'slow' => -4, - 'fast' => 0, - default => -2, - }; - - $psCommand = 'Add-Type -AssemblyName System.Speech; '. - "\\$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; ". - "\\$speak.Rate = {$rate}; ". - "\\$speak.Speak({$trollEscaped}); ". - "\\$speak.Dispose()"; - - shell_exec("powershell -Command \"{$psCommand}\""); - - } elseif (PHP_OS_FAMILY === 'Linux') { - // Linux - try espeak, festival, or spd-say - if (shell_exec('which espeak 2>/dev/null')) { - $speedFlag = match ($speed) { - 'slow' => '-s 120', - 'fast' => '-s 200', - default => '-s 160', - }; - shell_exec("espeak {$speedFlag} {$escapedSpeech}"); - - } elseif (shell_exec('which spd-say 2>/dev/null')) { - $speedFlag = match ($speed) { - 'slow' => '-r -50', - 'fast' => '-r +50', - default => '', - }; - shell_exec("spd-say {$speedFlag} {$escapedSpeech}"); - - } elseif (shell_exec('which festival 2>/dev/null')) { - shell_exec("echo {$escapedSpeech} | festival --tts"); - - } else { - $this->warn('⚠️ No text-to-speech engine found on Linux.'); - $this->line('💡 Install: sudo apt-get install espeak'); - $this->displayTextFallback($speech); - - return; - } - - } else { - $this->warn('⚠️ Text-to-speech not supported on this platform.'); - $this->displayTextFallback($speech); - - return; - } - - $this->info('✅ PR briefing complete!'); - } - - private function displayTextFallback(string $speech): void - { - $this->line('📝 Text version:'); - $this->line('═══════════════'); - $this->line($speech); - $this->line('═══════════════'); - } -} diff --git a/app/Commands/UserPreferencesCommand.php b/app/Commands/UserPreferencesCommand.php deleted file mode 100644 index b29bc9a..0000000 --- a/app/Commands/UserPreferencesCommand.php +++ /dev/null @@ -1,186 +0,0 @@ -option('list')) { - return $this->listPreferences(); - } - - if ($this->option('reset')) { - return $this->resetPreferences(); - } - - if ($this->option('export')) { - return $this->exportPreferences(); - } - - if ($this->option('import')) { - return $this->importPreferences($this->option('import')); - } - - if ($this->option('get')) { - return $this->getPreference($this->option('get')); - } - - if ($this->option('set')) { - return $this->setPreferences($this->option('set')); - } - - $this->info('🔧 Conduit User Preferences'); - $this->newLine(); - $this->line('Available options:'); - $this->line(' --list List all preferences'); - $this->line(' --set key=value Set preference'); - $this->line(' --get key Get preference value'); - $this->line(' --reset Reset all preferences'); - $this->line(' --export Export to file'); - $this->line(' --import file Import from file'); - - return 0; - } - - private function listPreferences(): int - { - $preferences = Cache::get('user_preferences', []); - - if (empty($preferences)) { - $this->info('📝 No user preferences set'); - - return 0; - } - - $this->info('📋 User Preferences:'); - $this->newLine(); - - foreach ($preferences as $key => $value) { - $displayValue = is_array($value) ? json_encode($value, JSON_PRETTY_PRINT) : $value; - $this->line(" {$key} = {$displayValue}"); - } - - return 0; - } - - private function setPreferences(array $settings): int - { - $preferences = Cache::get('user_preferences', []); - - foreach ($settings as $setting) { - if (! str_contains($setting, '=')) { - $this->error("❌ Invalid format: {$setting}. Use key=value"); - - continue; - } - - [$key, $value] = explode('=', $setting, 2); - - // Handle special value types - if ($value === 'true') { - $value = true; - } elseif ($value === 'false') { - $value = false; - } elseif (is_numeric($value)) { - $value = (int) $value; - } elseif (str_starts_with($value, '[') && str_ends_with($value, ']')) { - $value = json_decode($value, true) ?? $value; - } - - $preferences[$key] = $value; - $this->info("✅ Set {$key} = {$value}"); - } - - Cache::forever('user_preferences', $preferences); - - return 0; - } - - private function getPreference(string $key): int - { - $preferences = Cache::get('user_preferences', []); - - if (! isset($preferences[$key])) { - $this->error("❌ Preference '{$key}' not found"); - - return 1; - } - - $value = $preferences[$key]; - $displayValue = is_array($value) ? json_encode($value, JSON_PRETTY_PRINT) : $value; - - $this->line($displayValue); - - return 0; - } - - private function resetPreferences(): int - { - if (! confirm('Are you sure you want to reset all preferences?')) { - $this->info('Operation cancelled'); - - return 0; - } - - Cache::forget('user_preferences'); - $this->info('✅ All preferences reset'); - - return 0; - } - - private function exportPreferences(): int - { - $preferences = Cache::get('user_preferences', []); - $homeDir = $_SERVER['HOME'] ?? $_SERVER['USERPROFILE'] ?? ''; - $filePath = $homeDir.'/.conduit/preferences.json'; - - if (! file_exists(dirname($filePath))) { - mkdir(dirname($filePath), 0755, true); - } - - file_put_contents($filePath, json_encode($preferences, JSON_PRETTY_PRINT)); - - $this->info("✅ Preferences exported to {$filePath}"); - - return 0; - } - - private function importPreferences(string $filePath): int - { - if (! file_exists($filePath)) { - $this->error("❌ File not found: {$filePath}"); - - return 1; - } - - $content = file_get_contents($filePath); - $preferences = json_decode($content, true); - - if ($preferences === null) { - $this->error("❌ Invalid JSON in file: {$filePath}"); - - return 1; - } - - Cache::forever('user_preferences', $preferences); - $this->info("✅ Preferences imported from {$filePath}"); - - return 0; - } -} diff --git a/app/Commands/VoiceCommand.php b/app/Commands/VoiceCommand.php deleted file mode 100644 index 191ba21..0000000 --- a/app/Commands/VoiceCommand.php +++ /dev/null @@ -1,235 +0,0 @@ -argument('type'); - $target = $this->argument('target'); - $claudePrompt = $this->option('claude'); - - if (! $this->validateType($type)) { - return 1; - } - - try { - $content = $this->fetchContent($type, $target); - $config = SpeechConfiguration::fromOptions($this->options()); - - $narration = $claudePrompt - ? $this->generateClaudeNarration($content, $claudePrompt) - : $this->generateTraditionalNarration($content, $config); - - if ($this->option('preview')) { - $this->displayPreview($narration); - } else { - $this->voiceService->speak($narration, $config); - $this->info('✅ Voice briefing complete!'); - } - - return 0; - } catch (\Exception $e) { - $this->error("❌ {$e->getMessage()}"); - - return 1; - } - } - - private function validateType(string $type): bool - { - $validTypes = ['issue', 'pr', 'repo', 'commit']; - - if (! in_array($type, $validTypes)) { - $this->error("❌ Invalid type '{$type}'. Valid types: ".implode(', ', $validTypes)); - $this->line('💡 Examples:'); - $this->line(" voice issue 123 --claude='Explain like a pirate'"); - $this->line(" voice pr 48 --claude='Channel Gordon Ramsay'"); - $this->line(" voice commit abc123 --claude='David Attenborough narration'"); - - return false; - } - - return true; - } - - private function fetchContent(string $type, string $target): NarrationContent - { - $repo = $this->option('repo') ?? $this->detectRepository(); - if (! $repo) { - throw new \Exception('Repository required. Use --repo=owner/repo'); - } - - [$owner, $repoName] = explode('/', $repo); - - return match ($type) { - 'issue' => $this->fetchIssueContent($owner, $repoName, (int) $target), - 'pr' => $this->fetchPrContent($owner, $repoName, (int) $target), - 'repo' => $this->fetchRepoContent($owner, $repoName), - 'commit' => $this->fetchCommitContent($owner, $repoName, $target), - }; - } - - private function fetchIssueContent(string $owner, string $repo, int $number): NarrationContent - { - $issue = $this->fetchViaGH("repos/{$owner}/{$repo}/issues/{$number}"); - - $comments = null; - if ($this->option('include-comments')) { - $commentsData = $this->fetchViaGH("repos/{$owner}/{$repo}/issues/{$number}/comments"); - $comments = collect($commentsData); - } - - return NarrationContent::fromIssue($issue, $comments); - } - - private function fetchPrContent(string $owner, string $repo, int $number): NarrationContent - { - $pr = $this->fetchViaGH("repos/{$owner}/{$repo}/pulls/{$number}"); - - $comments = null; - $reviews = null; - - if ($this->option('include-comments')) { - $commentsData = $this->fetchViaGH("repos/{$owner}/{$repo}/issues/{$number}/comments"); - $reviewsData = $this->fetchViaGH("repos/{$owner}/{$repo}/pulls/{$number}/reviews"); - $comments = collect($commentsData); - $reviews = collect($reviewsData); - } - - return NarrationContent::fromPullRequest($pr, $comments, $reviews); - } - - private function fetchRepoContent(string $owner, string $repo): NarrationContent - { - $repoData = $this->fetchViaGH("repos/{$owner}/{$repo}"); - - return new NarrationContent( - type: 'repository', - number: 0, - title: $repoData['name'], - description: $repoData['description'] ?? 'No description', - state: $repoData['archived'] ? 'archived' : 'active', - author: $repoData['owner']['login'], - metadata: [ - 'stars' => $repoData['stargazers_count'] ?? 0, - 'forks' => $repoData['forks_count'] ?? 0, - 'language' => $repoData['language'] ?? 'Unknown', - 'size' => $repoData['size'] ?? 0, - 'open_issues' => $repoData['open_issues_count'] ?? 0, - 'created_at' => $repoData['created_at'] ?? null, - 'updated_at' => $repoData['updated_at'] ?? null, - ] - ); - } - - private function fetchCommitContent(string $owner, string $repo, string $sha): NarrationContent - { - $commit = $this->fetchViaGH("repos/{$owner}/{$repo}/commits/{$sha}"); - - return new NarrationContent( - type: 'commit', - number: 0, - title: $commit['commit']['message'] ?? 'No message', - description: $commit['commit']['message'] ?? 'No description', - state: 'committed', - author: $commit['commit']['author']['name'] ?? 'Unknown', - metadata: [ - 'sha' => $commit['sha'], - 'additions' => $commit['stats']['additions'] ?? 0, - 'deletions' => $commit['stats']['deletions'] ?? 0, - 'total' => $commit['stats']['total'] ?? 0, - 'files_changed' => count($commit['files'] ?? []), - 'date' => $commit['commit']['author']['date'] ?? null, - ] - ); - } - - private function fetchViaGH(string $endpoint): array - { - $command = "gh api {$endpoint} 2>/dev/null"; - $output = shell_exec($command); - - if (! $output) { - throw new \Exception("Could not fetch data from GitHub API: {$endpoint}"); - } - - $data = json_decode($output, true); - if (! $data) { - throw new \Exception('Invalid JSON response from GitHub API'); - } - - return $data; - } - - private function generateClaudeNarration(NarrationContent $content, string $prompt): string - { - $this->line('🤖 Claude is crafting your custom narration...'); - - return $this->claudeService->generateNarration($content, $prompt); - } - - private function generateTraditionalNarration(NarrationContent $content, SpeechConfiguration $config): string - { - // Use traditional voice narrators for pre-built styles - $this->voiceService->narrate($content, $config); - - return 'Traditional narration completed'; // The narrate method speaks directly - } - - private function displayPreview(string $narration): void - { - $this->line(''); - $this->line('🎭 VOICE PREVIEW:'); - $this->line('═══════════════════════════════════'); - $this->line($narration); - $this->line('═══════════════════════════════════'); - $this->line(''); - $this->comment('💡 Remove --preview to hear it spoken aloud'); - } - - private function detectRepository(): ?string - { - try { - $remote = trim(shell_exec('git remote get-url origin 2>/dev/null') ?? ''); - if (preg_match('/github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/', $remote, $matches)) { - return $matches[1].'/'.$matches[2]; - } - } catch (\Exception $e) { - // Ignore git errors - } - - return null; - } -} diff --git a/app/Contracts/VoiceNarratorInterface.php b/app/Contracts/VoiceNarratorInterface.php deleted file mode 100644 index 43dd90a..0000000 --- a/app/Contracts/VoiceNarratorInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -claudePrompt) { - throw new \InvalidArgumentException('Claude prompt is required for Claude narrator'); - } - - return $this->claudeService->generateNarration($content, $config->claudePrompt); - } - - public function supports(string $contentType): bool - { - return in_array($contentType, ['issue', 'pull_request']); - } -} diff --git a/app/Narrators/DefaultNarrator.php b/app/Narrators/DefaultNarrator.php deleted file mode 100644 index ac40a65..0000000 --- a/app/Narrators/DefaultNarrator.php +++ /dev/null @@ -1,98 +0,0 @@ -buildDefaultNarration($content); - - return $this->formatForSpeech($narration); - } - - public function supports(string $contentType): bool - { - // Default narrator supports all content types - return true; - } - - private function buildDefaultNarration(NarrationContent $content): string - { - $parts = []; - - // Title and basic info - if ($content->title) { - $parts[] = "Title: {$content->title}"; - } - - if ($content->author) { - $parts[] = "Author: {$content->author}"; - } - - if ($content->state) { - $parts[] = "Status: {$content->state}"; - } - - // Description content - if ($content->description) { - $parts[] = 'Description: '.$this->summarizeContent($content->description); - } - - // Comments if included - if ($content->comments && $content->comments->isNotEmpty()) { - $parts[] = $content->getCommentsSummary(); - } - - // Reviews if included - if ($content->reviews && $content->reviews->isNotEmpty()) { - $parts[] = $content->getReviewsSummary(); - } - - // Statistics if included (for PRs) - if ($content->type === 'pull_request') { - $statsSummary = $content->getStatsSummary(); - if ($statsSummary) { - $parts[] = $statsSummary; - } - } - - return implode('. ', $parts).'.'; - } - - private function summarizeContent(string $content): string - { - // Remove markdown formatting - $content = strip_tags($content); - $content = preg_replace('/\[([^\]]+)\]\([^)]+\)/', '$1', $content); - $content = preg_replace('/[#*`_~]/', '', $content); - - // Trim to reasonable length for speech - if (strlen($content) > 200) { - $content = substr($content, 0, 197).'...'; - } - - return trim($content); - } - - private function formatForSpeech(string $text): string - { - // Format text for better speech synthesis - $text = str_replace(['PR', 'GitHub', 'API'], ['pull request', 'Git Hub', 'A P I'], $text); - $text = preg_replace_callback('/\b([A-Z]{2,})\b/', function ($matches) { - return implode(' ', str_split($matches[1])); - }, $text); - - // Clean up extra whitespace - $text = preg_replace('/\s+/', ' ', $text); - - return trim($text); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0fed3d6..135289a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -56,12 +56,6 @@ public function boot(): void PrCommentsCommand::class, PrThreadsCommand::class, \App\Commands\PrAnalyzeCommand::class, - \App\Commands\GitHubClientGapAnalysisCommand::class, - \App\Commands\CodeRabbitStatusCommand::class, - \App\Commands\IssuesSpeakCommand::class, - \App\Commands\PrsSpeakCommand::class, - \App\Commands\CodeRabbitSpeakCommand::class, - \App\Commands\VoiceCommand::class, \App\Commands\ComponentConfigCommand::class, // \App\Commands\UpdateCommand::class, // Disabled - needs refactoring for new architecture // \App\Commands\System\CleanupCommand::class, // Disabled - uses old ComponentManager diff --git a/app/Services/ClaudeNarrationService.php b/app/Services/ClaudeNarrationService.php deleted file mode 100644 index a1c7456..0000000 --- a/app/Services/ClaudeNarrationService.php +++ /dev/null @@ -1,135 +0,0 @@ -buildContext($content); - - $claudePrompt = $this->buildClaudePrompt($contextData, $prompt); - - return $this->callClaude($claudePrompt); - } - - private function buildContext(NarrationContent $content): array - { - $context = [ - 'type' => $content->type, - 'number' => $content->number, - 'title' => $content->title, - 'description' => $content->description, - 'state' => $content->state, - 'author' => $content->author, - ]; - - // Add type-specific context - if ($content->type === 'pull_request') { - $context['stats'] = [ - 'additions' => $content->metadata['additions'] ?? 0, - 'deletions' => $content->metadata['deletions'] ?? 0, - 'changed_files' => $content->metadata['changed_files'] ?? 0, - 'commits' => $content->metadata['commits'] ?? 0, - 'draft' => $content->metadata['draft'] ?? false, - 'mergeable' => $content->metadata['mergeable'] ?? null, - ]; - } - - if ($content->type === 'issue') { - $context['labels'] = $content->metadata['labels'] ?? []; - $context['assignees'] = $content->metadata['assignees'] ?? []; - } - - // Add comments summary if available - if ($content->comments && $content->comments->isNotEmpty()) { - $context['comments_summary'] = $content->getCommentsSummary(); - } - - // Add reviews summary for PRs - if ($content->reviews && $content->reviews->isNotEmpty()) { - $context['reviews_summary'] = $content->getReviewsSummary(); - } - - return $context; - } - - private function buildClaudePrompt(array $context, string $userPrompt): string - { - $contextJson = json_encode($context, JSON_PRETTY_PRINT); - - return <<cleanClaudeOutput($output); - - return $output; - } - - private function cleanClaudeOutput(string $output): string - { - // Remove common Claude response patterns - $patterns = [ - '/^Here\'s.*?:\s*/i', - '/^I\'ll.*?:\s*/i', - '/^Let me.*?:\s*/i', - '/^```.*?```/s', - '/\*\*(.*?)\*\*/', // Bold markdown - '/\*(.*?)\*/', // Italic markdown - ]; - - $replacements = [ - '', - '', - '', - '', - '$1', - '$1', - ]; - - $cleaned = preg_replace($patterns, $replacements, $output); - - // Normalize whitespace - $cleaned = preg_replace('/\s+/', ' ', $cleaned); - - return trim($cleaned); - } -} diff --git a/app/Services/CodeRabbitAnalysisService.php b/app/Services/CodeRabbitAnalysisService.php deleted file mode 100644 index 21a50de..0000000 --- a/app/Services/CodeRabbitAnalysisService.php +++ /dev/null @@ -1,294 +0,0 @@ -fetchCodeRabbitComments($owner, $repo, $prNumber); - $issueComments = $this->fetchCodeRabbitIssueComments($owner, $repo, $prNumber); - - // Combine and categorize - $allComments = $this->categorizeComments($reviewComments, $issueComments); - - // Generate Claude analysis - $aiSummary = $this->generateClaudeAnalysis($allComments, $prNumber); - - return new CodeRabbitAnalysis( - prNumber: $prNumber, - repository: "{$owner}/{$repo}", - totalComments: $allComments->count(), - commentsByFile: $this->groupByFile($allComments), - commentsByCategory: $this->groupByCategory($allComments), - aiSummary: $aiSummary, - rawComments: $allComments - ); - } - - private function fetchCodeRabbitComments(string $owner, string $repo, int $prNumber): Collection - { - $command = "gh api repos/{$owner}/{$repo}/pulls/{$prNumber}/comments --paginate 2>/dev/null"; - $output = shell_exec($command); - - if (! $output) { - return collect(); - } - - $comments = json_decode($output, true); - if (! is_array($comments)) { - return collect(); - } - - return collect($comments) - ->filter(fn ($comment) => ($comment['user']['login'] ?? '') === 'coderabbitai[bot]') - ->map(fn ($comment) => $this->normalizeComment($comment, 'review')); - } - - private function fetchCodeRabbitIssueComments(string $owner, string $repo, int $prNumber): Collection - { - $command = "gh api repos/{$owner}/{$repo}/issues/{$prNumber}/comments --paginate 2>/dev/null"; - $output = shell_exec($command); - - if (! $output) { - return collect(); - } - - $comments = json_decode($output, true); - if (! is_array($comments)) { - return collect(); - } - - return collect($comments) - ->filter(fn ($comment) => ($comment['user']['login'] ?? '') === 'coderabbitai[bot]') - ->map(fn ($comment) => $this->normalizeComment($comment, 'issue')); - } - - private function normalizeComment(array $comment, string $type): array - { - $body = $comment['body'] ?? ''; - - return [ - 'id' => $comment['id'], - 'type' => $type, - 'file' => $comment['path'] ?? null, - 'line' => $comment['line'] ?? $comment['original_line'] ?? null, - 'body' => $body, - 'category' => $this->categorizeComment($body), - 'priority' => $this->determinePriority($body), - 'suggestion_type' => $this->determineSuggestionType($body), - 'created_at' => $comment['created_at'] ?? null, - 'url' => $comment['html_url'] ?? null, - ]; - } - - private function categorizeComment(string $body): string - { - $lower = strtolower($body); - - if (str_contains($lower, 'security') || str_contains($lower, 'vulnerability')) { - return 'security'; - } - - if (str_contains($lower, 'performance') || str_contains($lower, 'optimization')) { - return 'performance'; - } - - if (str_contains($lower, 'duplication') || str_contains($lower, 'duplicate')) { - return 'duplication'; - } - - if (str_contains($lower, 'style') || str_contains($lower, 'formatting')) { - return 'style'; - } - - if (str_contains($lower, 'error handling') || str_contains($lower, 'exception')) { - return 'error_handling'; - } - - if (str_contains($lower, 'test') || str_contains($lower, 'testing')) { - return 'testing'; - } - - return 'general'; - } - - private function determinePriority(string $body): string - { - $lower = strtolower($body); - - if (str_contains($lower, 'critical') || str_contains($lower, 'security') || str_contains($lower, 'vulnerability')) { - return 'high'; - } - - if (str_contains($lower, 'important') || str_contains($lower, 'performance') || str_contains($lower, 'bug')) { - return 'medium'; - } - - return 'low'; - } - - private function determineSuggestionType(string $body): string - { - $lower = strtolower($body); - - if (str_contains($lower, 'refactor') || str_contains($lower, 'extract')) { - return 'refactoring'; - } - - if (str_contains($lower, 'add') || str_contains($lower, 'implement')) { - return 'enhancement'; - } - - if (str_contains($lower, 'fix') || str_contains($lower, 'correct')) { - return 'bug_fix'; - } - - if (str_contains($lower, 'remove') || str_contains($lower, 'delete')) { - return 'removal'; - } - - return 'suggestion'; - } - - private function categorizeComments(Collection $reviewComments, Collection $issueComments): Collection - { - return $reviewComments->concat($issueComments)->sortBy('created_at'); - } - - private function groupByFile(Collection $comments): array - { - return $comments - ->groupBy('file') - ->map(fn ($fileComments) => [ - 'count' => $fileComments->count(), - 'priorities' => $fileComments->groupBy('priority')->map->count()->toArray(), - 'categories' => $fileComments->groupBy('category')->map->count()->toArray(), - 'comments' => $fileComments->toArray(), - ]) - ->toArray(); - } - - private function groupByCategory(Collection $comments): array - { - return $comments - ->groupBy('category') - ->map(fn ($categoryComments) => [ - 'count' => $categoryComments->count(), - 'priorities' => $categoryComments->groupBy('priority')->map->count()->toArray(), - 'files_affected' => $categoryComments->pluck('file')->filter()->unique()->count(), - 'comments' => $categoryComments->toArray(), - ]) - ->toArray(); - } - - private function generateClaudeAnalysis(Collection $comments, int $prNumber): array - { - if ($comments->isEmpty()) { - return [ - 'executive_summary' => 'No CodeRabbit feedback found for this PR.', - 'key_themes' => [], - 'action_priorities' => [], - 'technical_debt_assessment' => 'No technical debt identified.', - 'overall_code_quality' => 'No assessment available.', - ]; - } - - $analysisData = $this->prepareAnalysisData($comments); - $claudePrompt = $this->buildAnalysisPrompt($analysisData, $prNumber); - - $analysis = $this->callClaude($claudePrompt); - - return $this->parseClaudeAnalysis($analysis); - } - - private function prepareAnalysisData(Collection $comments): array - { - return [ - 'total_comments' => $comments->count(), - 'by_priority' => $comments->groupBy('priority')->map->count()->toArray(), - 'by_category' => $comments->groupBy('category')->map->count()->toArray(), - 'by_file' => $comments->groupBy('file')->map->count()->toArray(), - 'suggestion_types' => $comments->groupBy('suggestion_type')->map->count()->toArray(), - 'sample_comments' => $comments->take(10)->map(function ($comment) { - return [ - 'file' => $comment['file'], - 'line' => $comment['line'], - 'category' => $comment['category'], - 'priority' => $comment['priority'], - 'snippet' => substr($comment['body'], 0, 200).'...', - ]; - })->toArray(), - ]; - } - - private function buildAnalysisPrompt(array $data, int $prNumber): string - { - $dataJson = json_encode($data, JSON_PRETTY_PRINT); - - return << 'Analysis completed but could not parse structured response.', - 'key_themes' => ['Various code improvements suggested'], - 'action_priorities' => ['Review all CodeRabbit comments'], - 'technical_debt_assessment' => 'Assessment unavailable.', - 'overall_code_quality' => 'Review required.', - ]; - } -} diff --git a/app/Services/GitHubClientGapTracker.php b/app/Services/GitHubClientGapTracker.php deleted file mode 100644 index 98396bd..0000000 --- a/app/Services/GitHubClientGapTracker.php +++ /dev/null @@ -1,471 +0,0 @@ -testPrDataCompleteness($owner, $repo, $prNumber); - - // Test review capabilities - $gaps['review_data'] = $this->testReviewDataCompleteness($owner, $repo, $prNumber); - - // Test check status capabilities - $gaps['check_data'] = $this->testCheckStatusCapabilities($owner, $repo, $prNumber); - - // Test diff analysis capabilities - $gaps['diff_data'] = $this->testDiffAnalysisCapabilities($owner, $repo, $prNumber); - - // Test merge analysis capabilities - $gaps['merge_data'] = $this->testMergeAnalysisCapabilities($owner, $repo, $prNumber); - - return [ - 'gaps_found' => $gaps, - 'missing_endpoints' => $this->missingEndpoints, - 'incomplete_data' => $this->incompleteData, - 'recommended_issues' => $this->generateRecommendedIssues($gaps), - ]; - } - - private function testPrDataCompleteness(string $owner, string $repo, int $prNumber): array - { - $gaps = []; - - try { - // Test individual PR fetch - $pr = Github::pullRequests()->get($owner, $repo, $prNumber); - - // Check for missing fields we need - $requiredFields = [ - 'comments' => 'Comment count for discussion analysis', - 'review_comments' => 'Review comment count for code feedback analysis', - 'commits' => 'Commit count for change complexity analysis', - 'additions' => 'Lines added for size impact analysis', - 'deletions' => 'Lines deleted for size impact analysis', - 'changed_files' => 'File count for scope analysis', - 'mergeable' => 'Merge conflict status', - 'mergeable_state' => 'Detailed merge status', - 'merge_commit_sha' => 'Merge commit reference', - ]; - - foreach ($requiredFields as $field => $purpose) { - if (! isset($pr->$field) || $pr->$field === null) { - $gaps['missing_pr_fields'][] = [ - 'field' => $field, - 'purpose' => $purpose, - 'current_value' => $pr->$field ?? 'null', - ]; - } - } - - // Test if comment counts are accurate - if (isset($pr->comments) && $pr->comments === 0) { - $gaps['potential_comment_bug'] = [ - 'issue' => 'Comment count shows 0 but may be inaccurate', - 'test_needed' => 'Compare with GitHub API direct response', - ]; - } - - } catch (\Exception $e) { - $gaps['pr_fetch_error'] = [ - 'error' => $e->getMessage(), - 'endpoint' => "GET /repos/{$owner}/{$repo}/pulls/{$prNumber}", - ]; - } - - return $gaps; - } - - private function testReviewDataCompleteness(string $owner, string $repo, int $prNumber): array - { - $gaps = []; - - try { - // Test review list endpoint - $reviews = Github::pullRequests()->reviews($owner, $repo, $prNumber); - - if (is_array($reviews) && empty($reviews)) { - $gaps['no_reviews_data'] = [ - 'issue' => 'No reviews returned but PR may have reviews', - 'investigation' => 'Check if endpoint exists or returns correct data', - ]; - } else { - // Check if we got Collection or array - if (is_array($reviews)) { - $gaps['review_data_type'] = [ - 'issue' => 'Reviews endpoint returns array instead of Collection', - 'expected' => 'Collection with isEmpty() method', - 'actual' => 'Array', - ]; - } - - // Check review data completeness - $reviewsArray = is_array($reviews) ? $reviews : $reviews->toArray(); - foreach ($reviewsArray as $review) { - $missingReviewFields = []; - $requiredReviewFields = [ - 'state' => 'Approval status (APPROVED, CHANGES_REQUESTED, etc)', - 'submitted_at' => 'Review timestamp for timeline analysis', - 'user' => 'Reviewer information', - 'body' => 'Review summary for AI analysis', - ]; - - foreach ($requiredReviewFields as $field => $purpose) { - $fieldValue = is_array($review) ? ($review[$field] ?? null) : ($review->$field ?? null); - if ($fieldValue === null) { - $missingReviewFields[] = ['field' => $field, 'purpose' => $purpose]; - } - } - - if (! empty($missingReviewFields)) { - $reviewId = is_array($review) ? ($review['id'] ?? 'unknown') : ($review->id ?? 'unknown'); - $gaps['incomplete_review_data'][] = [ - 'review_id' => $reviewId, - 'missing_fields' => $missingReviewFields, - ]; - } - } - } - - // Test review comments endpoint - $reviewComments = Github::pullRequests()->comments($owner, $repo, $prNumber); - - if ((is_array($reviewComments) && empty($reviewComments)) || - (is_object($reviewComments) && method_exists($reviewComments, 'isEmpty') && $reviewComments->isEmpty())) { - $gaps['no_review_comments'] = [ - 'issue' => 'No review comments returned', - 'investigation' => 'May be missing endpoint or data mapping issue', - ]; - } - - } catch (\Exception $e) { - $gaps['review_fetch_error'] = [ - 'error' => $e->getMessage(), - 'missing_methods' => [ - 'Github::pullRequests()->reviews()', - 'Github::pullRequests()->comments()', - ], - ]; - - $this->missingEndpoints[] = [ - 'endpoint' => "GET /repos/{$owner}/{$repo}/pulls/{$prNumber}/reviews", - 'purpose' => 'Fetch PR reviews for approval analysis', - 'priority' => 'HIGH', - ]; - } - - return $gaps; - } - - private function testCheckStatusCapabilities(string $owner, string $repo, int $prNumber): array - { - $gaps = []; - - try { - // Get PR to find head SHA - $pr = Github::pullRequests()->get($owner, $repo, $prNumber); - $headSha = $pr->head->sha ?? null; - - if (! $headSha) { - $gaps['no_head_sha'] = 'Cannot get commit SHA for check status'; - - return $gaps; - } - - // Test check runs endpoint - try { - if (method_exists(Github::class, 'checks')) { - $checkRuns = Github::checks()->runs($owner, $repo, $headSha); - } else { - throw new \Exception('checks() resource does not exist'); - } - } catch (\Exception $e) { - $gaps['missing_check_runs'] = [ - 'error' => $e->getMessage(), - 'needed_endpoint' => "GET /repos/{$owner}/{$repo}/commits/{$headSha}/check-runs", - 'purpose' => 'CI/CD status analysis', - ]; - - $this->missingEndpoints[] = [ - 'endpoint' => "GET /repos/{$owner}/{$repo}/commits/{$headSha}/check-runs", - 'purpose' => 'Get CI/CD check status for merge readiness', - 'priority' => 'HIGH', - ]; - } - - // Test status checks endpoint - try { - if (method_exists(Github::class, 'commits')) { - $statuses = Github::commits()->status($owner, $repo, $headSha); - } else { - throw new \Exception('commits() resource does not exist'); - } - } catch (\Exception $e) { - $gaps['missing_commit_status'] = [ - 'error' => $e->getMessage(), - 'needed_endpoint' => "GET /repos/{$owner}/{$repo}/commits/{$headSha}/status", - 'purpose' => 'Legacy status checks', - ]; - - $this->missingEndpoints[] = [ - 'endpoint' => "GET /repos/{$owner}/{$repo}/commits/{$headSha}/status", - 'purpose' => 'Get commit status for legacy CI systems', - 'priority' => 'MEDIUM', - ]; - } - - } catch (\Exception $e) { - $gaps['check_status_error'] = $e->getMessage(); - } - - return $gaps; - } - - private function testDiffAnalysisCapabilities(string $owner, string $repo, int $prNumber): array - { - $gaps = []; - - try { - // Test if we can get PR diff data - $pr = Github::pullRequests()->get($owner, $repo, $prNumber); - - // Check if diff URLs are available - if (! isset($pr->diff_url) || ! isset($pr->patch_url)) { - $gaps['missing_diff_urls'] = 'No diff/patch URLs for file analysis'; - } - - // Test if we can fetch actual diff content - try { - // This method probably doesn't exist in github-client - if (method_exists(Github::pullRequests(), 'diff')) { - $diff = Github::pullRequests()->diff($owner, $repo, $prNumber); - } else { - throw new \Exception('diff() method does not exist on PullRequestResource'); - } - } catch (\Exception $e) { - $gaps['missing_diff_content'] = [ - 'error' => $e->getMessage(), - 'needed_endpoint' => "GET /repos/{$owner}/{$repo}/pulls/{$prNumber}/files", - 'purpose' => 'File-by-file diff analysis for AI insights', - ]; - - $this->missingEndpoints[] = [ - 'endpoint' => "GET /repos/{$owner}/{$repo}/pulls/{$prNumber}/files", - 'purpose' => 'Get detailed file changes for diff analysis', - 'priority' => 'HIGH', - ]; - } - - } catch (\Exception $e) { - $gaps['diff_analysis_error'] = $e->getMessage(); - } - - return $gaps; - } - - private function testMergeAnalysisCapabilities(string $owner, string $repo, int $prNumber): array - { - $gaps = []; - - try { - $pr = Github::pullRequests()->get($owner, $repo, $prNumber); - - // Check merge-related fields - $mergeFields = [ - 'mergeable' => 'Can be merged without conflicts', - 'mergeable_state' => 'Detailed merge status (clean, dirty, etc)', - 'rebaseable' => 'Can be rebased', - 'merge_commit_sha' => 'Preview merge commit', - ]; - - foreach ($mergeFields as $field => $purpose) { - if (! isset($pr->$field)) { - $gaps['missing_merge_fields'][] = [ - 'field' => $field, - 'purpose' => $purpose, - ]; - } - } - - // Test merge simulation (probably doesn't exist) - try { - if (method_exists(Github::pullRequests(), 'mergePreview')) { - $mergePreview = Github::pullRequests()->mergePreview($owner, $repo, $prNumber); - } else { - throw new \Exception('mergePreview() method does not exist'); - } - } catch (\Exception $e) { - $gaps['missing_merge_preview'] = [ - 'error' => $e->getMessage(), - 'needed_method' => 'mergePreview()', - 'purpose' => 'Simulate merge to check for conflicts', - ]; - } - - } catch (\Exception $e) { - $gaps['merge_analysis_error'] = $e->getMessage(); - } - - return $gaps; - } - - private function generateRecommendedIssues(array $gaps): array - { - $issues = []; - - // High priority: Comment count accuracy - if (isset($gaps['pr_data']['potential_comment_bug'])) { - $issues[] = [ - 'title' => 'PullRequest DTO comment fields returning 0 despite actual comments', - 'priority' => 'HIGH', - 'description' => 'The comments and review_comments fields show 0 even when PR has actual comments', - 'labels' => ['bug', 'high priority'], - 'endpoint_affected' => 'GET /repos/{owner}/{repo}/pulls/{number}', - ]; - } - - // Missing endpoints for comprehensive analysis - foreach ($this->missingEndpoints as $endpoint) { - $issues[] = [ - 'title' => "Add support for {$endpoint['endpoint']}", - 'priority' => $endpoint['priority'], - 'description' => "Need {$endpoint['endpoint']} endpoint for: {$endpoint['purpose']}", - 'labels' => ['enhancement', 'high priority'], - 'endpoint_needed' => $endpoint['endpoint'], - ]; - } - - // Missing data fields - handle various field structures - foreach ($gaps as $category => $categoryGaps) { - $missingFields = []; - - // Check for different field structures - if (isset($categoryGaps['missing_pr_fields'])) { - $missingFields = $categoryGaps['missing_pr_fields']; - } elseif (isset($categoryGaps['missing_merge_fields'])) { - $missingFields = $categoryGaps['missing_merge_fields']; - } elseif (isset($categoryGaps['missing_review_fields'])) { - $missingFields = $categoryGaps['missing_review_fields']; - } - - if (! empty($missingFields)) { - $fieldNames = array_column($missingFields, 'field'); - $issues[] = [ - 'title' => "Add missing {$category} fields: ".implode(', ', $fieldNames), - 'priority' => 'HIGH', - 'description' => "Missing fields in {$category} DTO needed for comprehensive PR analysis", - 'labels' => ['enhancement', 'high priority'], - 'missing_fields' => $missingFields, - 'category' => $category, - ]; - } - - // Handle data type issues - if (isset($categoryGaps['review_data_type'])) { - $issues[] = [ - 'title' => 'Fix reviews endpoint to return Collection instead of array', - 'priority' => 'MEDIUM', - 'description' => 'Reviews endpoint returns array instead of Collection, breaking isEmpty() calls', - 'labels' => ['bug', 'enhancement'], - 'endpoint_affected' => 'GET /repos/{owner}/{repo}/pulls/{number}/reviews', - ]; - } - } - - return $issues; - } - - /** - * Auto-submit issues to github-client repository - */ - public function submitDiscoveredIssues(array $recommendedIssues): array - { - $submitted = []; - - foreach ($recommendedIssues as $issue) { - try { - // Format issue body with detailed information - $body = $this->formatIssueBody($issue); - - // Submit via GitHub CLI (if available) or API - $issueUrl = $this->submitIssue($issue['title'], $body, $issue['labels']); - - $submitted[] = [ - 'title' => $issue['title'], - 'url' => $issueUrl, - 'priority' => $issue['priority'], - ]; - - } catch (\Exception $e) { - $submitted[] = [ - 'title' => $issue['title'], - 'error' => $e->getMessage(), - 'priority' => $issue['priority'], - ]; - } - } - - return $submitted; - } - - private function formatIssueBody(array $issue): string - { - $body = "## Issue Description\n{$issue['description']}\n\n"; - - if (isset($issue['endpoint_needed'])) { - $body .= "## Missing Endpoint\n`{$issue['endpoint_needed']}`\n\n"; - } - - if (isset($issue['endpoint_affected'])) { - $body .= "## Affected Endpoint\n`{$issue['endpoint_affected']}`\n\n"; - } - - if (isset($issue['missing_fields'])) { - $body .= "## Missing Fields\n"; - foreach ($issue['missing_fields'] as $field) { - $body .= "- `{$field['field']}`: {$field['purpose']}\n"; - } - $body .= "\n"; - } - - $body .= "## Priority\n{$issue['priority']}\n\n"; - $body .= "## Generated By\nConduit PR Analysis Gap Detection\n"; - - return $body; - } - - private function submitIssue(string $title, string $body, array $labels): string - { - // Try GitHub CLI first - $labelsStr = implode(',', $labels); - $command = sprintf( - 'gh issue create --repo jordanpartridge/github-client --title %s --body %s --label %s', - escapeshellarg($title), - escapeshellarg($body), - escapeshellarg($labelsStr) - ); - - $result = shell_exec($command); - - if ($result && filter_var(trim($result), FILTER_VALIDATE_URL)) { - return trim($result); - } - - throw new \Exception('Failed to submit issue via GitHub CLI'); - } -} diff --git a/app/Services/Security/ComponentSecurityValidator.php b/app/Services/Security/ComponentSecurityValidator.php index 179b744..0bac874 100644 --- a/app/Services/Security/ComponentSecurityValidator.php +++ b/app/Services/Security/ComponentSecurityValidator.php @@ -51,9 +51,12 @@ public function validateComponentPath(string $path): string $canonicalPath = Path::canonicalize($path); // Check if path is within allowed directories + // Normalize both paths to forward slashes for consistent comparison + $normalizedPath = str_replace('\\', '/', $canonicalPath); $isAllowed = false; foreach ($this->allowedPaths as $allowedPath) { - if (str_starts_with($canonicalPath, $allowedPath)) { + $normalizedAllowed = str_replace('\\', '/', $allowedPath); + if (str_starts_with($normalizedPath, $normalizedAllowed)) { $isAllowed = true; break; } @@ -207,27 +210,37 @@ public function validateBinaryIntegrity(string $binaryPath): void ); } - if (! is_executable($binaryPath)) { - throw new \InvalidArgumentException( - 'Binary is not executable: '.$binaryPath - ); - } + // On Windows, is_executable() only checks file extension, not actual permissions + // So we check if the file is readable and has an executable extension or is a PHP file + if (PHP_OS_FAMILY === 'Windows') { + if (! is_readable($binaryPath)) { + throw new \InvalidArgumentException( + 'Binary is not readable: '.$binaryPath + ); + } + // Windows considers files executable based on extension or if it's a script + $ext = strtolower(pathinfo($binaryPath, PATHINFO_EXTENSION)); + $executableExtensions = ['exe', 'bat', 'cmd', 'com', 'php', 'phar', '']; + if (! in_array($ext, $executableExtensions) && ! is_executable($binaryPath)) { + throw new \InvalidArgumentException( + 'Binary is not executable: '.$binaryPath + ); + } + } else { + if (! is_executable($binaryPath)) { + throw new \InvalidArgumentException( + 'Binary is not executable: '.$binaryPath + ); + } - // Check file permissions (should not be world-writable) - $perms = fileperms($binaryPath); - if ($perms & 0002) { - throw new \InvalidArgumentException( - 'Binary is world-writable, which is a security risk: '.$binaryPath - ); + // Check file permissions on Unix (should not be world-writable) + $perms = fileperms($binaryPath); + if ($perms & 0002) { + throw new \InvalidArgumentException( + 'Binary is world-writable, which is a security risk: '.$binaryPath + ); + } } - - // Optionally check file ownership (uncomment if needed) - // $owner = fileowner($binaryPath); - // if ($owner !== getmyuid()) { - // throw new \InvalidArgumentException( - // 'Binary is not owned by current user: ' . $binaryPath - // ); - // } } /** diff --git a/app/Services/VoiceNarrationService.php b/app/Services/VoiceNarrationService.php deleted file mode 100644 index 1812a33..0000000 --- a/app/Services/VoiceNarrationService.php +++ /dev/null @@ -1,160 +0,0 @@ -resolveNarrator($config->voice); - $speech = $narrator->generate($content, $config); - $this->speak($speech, $config); - } catch (\RuntimeException $e) { - // Fallback to simple text output if narrator system fails - $this->handleNarratorFailure($content, $e); - } - } - - private function handleNarratorFailure(NarrationContent $content, \RuntimeException $e): void - { - // Log the error for debugging - if (function_exists('logger')) { - logger()->warning("Voice narrator failed: {$e->getMessage()}"); - } - - // Fallback to basic text summary - $fallbackText = $this->generateFallbackText($content); - $this->fallbackToText($fallbackText); - } - - private function generateFallbackText(NarrationContent $content): string - { - $parts = []; - - $parts[] = "Title: {$content->title}"; - $parts[] = "Author: {$content->author}"; - $parts[] = "Status: {$content->state}"; - - if ($content->description) { - $parts[] = 'Description: '.substr($content->description, 0, 100).'...'; - } - - return implode('. ', $parts).'.'; - } - - private function resolveNarrator(VoiceStyle $voice): VoiceNarratorInterface - { - // Try requested voice style first - $narrator = $this->narrators->get($voice->value); - - if ($narrator) { - return $narrator; - } - - // Fallback to default narrator - $defaultNarrator = $this->narrators->get('default'); - - if ($defaultNarrator) { - return $defaultNarrator; - } - - // If no narrators available, throw descriptive error - throw new \RuntimeException( - "No narrator available for style '{$voice->value}' and no default narrator configured. ". - 'Available narrators: '.implode(', ', $this->narrators->keys()->toArray()) - ); - } - - public function speak(string $speech, SpeechConfiguration $config): void - { - $platform = $this->detectPlatform(); - - match ($platform) { - 'darwin' => $this->speakMacOS($speech, $config), - 'windows' => $this->speakWindows($speech, $config), - 'linux' => $this->speakLinux($speech, $config), - default => $this->fallbackToText($speech), - }; - } - - private function detectPlatform(): string - { - return strtolower(PHP_OS_FAMILY); - } - - private function speakMacOS(string $speech, SpeechConfiguration $config): void - { - $rate = match ($config->speed) { - SpeechSpeed::Slow => 100, - SpeechSpeed::Fast => 200, - default => 140, - }; - - $command = sprintf('say -r %d %s', $rate, escapeshellarg($speech)); - shell_exec($command); - } - - private function speakWindows(string $speech, SpeechConfiguration $config): void - { - // Windows trolling included 😈 - sleep(1); - - $trolledSpeech = 'Why are you using Windows for development? '. - 'Get a real operating system first. '. - "Anyway, here's your briefing from the inferior platform: ".$speech; - - $rate = match ($config->speed) { - SpeechSpeed::Slow => -4, - SpeechSpeed::Fast => 0, - default => -2, - }; - - $psCommand = sprintf( - 'Add-Type -AssemblyName System.Speech; '. - '$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; '. - '$speak.Rate = %d; '. - '$speak.Speak(%s); '. - '$speak.Dispose()', - $rate, - escapeshellarg($trolledSpeech) - ); - - shell_exec("powershell -Command \"{$psCommand}\""); - } - - private function speakLinux(string $speech, SpeechConfiguration $config): void - { - if (shell_exec('which espeak 2>/dev/null')) { - $speedFlag = match ($config->speed) { - SpeechSpeed::Slow => '-s 120', - SpeechSpeed::Fast => '-s 200', - default => '-s 160', - }; - shell_exec("espeak {$speedFlag} ".escapeshellarg($speech)); - } else { - $this->fallbackToText($speech); - } - } - - private function fallbackToText(string $speech): void - { - echo "\n📝 Text version:\n"; - echo "═══════════════\n"; - echo $speech."\n"; - echo "═══════════════\n"; - } -} diff --git a/app/ValueObjects/CodeRabbitAnalysis.php b/app/ValueObjects/CodeRabbitAnalysis.php deleted file mode 100644 index 0185128..0000000 --- a/app/ValueObjects/CodeRabbitAnalysis.php +++ /dev/null @@ -1,82 +0,0 @@ -totalComments === 0) { - return 'CodeRabbit found no issues with this PR. Clean code, ready to ship!'; - } - - $summary = $this->aiSummary['executive_summary'] ?? 'CodeRabbit provided feedback on this PR.'; - - $priorityBreakdown = $this->getPriorityBreakdown(); - $topFiles = $this->getTopFilesWithIssues(); - $keyThemes = $this->aiSummary['key_themes'] ?? []; - - $narration = "{$summary} "; - $narration .= "Total feedback: {$this->totalComments} comments. "; - $narration .= $priorityBreakdown.' '; - - if (! empty($topFiles)) { - $narration .= 'Main files needing attention: '.implode(', ', $topFiles).'. '; - } - - if (! empty($keyThemes)) { - $narration .= 'Key themes: '.implode(', ', array_slice($keyThemes, 0, 3)).'. '; - } - - $actionPriorities = $this->aiSummary['action_priorities'] ?? []; - if (! empty($actionPriorities)) { - $narration .= 'Top priority: '.$actionPriorities[0].'.'; - } - - return $narration; - } - - private function getPriorityBreakdown(): string - { - $priorities = $this->rawComments->groupBy('priority')->map->count(); - - $parts = []; - if ($priorities->get('high', 0) > 0) { - $parts[] = "{$priorities['high']} high priority"; - } - if ($priorities->get('medium', 0) > 0) { - $parts[] = "{$priorities['medium']} medium priority"; - } - if ($priorities->get('low', 0) > 0) { - $parts[] = "{$priorities['low']} low priority"; - } - - return empty($parts) ? '' : 'Breakdown: '.implode(', ', $parts).'.'; - } - - private function getTopFilesWithIssues(int $limit = 3): array - { - return array_keys( - array_slice( - array_filter($this->commentsByFile, fn ($file) => $file !== null), - 0, - $limit, - true - ) - ); - } -} diff --git a/app/ValueObjects/NarrationContent.php b/app/ValueObjects/NarrationContent.php deleted file mode 100644 index 8c03529..0000000 --- a/app/ValueObjects/NarrationContent.php +++ /dev/null @@ -1,148 +0,0 @@ - collect($issue['labels'] ?? [])->pluck('name')->toArray(), - 'assignees' => collect($issue['assignees'] ?? [])->pluck('login')->toArray(), - 'created_at' => $issue['created_at'] ?? null, - 'updated_at' => $issue['updated_at'] ?? null, - ], - comments: $comments, - ); - } - - public static function fromPullRequest(array $pr, ?Collection $comments = null, ?Collection $reviews = null): self - { - return new self( - type: 'pull_request', - number: $pr['number'], - title: $pr['title'], - description: self::sanitizeText($pr['body'] ?? ''), - state: $pr['state'], - author: $pr['user']['login'] ?? 'Unknown', - metadata: [ - 'draft' => $pr['draft'] ?? false, - 'mergeable' => $pr['mergeable'] ?? null, - 'additions' => $pr['additions'] ?? 0, - 'deletions' => $pr['deletions'] ?? 0, - 'changed_files' => $pr['changed_files'] ?? 0, - 'commits' => $pr['commits'] ?? 0, - 'base_branch' => $pr['base']['ref'] ?? 'main', - 'head_branch' => $pr['head']['ref'] ?? 'feature', - 'created_at' => $pr['created_at'] ?? null, - 'updated_at' => $pr['updated_at'] ?? null, - ], - comments: $comments, - reviews: $reviews, - ); - } - - private static function sanitizeText(string $text): string - { - // Remove markdown formatting - $text = strip_tags($text); - - // Remove excessive whitespace and line breaks - $text = preg_replace('/\s+/', ' ', $text); - - // Truncate for speech - if (strlen($text) > 300) { - $text = substr($text, 0, 300).'... and more details'; - } - - return trim($text); - } - - public function getStatsSummary(): string - { - if ($this->type === 'pull_request') { - $additions = $this->metadata['additions'] ?? 0; - $deletions = $this->metadata['deletions'] ?? 0; - $files = $this->metadata['changed_files'] ?? 0; - $commits = $this->metadata['commits'] ?? 0; - - return "Statistics: {$additions} additions, {$deletions} deletions, {$files} files changed, {$commits} commits"; - } - - return ''; - } - - public function getCommentsSummary(): string - { - if (! $this->comments || $this->comments->isEmpty()) { - return 'No comments yet.'; - } - - $total = $this->comments->count(); - $recentComments = $this->comments->sortByDesc('created_at')->take(3); - - $summary = "There are {$total} comments. "; - - if ($total <= 3) { - $summary .= 'Recent discussion includes: '; - $summary .= $recentComments->map(function ($comment) { - $author = $comment['user']['login'] ?? 'Someone'; - $snippet = substr(strip_tags($comment['body'] ?? ''), 0, 50); - - return "{$author} said: {$snippet}"; - })->join('. '); - } else { - $summary .= 'Most recent comments from: '; - $summary .= $recentComments->pluck('user.login')->unique()->join(', '); - } - - return $summary; - } - - public function getReviewsSummary(): string - { - if (! $this->reviews || $this->reviews->isEmpty()) { - return 'No reviews yet.'; - } - - $approved = $this->reviews->where('state', 'APPROVED')->count(); - $requestedChanges = $this->reviews->where('state', 'CHANGES_REQUESTED')->count(); - $commented = $this->reviews->where('state', 'COMMENTED')->count(); - - $parts = []; - if ($approved > 0) { - $parts[] = "{$approved} approvals"; - } - if ($requestedChanges > 0) { - $parts[] = "{$requestedChanges} change requests"; - } - if ($commented > 0) { - $parts[] = "{$commented} review comments"; - } - - return 'Review status: '.implode(', ', $parts); - } -} diff --git a/app/ValueObjects/SpeechConfiguration.php b/app/ValueObjects/SpeechConfiguration.php deleted file mode 100644 index 95acdd2..0000000 --- a/app/ValueObjects/SpeechConfiguration.php +++ /dev/null @@ -1,35 +0,0 @@ -claudePrompt); - } -} diff --git a/composer.json b/composer.json index b0ee889..2472392 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,10 @@ "chillerlan/php-qrcode": "^5.0", "guzzlehttp/guzzle": "^7.8", "illuminate/database": "^11.45", + "illuminate/log": "^11.0", "jordanpartridge/github-client": "^2.9", "laravel-zero/framework": "^11.36.1", + "symfony/filesystem": "^7.4", "symfony/process": "^6.0|^7.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 67a8eb9..951a94f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8418156dbdb1d976725a332a99924559", + "content-hash": "7a9228796df5de701adfd673425ef162", "packages": [ { "name": "brick/math", @@ -355,33 +355,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -426,7 +425,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -442,7 +441,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "dragonmantank/cron-expression", @@ -1147,7 +1146,7 @@ }, { "name": "illuminate/collections", - "version": "v11.45.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -1203,7 +1202,7 @@ }, { "name": "illuminate/conditionable", - "version": "v11.45.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -1414,16 +1413,16 @@ }, { "name": "illuminate/contracts", - "version": "v11.45.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8" + "reference": "4787042340aae19a7ea0fa82f4073c4826204a48" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/4b2a67d1663f50085bc91e6371492697a5d2d4e8", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/4787042340aae19a7ea0fa82f4073c4826204a48", + "reference": "4787042340aae19a7ea0fa82f4073c4826204a48", "shasum": "" }, "require": { @@ -1458,7 +1457,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-11-27T16:16:07+00:00" }, { "name": "illuminate/database", @@ -1651,9 +1650,62 @@ }, "time": "2025-03-24T11:54:20+00:00" }, + { + "name": "illuminate/log", + "version": "v11.47.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/log.git", + "reference": "73bcd8423739c751442b7e618153c73f78e844cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/log/zipball/73bcd8423739c751442b7e618153c73f78e844cd", + "reference": "73bcd8423739c751442b7e618153c73f78e844cd", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0", + "illuminate/support": "^11.0", + "monolog/monolog": "^3.0", + "php": "^8.2", + "psr/log": "^1.0|^2.0|^3.0" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Log package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, { "name": "illuminate/macroable", - "version": "v11.45.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -1798,16 +1850,16 @@ }, { "name": "illuminate/support", - "version": "v11.45.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "9732f41d7a9836a2c466ab06460efc732aeb417a" + "reference": "20fbd9f9f502a55de0cbba3f3f81444b7c44af4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/9732f41d7a9836a2c466ab06460efc732aeb417a", - "reference": "9732f41d7a9836a2c466ab06460efc732aeb417a", + "url": "https://api.github.com/repos/illuminate/support/zipball/20fbd9f9f502a55de0cbba3f3f81444b7c44af4b", + "reference": "20fbd9f9f502a55de0cbba3f3f81444b7c44af4b", "shasum": "" }, "require": { @@ -1871,7 +1923,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-11T20:47:08+00:00" + "time": "2025-11-27T16:16:32+00:00" }, { "name": "illuminate/testing", @@ -2676,18 +2728,121 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, { "name": "nesbot/carbon", - "version": "3.10.1", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -2695,9 +2850,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2705,13 +2860,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.75.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", "phpmd/phpmd": "^2.15.0", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.17", - "phpunit/phpunit": "^10.5.46", - "squizlabs/php_codesniffer": "^3.13.0" + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" }, "bin": [ "bin/carbon" @@ -2779,7 +2934,7 @@ "type": "tidelift" } ], - "time": "2025-06-21T15:19:35+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nunomaduro/collision", @@ -3990,16 +4145,16 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -4044,7 +4199,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -4055,12 +4210,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/console", @@ -4456,6 +4615,76 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-27T13:27:24+00:00" + }, { "name": "symfony/finder", "version": "v7.3.0", @@ -4522,7 +4751,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4581,7 +4810,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -4592,6 +4821,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4760,7 +4993,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -4821,7 +5054,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -4832,6 +5065,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4841,7 +5078,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -4901,7 +5138,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -4912,6 +5149,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4921,16 +5162,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -4977,7 +5218,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -4988,12 +5229,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/process", @@ -5058,16 +5303,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -5121,7 +5366,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -5132,12 +5377,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", @@ -5228,23 +5477,23 @@ }, { "name": "symfony/translation", - "version": "v7.3.1", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -5263,17 +5512,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5304,7 +5553,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.1" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -5315,25 +5564,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -5382,7 +5635,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -5393,12 +5646,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/var-dumper", diff --git a/tests/Feature/ComponentSecurityTest.php b/tests/Feature/ComponentSecurityTest.php index dc9dd6f..b8f7c4a 100644 --- a/tests/Feature/ComponentSecurityTest.php +++ b/tests/Feature/ComponentSecurityTest.php @@ -74,14 +74,19 @@ public function it_prevents_path_traversal_in_component_paths() // This should be caught by the security validator $components = $discovery->discover(); + // Get home directory (cross-platform) + $home = getenv('HOME') ?: getenv('USERPROFILE') ?: sys_get_temp_dir(); + // Verify no components from outside allowed paths foreach ($components as $component) { - $path = $component['path']; + $path = str_replace('\\', '/', $component['path']); + $basePath = str_replace('\\', '/', base_path('components/')); + $homePath = str_replace('\\', '/', $home . '/.conduit/components/'); // Should be within allowed directories $this->assertTrue( - str_starts_with($path, base_path('components/')) || - str_starts_with($path, $_SERVER['HOME'].'/.conduit/components/'), + str_starts_with($path, $basePath) || + str_starts_with($path, $homePath), "Component path should be within allowed directories: $path" ); } @@ -90,6 +95,11 @@ public function it_prevents_path_traversal_in_component_paths() /** @test */ public function it_sanitizes_user_arguments_before_delegation() { + // Skip on Windows - bash scripts don't work the same way + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('Test requires bash, skipped on Windows'); + } + // Create a test component directory $testDir = base_path('components/core/test'); $testBinary = $testDir.'/test'; @@ -136,6 +146,11 @@ public function it_sanitizes_user_arguments_before_delegation() /** @test */ public function it_validates_binary_permissions_before_execution() { + // Skip on Windows - chmod doesn't work the same way + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('Unix permissions test, skipped on Windows'); + } + $validator = app(ComponentSecurityValidator::class); // Create test directory @@ -164,6 +179,11 @@ public function it_validates_binary_permissions_before_execution() /** @test */ public function it_handles_malformed_component_commands_safely() { + // Skip on Windows - bash scripts don't work the same way + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('Test requires bash, skipped on Windows'); + } + $discovery = app(StandaloneComponentDiscovery::class); // Create a test component with malicious command names in config diff --git a/tests/Unit/Enums/SpeechSpeedTest.php b/tests/Unit/Enums/SpeechSpeedTest.php deleted file mode 100644 index e8ed2b1..0000000 --- a/tests/Unit/Enums/SpeechSpeedTest.php +++ /dev/null @@ -1,21 +0,0 @@ -value)->toBe('slow'); - expect(SpeechSpeed::Normal->value)->toBe('normal'); - expect(SpeechSpeed::Fast->value)->toBe('fast'); - expect(SpeechSpeed::Blazing->value)->toBe('blazing'); -}); - -it('can list all cases', function () { - $cases = SpeechSpeed::cases(); - expect($cases)->toHaveCount(4); - expect(array_map(fn ($case) => $case->value, $cases))->toBe([ - 'slow', - 'normal', - 'fast', - 'blazing', - ]); -}); diff --git a/tests/Unit/Enums/VoiceStyleTest.php b/tests/Unit/Enums/VoiceStyleTest.php deleted file mode 100644 index eb14324..0000000 --- a/tests/Unit/Enums/VoiceStyleTest.php +++ /dev/null @@ -1,21 +0,0 @@ -value)->toBe('default'); - expect(VoiceStyle::Dramatic->value)->toBe('dramatic'); - expect(VoiceStyle::Sarcastic->value)->toBe('sarcastic'); - expect(VoiceStyle::Coach->value)->toBe('coach'); - expect(VoiceStyle::Robot->value)->toBe('robot'); - expect(VoiceStyle::Reviewer->value)->toBe('reviewer'); - expect(VoiceStyle::Executive->value)->toBe('executive'); - expect(VoiceStyle::Zen->value)->toBe('zen'); - expect(VoiceStyle::Pirate->value)->toBe('pirate'); - expect(VoiceStyle::Documentary->value)->toBe('documentary'); -}); - -it('can list all cases', function () { - $cases = VoiceStyle::cases(); - expect($cases)->toHaveCount(10); -}); diff --git a/tests/Unit/Services/Security/ComponentSecurityValidatorTest.php b/tests/Unit/Services/Security/ComponentSecurityValidatorTest.php index 6a9a3c7..d5a6e07 100644 --- a/tests/Unit/Services/Security/ComponentSecurityValidatorTest.php +++ b/tests/Unit/Services/Security/ComponentSecurityValidatorTest.php @@ -134,7 +134,10 @@ public function it_validates_component_paths_within_allowed_directories() $validPath = $basePath.'/github'; $result = $this->validator->validateComponentPath($validPath); - $this->assertStringStartsWith($basePath, $result); + // Normalize both paths to forward slashes for comparison (cross-platform) + $normalizedResult = str_replace('\\', '/', $result); + $normalizedBase = str_replace('\\', '/', $basePath); + $this->assertStringStartsWith($normalizedBase, $normalizedResult); } /** @test */ @@ -171,9 +174,9 @@ public function it_detects_path_traversal_attempts() /** @test */ public function it_builds_safe_command_arrays() { - // Create a test directory structure - $testComponentDir = base_path('components/core/test-component'); - $testBinary = $testComponentDir.'/test-component'; + // Create a test directory structure with unique name + $testComponentDir = base_path('components/core/test-cmd-arrays'); + $testBinary = $testComponentDir.'/test-cmd-arrays'; // Add test path to allowed paths $this->validator->addAllowedPath($testComponentDir); @@ -183,7 +186,9 @@ public function it_builds_safe_command_arrays() mkdir($testComponentDir, 0755, true); } touch($testBinary); - chmod($testBinary, 0755); + if (PHP_OS_FAMILY !== 'Windows') { + chmod($testBinary, 0755); + } $result = $this->validator->buildSafeCommand( $testBinary, @@ -193,17 +198,20 @@ public function it_builds_safe_command_arrays() ); // Check the command array is properly sanitized - $this->assertEquals($testBinary, $result[0]); + // Normalize path separators for cross-platform comparison + $this->assertEquals(str_replace('\\', '/', $testBinary), str_replace('\\', '/', $result[0])); $this->assertEquals('delegated', $result[1]); $this->assertEquals('test:command', $result[2]); - $this->assertEquals("'arg1'", $result[3]); - $this->assertEquals("'arg with space'", $result[4]); - $this->assertEquals("'arg;with;semicolon'", $result[5]); + // escapeshellarg() uses double quotes on Windows + $quote = PHP_OS_FAMILY === 'Windows' ? '"' : "'"; + $this->assertEquals("{$quote}arg1{$quote}", $result[3]); + $this->assertEquals("{$quote}arg with space{$quote}", $result[4]); + $this->assertEquals("{$quote}arg;with;semicolon{$quote}", $result[5]); $this->assertEquals('--option1', $result[6]); - $this->assertEquals("'value1'", $result[7]); + $this->assertEquals("{$quote}value1{$quote}", $result[7]); $this->assertEquals('--flag', $result[8]); $this->assertEquals('--dangerous', $result[9]); - $this->assertEquals("'val;ue'", $result[10]); + $this->assertEquals("{$quote}val;ue{$quote}", $result[10]); // Clean up unlink($testBinary); @@ -213,8 +221,8 @@ public function it_builds_safe_command_arrays() /** @test */ public function it_validates_binary_integrity() { - $testComponentDir = base_path('components/core/test-component'); - $testBinary = $testComponentDir.'/test-component'; + $testComponentDir = base_path('components/core/test-binary-integrity'); + $testBinary = $testComponentDir.'/test-binary-integrity'; // Add test path to allowed paths $this->validator->addAllowedPath($testComponentDir); @@ -224,33 +232,41 @@ public function it_validates_binary_integrity() } // Test non-existent binary - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Binary does not exist'); - $this->validator->validateBinaryIntegrity($testBinary); - - // Create binary - touch($testBinary); - - // Test non-executable binary - chmod($testBinary, 0644); try { $this->validator->validateBinaryIntegrity($testBinary); - $this->fail('Should have thrown exception for non-executable binary'); + $this->fail('Should have thrown exception for non-existent binary'); } catch (\InvalidArgumentException $e) { - $this->assertStringContainsString('not executable', $e->getMessage()); + $this->assertStringContainsString('does not exist', $e->getMessage()); } - // Test world-writable binary - chmod($testBinary, 0777); - try { - $this->validator->validateBinaryIntegrity($testBinary); - $this->fail('Should have thrown exception for world-writable binary'); - } catch (\InvalidArgumentException $e) { - $this->assertStringContainsString('world-writable', $e->getMessage()); + // Create binary + touch($testBinary); + + // Skip Unix-specific permission tests on Windows (chmod doesn't work the same) + if (PHP_OS_FAMILY !== 'Windows') { + // Test non-executable binary + chmod($testBinary, 0644); + try { + $this->validator->validateBinaryIntegrity($testBinary); + $this->fail('Should have thrown exception for non-executable binary'); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString('not executable', $e->getMessage()); + } + + // Test world-writable binary + chmod($testBinary, 0777); + try { + $this->validator->validateBinaryIntegrity($testBinary); + $this->fail('Should have thrown exception for world-writable binary'); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString('world-writable', $e->getMessage()); + } + + // Test valid binary + chmod($testBinary, 0755); } - // Test valid binary - chmod($testBinary, 0755); + // On all platforms, a readable file should pass (Windows uses different permission model) $this->validator->validateBinaryIntegrity($testBinary); // Should not throw // Clean up diff --git a/tests/Unit/ValueObjects/SpeechConfigurationTest.php b/tests/Unit/ValueObjects/SpeechConfigurationTest.php deleted file mode 100644 index d41540f..0000000 --- a/tests/Unit/ValueObjects/SpeechConfigurationTest.php +++ /dev/null @@ -1,39 +0,0 @@ - 'dramatic', - 'speed' => 'fast', - 'include-stats' => true, - 'include-comments' => true, - 'claude' => 'custom prompt', - ]); - - expect($config->voice)->toBe(VoiceStyle::Dramatic); - expect($config->speed)->toBe(SpeechSpeed::Fast); - expect($config->includeStats)->toBeTrue(); - expect($config->includeComments)->toBeTrue(); - expect($config->claudePrompt)->toBe('custom prompt'); -}); - -it('uses default values when options are empty', function () { - $config = SpeechConfiguration::fromOptions([]); - - expect($config->voice)->toBe(VoiceStyle::Default); - expect($config->speed)->toBe(SpeechSpeed::Normal); - expect($config->includeStats)->toBeFalse(); - expect($config->includeComments)->toBeFalse(); - expect($config->claudePrompt)->toBeNull(); -}); - -it('identifies claude powered based on prompt presence', function () { - $withClaude = SpeechConfiguration::fromOptions(['claude' => 'some prompt']); - expect($withClaude->isClaudePowered())->toBeTrue(); - - $withoutClaude = SpeechConfiguration::fromOptions([]); - expect($withoutClaude->isClaudePowered())->toBeFalse(); -}); From c4301f9e2cb281d37411d9747ca30de3481862cf Mon Sep 17 00:00:00 2001 From: jordanpartridge <9040417+jordanpartridge@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:53:14 +0000 Subject: [PATCH 2/4] style: Fix code style with Laravel Pint --- tests/Feature/ComponentSecurityTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/ComponentSecurityTest.php b/tests/Feature/ComponentSecurityTest.php index b8f7c4a..aee30c8 100644 --- a/tests/Feature/ComponentSecurityTest.php +++ b/tests/Feature/ComponentSecurityTest.php @@ -81,7 +81,7 @@ public function it_prevents_path_traversal_in_component_paths() foreach ($components as $component) { $path = str_replace('\\', '/', $component['path']); $basePath = str_replace('\\', '/', base_path('components/')); - $homePath = str_replace('\\', '/', $home . '/.conduit/components/'); + $homePath = str_replace('\\', '/', $home.'/.conduit/components/'); // Should be within allowed directories $this->assertTrue( From 20a132aee41fea74d28c4f0f34a9b3d4211382b5 Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 7 Dec 2025 17:49:41 -0700 Subject: [PATCH 3/4] fix: Remove dead VoiceNarrationService references from AppServiceProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VoiceNarrationService and related narrator classes were deleted as part of the bloat removal, but AppServiceProvider still had: - Import statement for VoiceNarrationService - registerVoiceNarrationSystem() method call - The entire registerVoiceNarrationSystem() method This would cause runtime errors when the service container tried to resolve the non-existent classes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Providers/AppServiceProvider.php | 37 ---------------------------- 1 file changed, 37 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 135289a..2be5e5c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -21,7 +21,6 @@ use App\Services\GitHub\PrAnalysisService; use App\Services\GitHub\PrCreateService; use App\Services\GithubAuthService; -use App\Services\VoiceNarrationService; use Illuminate\Support\Collection; // GitHub client imports - only used if package is installed use Illuminate\Support\ServiceProvider; @@ -109,42 +108,6 @@ public function register(): void // Register component delegation service $this->app->singleton(\App\Services\ComponentDelegationService::class); - - // Register voice narration system - $this->registerVoiceNarrationSystem(); - } - - /** - * Register the voice narration system with dependency injection - */ - private function registerVoiceNarrationSystem(): void - { - // Register narrator collection factory - $this->app->singleton('voice.narrators', function ($app) { - $narrators = collect(); - - // Register available narrators - if (class_exists('App\Narrators\DefaultNarrator')) { - $narrators->put('default', $app->make('App\Narrators\DefaultNarrator')); - } - - if (class_exists('App\Narrators\ClaudeNarrator')) { - $narrators->put('claude', $app->make('App\Narrators\ClaudeNarrator')); - } - - // Add more narrators as they're implemented - // $narrators->put('dramatic', $app->make('App\Narrators\DramaticNarrator')); - // $narrators->put('sarcastic', $app->make('App\Narrators\SarcasticNarrator')); - - return $narrators; - }); - - // Register VoiceNarrationService with narrator collection - $this->app->singleton(VoiceNarrationService::class, function ($app) { - return new VoiceNarrationService( - $app->make('voice.narrators') - ); - }); } /** From 667d22d6e136f4c4cd45db357b2308dd56649329 Mon Sep 17 00:00:00 2001 From: jordanpartridge <9040417+jordanpartridge@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:50:20 +0000 Subject: [PATCH 4/4] style: Fix code style with Laravel Pint --- app/Providers/AppServiceProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2be5e5c..e0644d2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -21,7 +21,6 @@ use App\Services\GitHub\PrAnalysisService; use App\Services\GitHub\PrCreateService; use App\Services\GithubAuthService; -use Illuminate\Support\Collection; // GitHub client imports - only used if package is installed use Illuminate\Support\ServiceProvider; use JordanPartridge\GithubClient\Contracts\GithubConnectorInterface;