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..e0644d2 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -21,8 +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;
use JordanPartridge\GithubClient\Contracts\GithubConnectorInterface;
@@ -56,12 +54,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
@@ -115,42 +107,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')
- );
- });
}
/**
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 <</dev/null";
- $output = shell_exec($command);
-
- if (! $output) {
- throw new \Exception('Claude Code CLI failed or not available');
- }
-
- $output = trim($output);
-
- // Clean up any Claude formatting
- $output = $this->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 <</dev/null";
- $output = shell_exec($command);
-
- if (! $output) {
- throw new \Exception('Claude Code CLI failed or not available');
- }
-
- return trim($output);
- }
-
- private function parseClaudeAnalysis(string $analysis): array
- {
- // Try to extract JSON from Claude's response
- if (preg_match('/\{.*\}/s', $analysis, $matches)) {
- $json = json_decode($matches[0], true);
- if ($json) {
- return $json;
- }
- }
-
- // Fallback to basic parsing
- return [
- 'executive_summary' => '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..aee30c8 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();
-});