From e1b6ade6a5ce701b6364a697a250396f6ada876e Mon Sep 17 00:00:00 2001 From: Jordan Partridge Date: Fri, 8 Aug 2025 21:56:05 -0700 Subject: [PATCH 1/2] feat: Add comprehensive quality checks with CI/CD pipeline - Created QualityCommand with beautiful Laravel Prompts tables - Parse and display Pint, Larastan, and Pest issues in formatted tables - Add clickable file paths and line numbers using OSC 8 hyperlinks - Implement GitHub Actions workflow for automated quality checks - Run quality checks on PRs and pushes to master - Add component management commands (list, remove, update) - Fix all Larastan errors and achieve passing quality checks - Configure Laravel native features over Symfony - Add proper configuration files for conduit and github This ensures THE SHIT maintains high code quality standards\! --- .claude/settings.local.json | 3 +- .github/workflows/quality.yml | 191 +++ CLAUDE.md | 242 +++ app/Commands/ComponentConfigCommand.php | 39 +- app/Commands/ComponentInstallCommand.php | 110 +- app/Commands/ComponentListCommand.php | 135 ++ app/Commands/ComponentRemoveCommand.php | 154 ++ app/Commands/ComponentScaffoldCommand.php | 78 +- app/Commands/ComponentUpdateCommand.php | 271 +++ app/Commands/ConduitCommand.php | 33 +- app/Commands/DetectionTestCommand.php | 12 +- app/Commands/LsCommand.php | 246 +-- app/Commands/QualityCommand.php | 464 ++++++ app/Commands/TestHumanAiCommand.php | 8 +- app/Providers/ComponentServiceProvider.php | 35 +- app/ValueObjects/ComponentResult.php | 2 +- composer.json | 21 +- composer.lock | 1747 +++++++++++++++++--- config/conduit.php | 51 + config/database.php | 4 +- config/github.php | 80 + phpstan.neon | 13 + 22 files changed, 3466 insertions(+), 473 deletions(-) create mode 100644 .github/workflows/quality.yml create mode 100644 CLAUDE.md create mode 100644 app/Commands/ComponentListCommand.php create mode 100644 app/Commands/ComponentRemoveCommand.php create mode 100644 app/Commands/ComponentUpdateCommand.php create mode 100644 app/Commands/QualityCommand.php create mode 100644 config/conduit.php create mode 100644 config/github.php create mode 100644 phpstan.neon diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e00c65c..66f2fd2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(php:*)", "Bash(conduit knowledge:search:*)", "Bash(conduit:*)", - "Bash(composer:*)" + "Bash(composer:*)", + "Bash(./vendor/bin/phpstan analyse:*)" ], "deny": [] } diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..e295b49 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,191 @@ +name: ๐Ÿ’ฉ Quality Checks + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + workflow_dispatch: + +jobs: + quality: + name: ๐Ÿ’ฉ THE SHIT Quality + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: ['8.2', '8.3'] + + steps: + - name: ๐Ÿ“ฆ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ˜ Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, gd, json, mbstring, pdo + tools: composer:v2 + coverage: none + + - name: ๐Ÿ“‹ Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: ๐Ÿ’พ Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php-version }}-composer- + + - name: ๐Ÿ“š Install dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: ๐Ÿ’ฉ Run Quality Checks + run: | + echo "Running THE SHIT Quality Checks on PHP ${{ matrix.php-version }}" + php ๐Ÿ’ฉ quality --no-interaction + + - name: ๐Ÿ“ Upload Quality Report (if failed) + if: failure() + uses: actions/upload-artifact@v3 + with: + name: quality-report-php-${{ matrix.php-version }} + path: | + storage/logs/ + .phpunit.result.cache + phpstan.neon + + code-style: + name: ๐Ÿ“ Code Style (Pint) + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฆ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ˜ Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite + tools: composer:v2 + coverage: none + + - name: ๐Ÿ“š Install dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: ๐ŸŽจ Check code style + run: ./vendor/bin/pint --test + + - name: ๐Ÿ’ก Suggest fixes (if failed) + if: failure() + run: | + echo "Code style issues found! Run 'php ๐Ÿ’ฉ quality --fix' locally to auto-fix." + ./vendor/bin/pint --test --format=json > pint-report.json || true + + - name: ๐Ÿ“ค Upload Pint report + if: failure() + uses: actions/upload-artifact@v3 + with: + name: pint-report + path: pint-report.json + + static-analysis: + name: ๐Ÿ” Static Analysis (Larastan) + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฆ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ˜ Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite + tools: composer:v2 + coverage: none + + - name: ๐Ÿ“š Install dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: ๐Ÿ”ฌ Run static analysis + run: ./vendor/bin/phpstan analyse --memory-limit=512M --error-format=github + + tests: + name: ๐Ÿงช Tests (Pest) + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: ['8.2', '8.3'] + + steps: + - name: ๐Ÿ“ฆ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ˜ Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, gd, json, mbstring, pdo + tools: composer:v2 + coverage: none + + - name: ๐Ÿ“š Install dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: ๐Ÿงช Run tests + run: ./vendor/bin/pest --colors=always + + - name: ๐Ÿ“Š Upload test results + if: failure() + uses: actions/upload-artifact@v3 + with: + name: test-results-php-${{ matrix.php-version }} + path: tests/_output/ + + quality-summary: + name: ๐Ÿ“Š Quality Summary + runs-on: ubuntu-latest + needs: [quality, code-style, static-analysis, tests] + if: always() + + steps: + - name: ๐Ÿ’ฉ THE SHIT Quality Status + run: | + echo "========================================" + echo " ๐Ÿ’ฉ THE SHIT Quality Check Summary " + echo "========================================" + + if [ "${{ needs.quality.result }}" == "success" ]; then + echo "โœ… Overall Quality: PASSED" + else + echo "โŒ Overall Quality: FAILED" + fi + + echo "" + echo "Individual Checks:" + echo " ๐Ÿ“ Code Style: ${{ needs.code-style.result }}" + echo " ๐Ÿ” Static Analysis: ${{ needs.static-analysis.result }}" + echo " ๐Ÿงช Tests: ${{ needs.tests.result }}" + echo "" + + if [ "${{ needs.quality.result }}" == "success" ]; then + echo "๐ŸŽ‰ Your code is THE SHIT (in a good way)!" + else + echo "๐Ÿ’ก Run 'php ๐Ÿ’ฉ quality --fix' locally to fix issues" + fi + + echo "========================================" + + - name: ๐Ÿšซ Fail if quality checks failed + if: | + needs.quality.result == 'failure' || + needs.code-style.result == 'failure' || + needs.static-analysis.result == 'failure' || + needs.tests.result == 'failure' + run: exit 1 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..990ead7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,242 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +THE SHIT (Scaling Humans Into Tomorrow) is a Laravel Zero CLI framework built for Human-AI collaboration. The project uses ๐Ÿ’ฉ emoji as its executable and follows component-based architecture with GitHub-distributed extensions. + +## Key Commands + +### Testing +```bash +# Run all tests +./vendor/bin/pest + +# Run specific test file +./vendor/bin/pest tests/Feature/ExampleTest.php + +# Run with coverage +./vendor/bin/pest --coverage +``` + +### Code Quality +```bash +# Format code with Laravel Pint +./vendor/bin/pint + +# Check formatting without fixing +./vendor/bin/pint --test +``` + +### Development +```bash +# Install dependencies +composer install + +# Update dependencies +composer update + +# Clear Laravel Zero cache +php ๐Ÿ’ฉ cache:clear + +# List all commands +php ๐Ÿ’ฉ list +``` + +## Architectural Principles + +### Expert-Level Development Standards +- **NO WORKAROUNDS**: Fix issues at their root cause, not with temporary patches +- **NO TECHNICAL DEBT**: Every implementation should be production-ready +- **PROPER AUTHENTICATION**: Set up authentication systems correctly, not bypass them +- **DESIGN PATTERNS**: Use appropriate design patterns for maintainability +- **DEPENDENCY INJECTION**: Prefer DI over facades for testability +- **ERROR HANDLING**: Implement comprehensive error handling, not quick fixes +- **CONFIGURATION**: Externalize configuration properly (env vars, config files) +- **USE LARAVEL NATIVE**: Always use Laravel/Laravel Zero native features over Symfony directly + - Use `Illuminate\Support\Facades\Process` NOT `Symfony\Component\Process` + - Use Laravel Prompts (`warning()`, `info()`, `error()`) for CLI output + - Use Laravel's Http facade for API calls + - Use Laravel's File facade for filesystem operations + +When encountering issues: +1. Identify the root cause +2. Design a proper solution +3. Implement it correctly the first time +4. Add tests to prevent regression +5. Document the solution + +## Architecture + +### Command Structure +All commands extend `App\Commands\ConduitCommand` which provides Human-AI collaboration patterns: + +- **Smart Input Methods**: `smartText()`, `smartConfirm()`, `smartChoice()` - automatically adapt to interactive vs non-interactive modes +- **User Agent Detection**: Detects human, claude, ai, or ci agents via `CONDUIT_USER_AGENT` environment variable +- **Response Formatting**: `jsonResponse()` for AI agents, `smartInfo()` for humans + +### Component System +Components are GitHub-based packages installed to `๐Ÿ’ฉ-components/`: + +- **Installation**: `php ๐Ÿ’ฉ component:install ` fetches from `S-H-I-T` GitHub organization +- **Scaffolding**: `php ๐Ÿ’ฉ component:scaffold ` creates new component structure +- **Configuration**: `php ๐Ÿ’ฉ component:config ` manages component settings +- **Manifest**: Each component has a `๐Ÿ’ฉ.json` file defining metadata and dependencies + +### Directory Structure +``` +app/Commands/ # Core CLI commands extending ConduitCommand +app/Providers/ # Service providers (AppServiceProvider, ComponentServiceProvider) +app/ValueObjects/ # Domain objects (Component, ComponentManifest) +๐Ÿ’ฉ-components/ # Installed components directory +config/ # Laravel Zero configuration +tests/ # Pest test suites +``` + +## Human-AI Collaboration Patterns + +When developing commands, always implement both human and AI modes: + +```php +// Detect mode +if ($this->isNonInteractiveMode()) { + // AI mode: return structured data + return $this->jsonResponse(['data' => $result]); +} + +// Human mode: display formatted output +$this->smartInfo('Operation completed'); +$this->table(['Column'], $data); +``` + +## Component Development + +### Creating a New Component +1. Use scaffold command: `php ๐Ÿ’ฉ component:scaffold my-component` +2. Edit generated `๐Ÿ’ฉ.json` manifest with proper metadata +3. Implement commands in `app/Commands/` directory +4. Register in component's service provider + +### Component Manifest Structure +```json +{ + "name": "component-name", + "description": "Component description", + "version": "1.0.0", + "shit_acronym": "Specific Helpful Implementation Tool", + "requires": { + "php": "^8.2" + } +} +``` + +## Testing Standards + +- Use Pest for all tests +- Place feature tests in `tests/Feature/` +- Place unit tests in `tests/Unit/` +- Test both human and AI modes for commands +- Mock external dependencies with Mockery + +## Code Quality Standards + +### MANDATORY: Run Before EVERY Commit +**NEVER skip these checks. NO EXCEPTIONS.** + +โš ๏ธ **Current Status**: Larastan has some warnings that need fixing (env() usage, json options). +These should be addressed but don't block functionality. + +```bash +# Option 1: Use the built-in quality command +php ๐Ÿ’ฉ quality --fix # Auto-fixes code style and runs all checks + +# Option 2: Use composer scripts +composer quality # Runs pint (fix), phpstan, and tests +composer check # Runs pint (test only), phpstan, and tests + +# Option 3: Run individually +composer pint # Fix code style +composer pint:test # Check code style without fixing +composer stan # Run static analysis +composer test # Run tests +``` + +If ANY of these fail, FIX IT before committing. No "I'll fix it later" - fix it NOW. + +### Available Quality Scripts +- `composer quality` - Runs all checks with auto-fix +- `composer check` - Runs all checks without auto-fix +- `php ๐Ÿ’ฉ quality` - Interactive quality command with Laravel Zero task interface + - Shows progress with loading indicators and checkmarks โœ“ + - Works in any PHP project directory (not just THE SHIT) + - `--fix` - Auto-fix code style issues with Pint + - `--no-tests` - Skip tests for quick checks + - `--path=/path/to/project` - Check a different project + - Gracefully handles missing tools (won't fail if Pint/Stan/Pest not installed) + +### Laravel Pint Configuration +- **Standard**: PSR-12 with Laravel preset +- **Auto-fix**: Always run Pint to fix style issues +- **Check only**: Use `./vendor/bin/pint --test` to verify without changing +- **Key Rules**: + - No unused imports + - Proper spacing around operators + - Consistent brace positioning + - Single quotes for simple strings + - Trailing commas in multiline arrays + +### Larastan (PHPStan) Configuration +- **Level**: 5 (balanced strictness) +- **Config**: `phpstan.neon` in project root +- **Key Checks**: + - Undefined methods and properties + - Type mismatches + - Dead code detection + - Proper use of Laravel features (no env() outside config) + - Console command option validation +- **Fix ALL errors**: Don't ignore or suppress unless absolutely necessary + +### Testing Standards +- **Framework**: Pest (NOT PHPUnit directly) +- **Structure**: + - Feature tests in `tests/Feature/` + - Unit tests in `tests/Unit/` +- **Coverage**: Aim for >80% on new code +- **Test both modes**: Human interactive AND AI/non-interactive + +### Code Style Guidelines + +- Follow PSR-12 standards via Laravel Pint +- Use emoji thoughtfully in user-facing output +- Maintain SHIT acronym creativity for components +- Keep commands focused and single-purpose +- Always provide JSON responses in non-interactive mode +- Use Laravel native features over Symfony +- Prefer Laravel Prompts for CLI interactions + +## Important Conventions + +1. **Emoji Usage**: The ๐Ÿ’ฉ emoji is the brand identity - use it consistently in filenames and output +2. **Error Handling**: Commands should fail gracefully with helpful messages in both human and AI modes +3. **Documentation**: Update `docs/knowledge/` for architectural decisions +4. **Component Isolation**: Each component has its own vendor directory - never share dependencies +5. **Version Constraints**: Support semantic versioning in component requirements + +## Commit Standards + +- Use clear, concise commit messages with bullet points for multiple changes +- NO Claude Code attribution in commits (no "๐Ÿค– Generated with Claude Code" or co-authoring) +- Minimal use of emoji - only ๐Ÿš€ for major features if absolutely necessary +- Format: `: ` (e.g., `fix: resolve OAuth token refresh issue`) +- For multiple changes, use bullet points in the body: + ``` + feat: add releases endpoint to GitHub client + + โ€ข Add ReleasesResource class + โ€ข Implement Index, Get, and Latest requests + โ€ข Create ReleaseData DTOs + โ€ข Wire up releases() method in connector + ``` +- Keep commits atomic and focused on a single concern +- Never include sensitive information or API keys \ No newline at end of file diff --git a/app/Commands/ComponentConfigCommand.php b/app/Commands/ComponentConfigCommand.php index c1420d4..1e9608b 100644 --- a/app/Commands/ComponentConfigCommand.php +++ b/app/Commands/ComponentConfigCommand.php @@ -60,6 +60,7 @@ protected function executeCommand(): int // Smart confirm if (! $this->smartConfirm('Save these settings?', default: true)) { $this->smartInfo('Configuration cancelled.'); + return self::SUCCESS; } @@ -78,6 +79,7 @@ protected function executeCommand(): int } else { $this->smartOutput($config, 'โœ… Component configuration saved!'); $this->smartLine(' Use "component:config" anytime to update these settings.'); + return self::SUCCESS; } } @@ -87,6 +89,7 @@ protected function executeNonInteractive(): int // Check if already configured if ($this->hasValidConfig()) { $config = $this->loadConfig(); + return $this->jsonResponse($config); } @@ -98,20 +101,23 @@ protected function executeNonInteractive(): int ]; $this->saveConfig($config); + return $this->jsonResponse($config); } private function hasValidConfig(): bool { $config = $this->loadConfig(); - return !empty($config['github_username']) && - !empty($config['php_namespace']) && - !empty($config['author_email']); + + return ! empty($config['github_username']) && + ! empty($config['php_namespace']) && + ! empty($config['author_email']); } private function loadConfig(): array { $configPath = $this->getConfigPath(); + return file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; } @@ -119,12 +125,12 @@ private function detectGithubUsername(): string { // Try current config first $config = $this->loadConfig(); - if (!empty($config['github_username'])) { + if (! empty($config['github_username'])) { return $config['github_username']; } - // Try environment variable - if ($username = env('CONDUIT_GITHUB_USERNAME')) { + // Try config (which checks environment) + if ($username = config('conduit.components.github_username')) { return $username; } @@ -141,12 +147,12 @@ private function detectPhpNamespace(string $githubUsername): string { // Try current config first $config = $this->loadConfig(); - if (!empty($config['php_namespace'])) { + if (! empty($config['php_namespace'])) { return $config['php_namespace']; } - // Try environment variable - if ($namespace = env('CONDUIT_PHP_NAMESPACE')) { + // Try config (which checks environment) + if ($namespace = config('conduit.components.namespace')) { return $namespace; } @@ -158,12 +164,12 @@ private function detectAuthorEmail(): string { // Try current config first $config = $this->loadConfig(); - if (!empty($config['author_email'])) { + if (! empty($config['author_email'])) { return $config['author_email']; } - // Try environment variable - if ($email = env('CONDUIT_AUTHOR_EMAIL')) { + // Try config (which checks environment) + if ($email = config('conduit.components.author_email')) { return $email; } @@ -181,6 +187,7 @@ private function getGitConfig(string $key): ?string // Use Process class for safer git config execution $process = new \Symfony\Component\Process\Process(['git', 'config', '--global', $key]); $process->run(); + return $process->isSuccessful() ? trim($process->getOutput()) : null; } @@ -188,16 +195,16 @@ private function saveConfig(array $settings): void { $configPath = $this->getConfigPath(); $dir = dirname($configPath); - - if (!is_dir($dir)) { + + if (! is_dir($dir)) { mkdir($dir, 0755, true); } - + file_put_contents($configPath, json_encode($settings, JSON_PRETTY_PRINT)); } private function getConfigPath(): string { - return $_SERVER['HOME'] . '/.config/conduit/component-config.json'; + return $_SERVER['HOME'].'/.config/conduit/component-config.json'; } } diff --git a/app/Commands/ComponentInstallCommand.php b/app/Commands/ComponentInstallCommand.php index abcf1c3..7a10957 100644 --- a/app/Commands/ComponentInstallCommand.php +++ b/app/Commands/ComponentInstallCommand.php @@ -2,11 +2,15 @@ namespace App\Commands; -use Symfony\Component\Process\Process; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Process; + +use function Laravel\Prompts\warning; class ComponentInstallCommand extends ConduitCommand { - protected $signature = 'install {component : Component name (e.g., spotify, github)} {version? : Version constraint (e.g., ^1.0, ~2.1, 1.2.3)}'; + protected $signature = 'install {component : Component name (e.g., spotify, github)} {version? : Version constraint (e.g., ^1.0, ~2.1, 1.2.3)} {--json : Output as JSON}'; + protected $description = 'Install a component from THE SHIT ecosystem'; protected function executeCommand(): int @@ -14,69 +18,82 @@ protected function executeCommand(): int $component = $this->argument('component'); $version = $this->argument('version') ?? '*'; $componentsDir = base_path('๐Ÿ’ฉ-components'); - $componentPath = $componentsDir . '/' . $component; + $componentPath = $componentsDir.'/'.$component; // Check if already installed if (is_dir($componentPath)) { $this->smartInfo("โœ… Component '{$component}' is already installed"); + return self::SUCCESS; } $this->smartInfo("๐Ÿ’ฉ Installing {$component}..."); // Create components directory if needed - if (!is_dir($componentsDir)) { + if (! is_dir($componentsDir)) { mkdir($componentsDir, 0755, true); } // Determine the GitHub repo $repo = "S-H-I-T/{$component}"; - + try { // Get releases from GitHub $releases = $this->getGitHubReleases($repo); - + if (empty($releases)) { // No releases, try to clone from main branch - $this->smartInfo("๐Ÿ“ฆ No releases found, installing from main branch..."); + $this->smartInfo('๐Ÿ“ฆ No releases found, installing from main branch...'); + return $this->cloneFromGitHub($repo, $componentPath, 'main'); } // Find matching version $release = $this->findMatchingRelease($releases, $version); - - if (!$release) { + + if (! $release) { $this->forceOutput("โŒ No release matching version constraint '{$version}'", 'error'); + return self::FAILURE; } $this->smartInfo("๐Ÿ“ฆ Installing {$component} v{$release['tag_name']}..."); - + // Clone specific tag return $this->cloneFromGitHub($repo, $componentPath, $release['tag_name']); - + } catch (\Exception $e) { - $this->forceOutput("โŒ Failed to install: " . $e->getMessage(), 'error'); + $this->forceOutput('โŒ Failed to install: '.$e->getMessage(), 'error'); + return self::FAILURE; } } private function getGitHubReleases(string $repo): array { - $response = Http::withHeaders([ - 'Accept' => 'application/vnd.github.v3+json', - 'User-Agent' => 'THE-SHIT-CLI' - ])->get("https://api.github.com/repos/{$repo}/releases"); - - if ($response->failed()) { - if ($response->status() === 404) { - // Repo might not exist or have releases + try { + // Use HTTP directly for unauthenticated requests + $response = Http::withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'THE-SHIT-CLI', + ])->get("https://api.github.com/repos/{$repo}/releases"); + + if ($response->failed()) { + if ($response->status() === 404) { + // Repo might not exist or have releases + return []; + } + throw new \Exception('GitHub API error: '.$response->body()); + } + + return $response->json(); + } catch (\Exception $e) { + // Check if it's a 404 (repo doesn't exist or no releases) + if (str_contains($e->getMessage(), '404')) { return []; } - throw new \Exception("GitHub API error: " . $response->body()); + throw new \Exception('GitHub API error: '.$e->getMessage()); } - - return $response->json(); } private function findMatchingRelease(array $releases, string $constraint): ?array @@ -89,17 +106,17 @@ private function findMatchingRelease(array $releases, string $constraint): ?arra // TODO: Implement full semver constraint matching foreach ($releases as $release) { $tag = ltrim($release['tag_name'], 'v'); - + // Exact match if ($tag === ltrim($constraint, 'v^~')) { return $release; } - + // Simple caret (^) matching - same major version if (str_starts_with($constraint, '^')) { $constraintVersion = ltrim($constraint, '^'); $major = explode('.', $constraintVersion)[0]; - if (str_starts_with($tag, $major . '.')) { + if (str_starts_with($tag, $major.'.')) { return $release; } } @@ -116,15 +133,15 @@ private function cloneFromGitHub(string $repo, string $path, string $ref): int '--depth', '1', '--branch', $ref, "https://github.com/{$repo}.git", - $path + $path, ]; - $process = new Process($cloneCommand); - $process->setTimeout(300); - $process->run(); + $result = Process::timeout(300) + ->run(implode(' ', array_map('escapeshellarg', $cloneCommand))); + + if (! $result->successful()) { + $this->forceOutput('โŒ Failed to clone repository: '.$result->errorOutput(), 'error'); - if (!$process->isSuccessful()) { - $this->forceOutput("โŒ Failed to clone repository: " . $process->getErrorOutput(), 'error'); return self::FAILURE; } @@ -132,35 +149,34 @@ private function cloneFromGitHub(string $repo, string $path, string $ref): int $this->exec("rm -rf {$path}/.git"); // Install dependencies if composer.json exists - if (file_exists($path . '/composer.json')) { - $this->smartInfo("๐Ÿ“š Installing dependencies..."); - $installProcess = new Process(['composer', 'install', '--no-dev'], $path); - $installProcess->setTimeout(300); - $installProcess->run(); - - if (!$installProcess->isSuccessful()) { - $this->smartWarn("โš ๏ธ Warning: Failed to install dependencies"); + if (file_exists($path.'/composer.json')) { + $this->smartInfo('๐Ÿ“š Installing dependencies...'); + $result = Process::path($path) + ->timeout(300) + ->run('composer install --no-dev'); + + if (! $result->successful()) { + warning('Failed to install dependencies'); } } // Make executable if exists - $manifest = json_decode(file_get_contents($path . '/๐Ÿ’ฉ.json'), true); + $manifest = json_decode(file_get_contents($path.'/๐Ÿ’ฉ.json'), true); if (isset($manifest['executable'])) { - $executable = $path . '/bin/' . $manifest['executable']; + $executable = $path.'/bin/'.$manifest['executable']; if (file_exists($executable)) { chmod($executable, 0755); } } $this->smartInfo("โœ… Successfully installed {$repo}@{$ref}"); - $this->smartInfo("๐Ÿš€ Component ready to use!"); - + $this->smartInfo('๐Ÿš€ Component ready to use!'); + return self::SUCCESS; } private function exec(string $command): void { - $process = Process::fromShellCommandline($command); - $process->run(); + Process::run($command); } -} \ No newline at end of file +} diff --git a/app/Commands/ComponentListCommand.php b/app/Commands/ComponentListCommand.php new file mode 100644 index 0000000..7f9f845 --- /dev/null +++ b/app/Commands/ComponentListCommand.php @@ -0,0 +1,135 @@ +warn('No components installed yet.'); + $this->smartInfo('Use `php ๐Ÿ’ฉ install ` to install your first component.'); + + return self::SUCCESS; + } + + $components = $this->scanForComponents($componentsDir); + + if (empty($components)) { + $this->warn('No components found.'); + + return self::SUCCESS; + } + + // JSON output for AI/automation + if ($this->option('json') || $this->isNonInteractiveMode()) { + return $this->jsonResponse(['components' => $components]); + } + + // Human-readable output + $this->displayComponentsTable($components); + + return self::SUCCESS; + } + + private function scanForComponents(string $dir): array + { + $components = []; + $directories = File::directories($dir); + + foreach ($directories as $componentDir) { + $manifestPath = $componentDir.'/๐Ÿ’ฉ.json'; + + if (! file_exists($manifestPath)) { + continue; + } + + try { + $manifest = json_decode(file_get_contents($manifestPath), true); + $componentName = basename($componentDir); + + $components[] = [ + 'name' => $componentName, + 'version' => $manifest['version'] ?? 'unknown', + 'description' => $manifest['description'] ?? 'No description', + 'shit_acronym' => $manifest['shit_acronym'] ?? null, + 'path' => $componentDir, + 'commands' => $this->getComponentCommands($componentDir), + ]; + } catch (\Exception $e) { + // Skip invalid components + continue; + } + } + + return $components; + } + + private function getComponentCommands(string $componentDir): array + { + $commands = []; + $commandsDir = $componentDir.'/app/Commands'; + + if (! is_dir($commandsDir)) { + return $commands; + } + + $files = File::files($commandsDir); + + foreach ($files as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $className = $file->getFilenameWithoutExtension(); + + // Skip base classes and interfaces + if (in_array($className, ['BaseCommand', 'ConduitCommand', 'CommandInterface'])) { + continue; + } + + $commands[] = strtolower(str_replace('Command', '', $className)); + } + + return $commands; + } + + private function displayComponentsTable(array $components): void + { + $this->smartInfo("๐Ÿ’ฉ Installed Components\n"); + + $rows = []; + foreach ($components as $component) { + $acronym = $component['shit_acronym'] + ? "\n{$component['shit_acronym']}" + : ''; + + $commands = ! empty($component['commands']) + ? "\n".implode(', ', $component['commands']).'' + : "\nNo commands"; + + $rows[] = [ + $component['name'], + $component['version'], + $component['description'].$acronym, + count($component['commands']).' commands'.$commands, + ]; + } + + $this->table( + ['Component', 'Version', 'Description', 'Commands'], + $rows + ); + + $this->newLine(); + $this->smartInfo('๐Ÿ’ก Tip: Use `php ๐Ÿ’ฉ :` to run component commands'); + } +} diff --git a/app/Commands/ComponentRemoveCommand.php b/app/Commands/ComponentRemoveCommand.php new file mode 100644 index 0000000..c56f173 --- /dev/null +++ b/app/Commands/ComponentRemoveCommand.php @@ -0,0 +1,154 @@ +argument('component'); + $componentsDir = base_path('๐Ÿ’ฉ-components'); + $componentPath = $componentsDir.'/'.$component; + + // Check if component exists + if (! is_dir($componentPath)) { + $this->forceOutput("โŒ Component '{$component}' is not installed", 'error'); + + // Suggest similar components + $installed = $this->getInstalledComponents($componentsDir); + if (! empty($installed)) { + $this->smartInfo("\nInstalled components: ".implode(', ', $installed)); + } + + return self::FAILURE; + } + + // Get component info before removal + $manifest = $this->getComponentManifest($componentPath); + $componentInfo = $manifest ? "{$component} v{$manifest['version']}" : $component; + + // Confirm removal + if (! $this->option('force')) { + $confirmed = $this->smartConfirm( + "Are you sure you want to remove {$componentInfo}?", + false + ); + + if (! $confirmed) { + $this->smartInfo('Removal cancelled.'); + + return self::SUCCESS; + } + } + + $this->smartInfo("๐Ÿ—‘๏ธ Removing {$componentInfo}..."); + + try { + // Remove component directory + $this->removeDirectory($componentPath); + + // Update registry if it exists + $this->updateComponentRegistry($component, 'remove'); + + $this->smartInfo("โœ… Successfully removed {$componentInfo}"); + + // Check if components directory is empty + if ($this->isDirectoryEmpty($componentsDir)) { + File::deleteDirectory($componentsDir); + $this->smartInfo('๐Ÿ“ฆ No components remaining. Components directory removed.'); + } + + return self::SUCCESS; + + } catch (\Exception $e) { + $this->forceOutput('โŒ Failed to remove component: '.$e->getMessage(), 'error'); + + return self::FAILURE; + } + } + + private function getInstalledComponents(string $dir): array + { + if (! is_dir($dir)) { + return []; + } + + $components = []; + foreach (File::directories($dir) as $componentDir) { + $components[] = basename($componentDir); + } + + return $components; + } + + private function getComponentManifest(string $componentPath): ?array + { + $manifestPath = $componentPath.'/๐Ÿ’ฉ.json'; + + if (! file_exists($manifestPath)) { + return null; + } + + try { + return json_decode(file_get_contents($manifestPath), true); + } catch (\Exception $e) { + return null; + } + } + + private function removeDirectory(string $path): void + { + // Use File facade for consistent removal + if (! File::deleteDirectory($path)) { + // Fallback to system command if needed + $result = Process::run('rm -rf '.escapeshellarg($path)); + + if (! $result->successful()) { + throw new \Exception("Could not remove directory: {$path}"); + } + } + } + + private function isDirectoryEmpty(string $dir): bool + { + if (! is_dir($dir)) { + return true; + } + + $items = scandir($dir); + + return count($items) <= 2; // Only . and .. + } + + private function updateComponentRegistry(string $component, string $action): void + { + $registryPath = base_path('๐Ÿ’ฉ-components.json'); + + if (! file_exists($registryPath)) { + return; + } + + try { + $registry = json_decode(file_get_contents($registryPath), true) ?? []; + + if ($action === 'remove' && isset($registry['installed'][$component])) { + unset($registry['installed'][$component]); + $registry['removed'][] = [ + 'name' => $component, + 'removed_at' => now()->toIso8601String(), + ]; + } + + file_put_contents($registryPath, json_encode($registry, JSON_PRETTY_PRINT)); + } catch (\Exception $e) { + // Registry update is non-critical, continue + } + } +} diff --git a/app/Commands/ComponentScaffoldCommand.php b/app/Commands/ComponentScaffoldCommand.php index 43248f6..832cdc8 100644 --- a/app/Commands/ComponentScaffoldCommand.php +++ b/app/Commands/ComponentScaffoldCommand.php @@ -2,36 +2,39 @@ namespace App\Commands; -use Symfony\Component\Process\Process; +use Illuminate\Support\Facades\Process; class ComponentScaffoldCommand extends ConduitCommand { - protected $signature = 'component:scaffold {name : Component name (e.g., spotify, github, docker)}'; + protected $signature = 'component:scaffold {name : Component name (e.g., spotify, github, docker)} {--json : Output as JSON}'; + protected $description = 'Scaffold a new component using the conduit-component skeleton'; protected function executeCommand(): int { $name = $this->argument('name'); $componentsDir = base_path('๐Ÿ’ฉ-components'); - $componentPath = $componentsDir . '/' . $name; + $componentPath = $componentsDir.'/'.$name; $skeletonPath = '/Users/jordanpartridge/packages/conduit-component'; // Check if component already exists if (is_dir($componentPath)) { $this->forceOutput("โŒ Component '{$name}' already exists at: {$componentPath}", 'error'); + return self::FAILURE; } // Create components directory if it doesn't exist - if (!is_dir($componentsDir)) { + if (! is_dir($componentsDir)) { mkdir($componentsDir, 0755, true); } // Copy skeleton to component directory $this->smartInfo("๐Ÿ“ฆ Creating component '{$name}' from skeleton..."); - - if (!$this->copyDirectory($skeletonPath, $componentPath)) { - $this->forceOutput("โŒ Failed to copy skeleton directory", 'error'); + + if (! $this->copyDirectory($skeletonPath, $componentPath)) { + $this->forceOutput('โŒ Failed to copy skeleton directory', 'error'); + return self::FAILURE; } @@ -40,13 +43,13 @@ protected function executeCommand(): int $description = $this->smartText('Component description', '', "Conduit {$name} integration", true); $authorName = $this->smartText('Author name', '', 'Jordan Partridge'); $authorEmail = $this->smartText('Author email', '', 'jordan@partridge.rocks'); - + // Generate namespace - $namespace = 'ConduitComponents\\' . ucfirst($name); - + $namespace = 'ConduitComponents\\'.ucfirst($name); + // Replace placeholders in files - $this->smartInfo("๐Ÿ”ง Customizing component files..."); - + $this->smartInfo('๐Ÿ”ง Customizing component files...'); + $replacements = [ '{{VENDOR}}' => $vendor, '{{PACKAGE_NAME}}' => "conduit-{$name}", @@ -63,7 +66,7 @@ protected function executeCommand(): int $this->createManifest($componentPath, $name, $description); // Install component dependencies - $this->smartInfo("๐Ÿ“š Installing component dependencies..."); + $this->smartInfo('๐Ÿ“š Installing component dependencies...'); $this->installDependencies($componentPath); // Create bin directory and executable @@ -72,22 +75,22 @@ protected function executeCommand(): int $this->smartInfo("โœ… Component '{$name}' scaffolded successfully!"); $this->smartNewLine(); $this->smartInfo("๐Ÿ“ Location: {$componentPath}"); - $this->smartInfo("๐Ÿš€ Next steps:"); + $this->smartInfo('๐Ÿš€ Next steps:'); $this->smartLine(" 1. Add your business logic to src/{$name}Service.php"); - $this->smartLine(" 2. Define commands in src/Commands/"); + $this->smartLine(' 2. Define commands in src/Commands/'); $this->smartLine(" 3. Test with: php {$componentPath}/component "); - $this->smartLine(" 4. Conduit will automatically discover the component"); + $this->smartLine(' 4. Conduit will automatically discover the component'); return self::SUCCESS; } private function copyDirectory(string $source, string $destination): bool { - if (!is_dir($source)) { + if (! is_dir($source)) { return false; } - if (!is_dir($destination)) { + if (! is_dir($destination)) { mkdir($destination, 0755, true); } @@ -97,10 +100,10 @@ private function copyDirectory(string $source, string $destination): bool ); foreach ($iterator as $item) { - $target = $destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); + $target = $destination.DIRECTORY_SEPARATOR.$iterator->getSubPathName(); if ($item->isDir()) { - if (!is_dir($target)) { + if (! is_dir($target)) { mkdir($target, 0755, true); } } else { @@ -121,7 +124,7 @@ private function replaceInDirectory(string $directory, array $replacements): voi if ($file->isFile() && $this->shouldProcessFile($file->getPathname())) { $content = file_get_contents($file->getPathname()); $newContent = str_replace(array_keys($replacements), array_values($replacements), $content); - + if ($content !== $newContent) { file_put_contents($file->getPathname(), $newContent); } @@ -133,10 +136,10 @@ private function shouldProcessFile(string $filename): bool { $extension = pathinfo($filename, PATHINFO_EXTENSION); $excludeDirs = ['vendor', '.git', 'node_modules']; - + // Skip binary files and vendor directories foreach ($excludeDirs as $dir) { - if (str_contains($filename, DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR)) { + if (str_contains($filename, DIRECTORY_SEPARATOR.$dir.DIRECTORY_SEPARATOR)) { return false; } } @@ -153,46 +156,45 @@ private function createManifest(string $componentPath, string $name, string $des 'executable' => $name, 'commands' => [ "{$name}:example" => "Example {$name} command", - "{$name}:test" => "Test {$name} functionality" - ] + "{$name}:test" => "Test {$name} functionality", + ], ]; file_put_contents( - $componentPath . '/๐Ÿ’ฉ.json', + $componentPath.'/๐Ÿ’ฉ.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ); } private function installDependencies(string $componentPath): void { - $process = new Process(['composer', 'install', '--no-dev'], $componentPath); - $process->setTimeout(300); - try { - $process->run(); + Process::path($componentPath) + ->timeout(300) + ->run('composer install --no-dev'); } catch (\Exception $e) { - $this->forceOutput("โš ๏ธ Warning: Failed to install dependencies: " . $e->getMessage(), 'warn'); + $this->forceOutput('โš ๏ธ Warning: Failed to install dependencies: '.$e->getMessage(), 'warn'); } } private function createExecutable(string $componentPath, string $name, string $namespace): void { - $binDir = $componentPath . '/bin'; - if (!is_dir($binDir)) { + $binDir = $componentPath.'/bin'; + if (! is_dir($binDir)) { mkdir($binDir, 0755, true); } - $executablePath = $binDir . '/' . $name; + $executablePath = $binDir.'/'.$name; $executableContent = $this->generateExecutable($name, $namespace); - + file_put_contents($executablePath, $executableContent); chmod($executablePath, 0755); } private function generateExecutable(string $name, string $namespace): string { - $serviceClass = str_replace('\\', '\\\\', $namespace . '\\' . ucfirst($name) . 'Service'); - + $serviceClass = str_replace('\\', '\\\\', $namespace.'\\'.ucfirst($name).'Service'); + return <<argument('component'); + $updateAll = $this->option('all'); + + if (! $component && ! $updateAll) { + $this->error('Please specify a component or use --all to update all components'); + + return self::FAILURE; + } + + $componentsDir = base_path('๐Ÿ’ฉ-components'); + + if (! is_dir($componentsDir)) { + $this->warn('No components installed.'); + + return self::SUCCESS; + } + + if ($updateAll) { + return $this->updateAllComponents($componentsDir); + } + + return $this->updateSingleComponent($component, $componentsDir); + } + + private function updateAllComponents(string $componentsDir): int + { + $components = $this->getInstalledComponents($componentsDir); + + if (empty($components)) { + $this->warn('No components found to update.'); + + return self::SUCCESS; + } + + $this->smartInfo("๐Ÿ’ฉ Updating all components...\n"); + + $updated = 0; + $failed = 0; + + foreach ($components as $component) { + $result = $this->updateComponent($component['name'], $component['path']); + + if ($result) { + $updated++; + } else { + $failed++; + } + } + + $this->newLine(); + $this->smartInfo("โœ… Updated: {$updated} components"); + + if ($failed > 0) { + $this->warn("โŒ Failed: {$failed} components"); + + return self::FAILURE; + } + + return self::SUCCESS; + } + + private function updateSingleComponent(string $component, string $componentsDir): int + { + $componentPath = $componentsDir.'/'.$component; + + if (! is_dir($componentPath)) { + $this->error("Component '{$component}' is not installed"); + + return self::FAILURE; + } + + $this->smartInfo("๐Ÿ’ฉ Updating {$component}..."); + + if ($this->updateComponent($component, $componentPath)) { + $this->smartInfo("โœ… Successfully updated {$component}"); + + return self::SUCCESS; + } + + $this->error("โŒ Failed to update {$component}"); + + return self::FAILURE; + } + + private function updateComponent(string $name, string $path): bool + { + try { + // Get current version + $currentManifest = $this->getComponentManifest($path); + $currentVersion = $currentManifest['version'] ?? 'unknown'; + + // Check for latest version on GitHub + $repo = "S-H-I-T/{$name}"; + $latestVersion = $this->getLatestVersion($repo); + + if (! $latestVersion) { + $this->warn("Could not fetch latest version for {$name}"); + + return false; + } + + // Compare versions + if (version_compare($currentVersion, $latestVersion, '>=')) { + $this->info("{$name} is already up to date (v{$currentVersion})"); + + return true; + } + + $this->info("Updating {$name} from v{$currentVersion} to v{$latestVersion}"); + + // Backup current installation + $backupPath = $path.'.backup'; + $this->backupComponent($path, $backupPath); + + try { + // Remove old version + File::deleteDirectory($path); + + // Clone new version + $this->cloneComponent($repo, $path, $latestVersion); + + // Install dependencies + $this->installDependencies($path); + + // Remove backup + File::deleteDirectory($backupPath); + + return true; + + } catch (\Exception $e) { + // Restore from backup on failure + $this->warn('Update failed, restoring backup...'); + File::deleteDirectory($path); + File::moveDirectory($backupPath, $path); + throw $e; + } + + } catch (\Exception $e) { + $this->error("Error updating {$name}: ".$e->getMessage()); + + return false; + } + } + + private function getLatestVersion(string $repo): ?string + { + try { + $response = Http::withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'THE-SHIT-CLI', + ])->get("https://api.github.com/repos/{$repo}/releases/latest"); + + if ($response->successful()) { + $release = $response->json(); + + return ltrim($release['tag_name'] ?? '', 'v'); + } + + // If no releases, get default branch latest commit + $response = Http::withHeaders([ + 'Accept' => 'application/vnd.github.v3+json', + 'User-Agent' => 'THE-SHIT-CLI', + ])->get("https://api.github.com/repos/{$repo}"); + + if ($response->successful()) { + // Use main/master branch as version "dev-main" + return 'dev-main'; + } + + return null; + } catch (\Exception $e) { + return null; + } + } + + private function cloneComponent(string $repo, string $path, string $version): void + { + $cloneCommand = [ + 'git', 'clone', + '--depth', '1', + ]; + + // Add branch/tag if not dev version + if (! str_starts_with($version, 'dev-')) { + $cloneCommand[] = '--branch'; + $cloneCommand[] = "v{$version}"; + } + + $cloneCommand[] = "https://github.com/{$repo}.git"; + $cloneCommand[] = $path; + + $result = Process::timeout(300) + ->run(implode(' ', array_map('escapeshellarg', $cloneCommand))); + + if (! $result->successful()) { + throw new \Exception('Failed to clone repository: '.$result->errorOutput()); + } + + // Remove .git directory + File::deleteDirectory($path.'/.git'); + } + + private function installDependencies(string $path): void + { + if (! file_exists($path.'/composer.json')) { + return; + } + + Process::path($path) + ->timeout(300) + ->run('composer install --no-dev'); + + // Non-critical if dependencies fail + } + + private function backupComponent(string $source, string $destination): void + { + if (! File::copyDirectory($source, $destination)) { + throw new \Exception('Failed to backup component'); + } + } + + private function getInstalledComponents(string $dir): array + { + $components = []; + + foreach (File::directories($dir) as $componentDir) { + $manifestPath = $componentDir.'/๐Ÿ’ฉ.json'; + + if (file_exists($manifestPath)) { + $components[] = [ + 'name' => basename($componentDir), + 'path' => $componentDir, + ]; + } + } + + return $components; + } + + private function getComponentManifest(string $path): ?array + { + $manifestPath = $path.'/๐Ÿ’ฉ.json'; + + if (! file_exists($manifestPath)) { + return null; + } + + try { + return json_decode(file_get_contents($manifestPath), true); + } catch (\Exception $e) { + return null; + } + } +} diff --git a/app/Commands/ConduitCommand.php b/app/Commands/ConduitCommand.php index c2daa15..67a3e83 100644 --- a/app/Commands/ConduitCommand.php +++ b/app/Commands/ConduitCommand.php @@ -10,9 +10,9 @@ /** * Base command for Human-AI collaboration pattern - * + * * This architecture enables seamless interaction between: - * - Jordan (Human) โ†’ Beautiful interactive prompts + * - Jordan (Human) โ†’ Beautiful interactive prompts * - Claude (AI) โ†’ JSON output and non-interactive defaults * - CI/CD โ†’ Automated execution with environment variables */ @@ -39,6 +39,7 @@ final public function handle(): int } catch (NonInteractiveValidationException $e) { // Graceful fallback to non-interactive if prompts fail $this->warn('Falling back to non-interactive mode due to prompt failure'); + return $this->executeNonInteractive(); } } @@ -58,7 +59,7 @@ protected function executeNonInteractive(): int */ protected function isNonInteractiveMode(): bool { - return !$this->input->isInteractive() || + return ! $this->input->isInteractive() || in_array('--no-interaction', $GLOBALS['argv'] ?? []) || in_array('-n', $GLOBALS['argv'] ?? []) || $this->getUserAgent() !== 'human'; @@ -69,9 +70,8 @@ protected function isNonInteractiveMode(): bool */ protected function getUserAgent(): string { - return $_SERVER['CONDUIT_USER_AGENT'] ?? - env('CONDUIT_USER_AGENT') ?? - 'human'; + return $_SERVER['CONDUIT_USER_AGENT'] ?? + config('conduit.user_agent', 'human'); } /** @@ -83,13 +83,14 @@ protected function smartText( mixed $default = '', bool $required = false, string $hint = '', - callable $validate = null + ?callable $validate = null ): string { // In non-interactive mode, return default immediately if ($this->isNonInteractiveMode()) { if (empty($default) && $required) { throw new \RuntimeException("Non-interactive mode requires a default value for: {$label}"); } + return (string) $default; } @@ -136,14 +137,15 @@ protected function smartConfirm( */ protected function smartOutput(array $data, string $humanMessage = ''): void { - if ($this->option('json') || $this->getUserAgent() !== 'human') { + $jsonMode = (bool) $this->option('json'); + if ($jsonMode || $this->getUserAgent() !== 'human') { $this->line(json_encode($data, JSON_PRETTY_PRINT)); } else { if ($humanMessage) { $this->info($humanMessage); } foreach ($data as $key => $value) { - $this->line(" " . ucwords(str_replace('_', ' ', $key)) . ": {$value}"); + $this->line(' '.ucwords(str_replace('_', ' ', $key)).": {$value}"); } } } @@ -153,7 +155,7 @@ protected function smartOutput(array $data, string $humanMessage = ''): void */ protected function smartInfo(string $message): void { - if (!$this->isNonInteractiveMode()) { + if (! $this->isNonInteractiveMode()) { $this->info($message); } } @@ -163,7 +165,7 @@ protected function smartInfo(string $message): void */ protected function smartLine(string $message, bool $force = false): void { - if (!$this->isNonInteractiveMode() || $force) { + if (! $this->isNonInteractiveMode() || $force) { $this->line($message); } } @@ -173,7 +175,7 @@ protected function smartLine(string $message, bool $force = false): void */ protected function smartNewLine(): void { - if (!$this->isNonInteractiveMode()) { + if (! $this->isNonInteractiveMode()) { $this->newLine(); } } @@ -183,7 +185,7 @@ protected function smartNewLine(): void */ protected function forceOutput(string $message, string $type = 'line'): void { - match($type) { + match ($type) { 'info' => $this->info($message), 'error' => $this->error($message), 'warn' => $this->warn($message), @@ -196,13 +198,14 @@ protected function forceOutput(string $message, string $type = 'line'): void */ protected function jsonResponse(array $data, int $status = self::SUCCESS): int { - if ($this->isNonInteractiveMode() || $this->option('json')) { + if ($this->isNonInteractiveMode() || (bool) $this->option('json')) { $response = [ 'status' => $status === self::SUCCESS ? 'success' : 'error', - 'data' => $data + 'data' => $data, ]; $this->line(json_encode($response, JSON_PRETTY_PRINT)); } + return $status; } } diff --git a/app/Commands/DetectionTestCommand.php b/app/Commands/DetectionTestCommand.php index e502699..870195a 100644 --- a/app/Commands/DetectionTestCommand.php +++ b/app/Commands/DetectionTestCommand.php @@ -20,8 +20,8 @@ protected function executeCommand(): int 'argv' => $GLOBALS['argv'] ?? [], 'environment_vars' => [ 'CONDUIT_USER_AGENT' => $_SERVER['CONDUIT_USER_AGENT'] ?? null, - 'env_CONDUIT_USER_AGENT' => env('CONDUIT_USER_AGENT') ?? null, - ] + 'config_CONDUIT_USER_AGENT' => config('conduit.user_agent'), + ], ]; if ($this->isNonInteractiveMode()) { @@ -29,16 +29,16 @@ protected function executeCommand(): int } else { $this->info('๐Ÿ” Smart Detection Debug Results:'); $this->newLine(); - + foreach ($detectionResults as $key => $value) { if ($key === 'argv' || $key === 'environment_vars') { - $this->line(" {$key}: " . json_encode($value)); + $this->line(" {$key}: ".json_encode($value)); } else { $status = is_bool($value) ? ($value ? 'โœ… TRUE' : 'โŒ FALSE') : $value; $this->line(" {$key}: {$status}"); } } - + $this->newLine(); $this->line('๐Ÿ’ก Detection Logic:'); $this->line(' Non-interactive if ANY of these are true:'); @@ -46,7 +46,7 @@ protected function executeCommand(): int $this->line(' โ€ข --no-interaction flag in argv'); $this->line(' โ€ข -n flag in argv'); $this->line(' โ€ข user_agent !== "human"'); - + return self::SUCCESS; } } diff --git a/app/Commands/LsCommand.php b/app/Commands/LsCommand.php index 90a7cf8..75351ec 100644 --- a/app/Commands/LsCommand.php +++ b/app/Commands/LsCommand.php @@ -2,13 +2,14 @@ namespace App\Commands; -use Illuminate\Support\Facades\File; use Carbon\Carbon; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Process; -use function Laravel\Prompts\table; -use function Laravel\Prompts\select; -use function Laravel\Prompts\search; use function Laravel\Prompts\confirm; +use function Laravel\Prompts\search; +use function Laravel\Prompts\select; +use function Laravel\Prompts\table; use function Laravel\Prompts\text; class LsCommand extends ConduitCommand @@ -23,16 +24,17 @@ protected function executeCommand(): int if ($this->option('guide')) { return $this->showSexyHelp(); } - + $path = $this->argument('path') ?? getcwd(); - - if (!is_dir($path)) { + + if (! is_dir($path)) { $this->forceOutput("๐Ÿ’ฉ Path doesn't exist: {$path}", 'error'); + return self::FAILURE; } $files = $this->scanDirectory($path); - + if ($this->option('recent')) { $files = collect($files)->sortByDesc('modified')->values()->all(); } elseif ($this->option('large')) { @@ -49,15 +51,15 @@ protected function executeCommand(): int return $this->displayInteractive($files, $path); } - + private function showSexyHelp(): int { $this->smartInfo("๐Ÿ’ฉ SHIT ls - The file lister that doesn't lie to you"); $this->smartNewLine(); - - $this->smartLine("Usage: ./๐Ÿ’ฉ ls [path] [options]"); + + $this->smartLine('Usage: ./๐Ÿ’ฉ ls [path] [options]'); $this->smartNewLine(); - + table( ['๐Ÿšฉ Flag', '๐Ÿ“ Description', '๐Ÿ’ก Example'], [ @@ -71,10 +73,10 @@ private function showSexyHelp(): int ['--guide', 'Show this sexy options guide', './๐Ÿ’ฉ ls --guide'], ] ); - + $this->smartNewLine(); - $this->smartLine("๐ŸŽญ Permission Emojis:"); - + $this->smartLine('๐ŸŽญ Permission Emojis:'); + table( ['๐ŸŽจ Emoji', '๐Ÿ“Š Octal', '๐Ÿ“ Description'], [ @@ -90,31 +92,33 @@ private function showSexyHelp(): int ['๐Ÿšซ', '000', 'No permissions'], ] ); - + $this->smartNewLine(); $this->smartLine("๐Ÿ’ฉ Finally, a file lister that doesn't pretend to be enterprise-grade."); - + return self::SUCCESS; } private function scanDirectory(string $path): array { $files = []; - + try { $items = scandir($path); - + foreach ($items as $item) { - if ($item === '.' || $item === '..') continue; - - $fullPath = $path . DIRECTORY_SEPARATOR . $item; + if ($item === '.' || $item === '..') { + continue; + } + + $fullPath = $path.DIRECTORY_SEPARATOR.$item; $stat = stat($fullPath); - + if ($stat === false) { // Skip files we can't stat continue; } - + $files[] = [ 'name' => $item, 'type' => is_dir($fullPath) ? 'directory' : 'file', @@ -126,9 +130,9 @@ private function scanDirectory(string $path): array ]; } } catch (\Exception $e) { - $this->forceOutput("๐Ÿ’ฉ Error reading directory: " . $e->getMessage(), 'error'); + $this->forceOutput('๐Ÿ’ฉ Error reading directory: '.$e->getMessage(), 'error'); } - + return $files; } @@ -136,16 +140,25 @@ private function getIcon(string $path): string { if (is_dir($path)) { // Special directories - if (basename($path) === '.git') return '๐Ÿ”ง'; - if (basename($path) === 'vendor') return '๐Ÿ“ฆ'; - if (basename($path) === 'node_modules') return '๐Ÿ“ฆ'; - if (basename($path) === 'tests') return '๐Ÿงช'; + if (basename($path) === '.git') { + return '๐Ÿ”ง'; + } + if (basename($path) === 'vendor') { + return '๐Ÿ“ฆ'; + } + if (basename($path) === 'node_modules') { + return '๐Ÿ“ฆ'; + } + if (basename($path) === 'tests') { + return '๐Ÿงช'; + } + return '๐Ÿ“'; } $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - - return match($extension) { + + return match ($extension) { 'php' => '๐Ÿ˜', 'js', 'ts' => '๐Ÿ’›', 'json' => '๐Ÿ“‹', @@ -169,26 +182,26 @@ private function getPermissions(string $path): string $perms = fileperms($path) & 0777; $permString = $this->formatPermissionString($perms); $isDir = is_dir($path); - + // Choose format based on flags if ($this->option('detailed-perms')) { - return $this->getPermissionEmoji($perms, $isDir) . ' ' . $permString; + return $this->getPermissionEmoji($perms, $isDir).' '.$permString; } elseif ($this->option('octal')) { - return $this->getPermissionEmoji($perms, $isDir) . ' ' . sprintf('%03o', $perms); + return $this->getPermissionEmoji($perms, $isDir).' '.sprintf('%03o', $perms); } - + // Default: emoji + description (different for dirs vs files) if ($isDir) { - return match($perms) { + return match ($perms) { 0755 => '๐Ÿ“ Dir Access', // rwxr-xr-x 0700 => '๐Ÿ  Private Dir', // rwx------ 0777 => '๐Ÿšจ World Write!', // rwxrwxrwx (dangerous!) 0555 => '๐Ÿ” Read Only', // r-xr-xr-x 0000 => '๐Ÿšซ No Access', // --------- - default => $this->getPermissionEmoji($perms, $isDir) . ' ' . sprintf('%03o', $perms) + default => $this->getPermissionEmoji($perms, $isDir).' '.sprintf('%03o', $perms) }; } else { - return match($perms) { + return match ($perms) { 0755 => '๐Ÿ”“ Executable', // rwxr-xr-x 0644 => '๐Ÿ“– Standard', // rw-r--r-- 0600 => '๐Ÿ”’ Private', // rw------- @@ -198,15 +211,15 @@ private function getPermissions(string $path): string 0555 => '๐Ÿ” Read/Run', // r-xr-xr-x 0444 => '๐Ÿ‘๏ธ Read Only', // r--r--r-- 0000 => '๐Ÿšซ No Access', // --------- - default => $this->getPermissionEmoji($perms, $isDir) . ' ' . sprintf('%03o', $perms) + default => $this->getPermissionEmoji($perms, $isDir).' '.sprintf('%03o', $perms) }; } } - + private function getPermissionEmoji(int $perms, bool $isDir = false): string { if ($isDir) { - return match($perms) { + return match ($perms) { 0755 => '๐Ÿ“', // Directory access 0700 => '๐Ÿ ', // Private directory 0777 => '๐Ÿšจ', // Dangerous! @@ -215,7 +228,7 @@ private function getPermissionEmoji(int $perms, bool $isDir = false): string default => '๐Ÿ“‚' // Generic folder for uncommon perms }; } else { - return match($perms) { + return match ($perms) { 0755 => '๐Ÿ”“', // Executable file 0644 => '๐Ÿ“–', // Standard file 0600 => '๐Ÿ”’', // Private file @@ -229,7 +242,7 @@ private function getPermissionEmoji(int $perms, bool $isDir = false): string }; } } - + private function formatPermissionString(int $perms): string { $info = ''; @@ -254,24 +267,24 @@ private function formatPermissionString(int $perms): string private function getGitStatus(string $path): ?string { - if (!is_dir('.git')) { - // Use Process class for safer execution - $process = new \Symfony\Component\Process\Process(['git', 'rev-parse', '--git-dir']); - $process->run(); - if (!$process->isSuccessful()) { + if (! is_dir('.git')) { + // Use Laravel Process for safer execution + $result = Process::run('git rev-parse --git-dir'); + if (! $result->successful()) { return null; } } - $relativePath = str_replace(getcwd() . '/', '', $path); - // Use Process class for safer git status execution - $process = new \Symfony\Component\Process\Process(['git', 'status', '--porcelain', $relativePath]); - $process->run(); - $status = $process->isSuccessful() ? trim($process->getOutput()) : ''; - - if (empty($status)) return 'โœ…'; - - return match(substr($status, 0, 2)) { + $relativePath = str_replace(getcwd().'/', '', $path); + // Use Laravel Process for safer git status execution + $result = Process::run('git status --porcelain '.escapeshellarg($relativePath)); + $status = $result->successful() ? trim($result->output()) : ''; + + if (empty($status)) { + return 'โœ…'; + } + + return match (substr($status, 0, 2)) { '??' => 'โ“', 'A ' => 'โž•', 'M ' => '๐Ÿ“', @@ -283,16 +296,17 @@ private function getGitStatus(string $path): ?string }; } - private function formatSize(int $bytes): string { - if ($bytes === 0) return '0 B'; - + if ($bytes === 0) { + return '0 B'; + } + $units = ['B', 'KB', 'MB', 'GB', 'TB']; $factor = floor(log($bytes, 1024)); $factor = min($factor, count($units) - 1); - - return sprintf("%.1f %s", $bytes / pow(1024, $factor), $units[$factor]); + + return sprintf('%.1f %s', $bytes / pow(1024, $factor), $units[$factor]); } private function displayInteractive(array $files, string $path): int @@ -302,6 +316,7 @@ private function displayInteractive(array $files, string $path): int if (empty($files)) { $this->smartLine('Empty directory (how sad)'); + return self::SUCCESS; } @@ -310,18 +325,18 @@ private function displayInteractive(array $files, string $path): int foreach ($files as $file) { $sizeStr = $file['type'] === 'directory' ? '-' : $this->formatSize($file['size']); $modifiedStr = $file['modified']->diffForHumans(); - $nameWithIcon = $file['icon'] . ' ' . $file['name']; - + $nameWithIcon = $file['icon'].' '.$file['name']; + // Add git status if enabled if ($this->option('git') && $file['git_status']) { - $nameWithIcon .= ' ' . $file['git_status']; + $nameWithIcon .= ' '.$file['git_status']; } $rows[] = [ substr($nameWithIcon, 0, 30), $sizeStr, substr($modifiedStr, 0, 15), - $file['permissions'] + $file['permissions'], ]; } @@ -332,8 +347,8 @@ private function displayInteractive(array $files, string $path): int ); $this->smartNewLine(); - $this->smartLine("๐Ÿ’ก Tip: Use --json for machine-readable output"); - $this->smartLine("๐Ÿ’ก Tip: Use --recent, --large, or --git for different views"); + $this->smartLine('๐Ÿ’ก Tip: Use --json for machine-readable output'); + $this->smartLine('๐Ÿ’ก Tip: Use --recent, --large, or --git for different views'); return self::SUCCESS; } @@ -342,16 +357,17 @@ private function runInteractiveBrowser(string $currentPath): int { while (true) { $files = $this->scanDirectory($currentPath); - + if (empty($files)) { $this->smartLine("Empty directory: {$currentPath}"); $action = select( '๐Ÿš€ What would you like to do?', ['Go up one level', 'Exit browser'] ); - + if ($action === 'Go up one level') { $currentPath = dirname($currentPath); + continue; } else { break; @@ -360,29 +376,29 @@ private function runInteractiveBrowser(string $currentPath): int // Build options for selection $options = []; - + // Add "Go up" option if not at root if ($currentPath !== '/') { $options['..'] = '๐Ÿ“ .. (Go up one level)'; } - + // Add all files and directories foreach ($files as $file) { - $display = $file['icon'] . ' ' . $file['name']; - + $display = $file['icon'].' '.$file['name']; + if ($file['type'] === 'directory') { $display .= '/'; } else { - $display .= ' (' . $this->formatSize($file['size']) . ')'; + $display .= ' ('.$this->formatSize($file['size']).')'; } - + if ($this->option('git') && $file['git_status']) { - $display .= ' ' . $file['git_status']; + $display .= ' '.$file['git_status']; } - + $options[$file['name']] = $display; } - + // Add action options $options['__actions__'] = 'โšก Actions...'; $options['__exit__'] = '๐Ÿšช Exit browser'; @@ -398,27 +414,30 @@ private function runInteractiveBrowser(string $currentPath): int if ($choice === '__exit__') { break; } - + if ($choice === '__actions__') { $this->showFileActions($currentPath); + continue; } - + if ($choice === '..') { $currentPath = dirname($currentPath); + continue; } - $selectedPath = $currentPath . DIRECTORY_SEPARATOR . $choice; - + $selectedPath = $currentPath.DIRECTORY_SEPARATOR.$choice; + if (is_dir($selectedPath)) { $currentPath = $selectedPath; } else { $this->handleFileSelection($selectedPath); } } - + $this->smartInfo('๐Ÿ‘‹ Exited interactive browser'); + return self::SUCCESS; } @@ -431,7 +450,7 @@ private function showFileActions(string $currentPath): void 'create_file' => '๐Ÿ“„ Create new file', 'create_dir' => '๐Ÿ“ Create new directory', 'show_path' => '๐Ÿ“ Show current path', - 'back' => 'โฌ…๏ธ Back to browser' + 'back' => 'โฌ…๏ธ Back to browser', ] ); @@ -439,12 +458,12 @@ private function showFileActions(string $currentPath): void case 'refresh': $this->smartInfo('๐Ÿ”„ Directory refreshed'); break; - + case 'create_file': $filename = text('๐Ÿ“„ Enter filename:'); if ($filename) { - $fullPath = $currentPath . DIRECTORY_SEPARATOR . $filename; - if (!file_exists($fullPath)) { + $fullPath = $currentPath.DIRECTORY_SEPARATOR.$filename; + if (! file_exists($fullPath)) { touch($fullPath); $this->smartInfo("โœ… Created file: {$filename}"); } else { @@ -452,12 +471,12 @@ private function showFileActions(string $currentPath): void } } break; - + case 'create_dir': $dirname = text('๐Ÿ“ Enter directory name:'); if ($dirname) { - $fullPath = $currentPath . DIRECTORY_SEPARATOR . $dirname; - if (!is_dir($fullPath)) { + $fullPath = $currentPath.DIRECTORY_SEPARATOR.$dirname; + if (! is_dir($fullPath)) { mkdir($fullPath, 0755, true); $this->smartInfo("โœ… Created directory: {$dirname}"); } else { @@ -465,7 +484,7 @@ private function showFileActions(string $currentPath): void } } break; - + case 'show_path': $this->smartInfo("๐Ÿ“ Current path: {$currentPath}"); break; @@ -476,7 +495,7 @@ private function handleFileSelection(string $filePath): void { $filename = basename($filePath); $filesize = $this->formatSize(filesize($filePath)); - + $action = select( "๐Ÿ“„ {$filename} ({$filesize})", [ @@ -485,7 +504,7 @@ private function handleFileSelection(string $filePath): void 'copy_path' => '๐Ÿ“‹ Copy path to clipboard', 'delete' => '๐Ÿ—‘๏ธ Delete file', 'info' => 'โ„น๏ธ Show file info', - 'back' => 'โฌ…๏ธ Back to browser' + 'back' => 'โฌ…๏ธ Back to browser', ] ); @@ -493,22 +512,22 @@ private function handleFileSelection(string $filePath): void case 'view': $this->viewFile($filePath); break; - + case 'edit': $this->editFile($filePath); break; - + case 'copy_path': $this->smartInfo("๐Ÿ“‹ Path copied: {$filePath}"); break; - + case 'delete': if (confirm("๐Ÿ—‘๏ธ Are you sure you want to delete {$filename}?")) { unlink($filePath); $this->smartInfo("โœ… Deleted: {$filename}"); } break; - + case 'info': $this->showFileInfo($filePath); break; @@ -519,18 +538,18 @@ private function viewFile(string $filePath): void { $content = file_get_contents($filePath); $lines = explode("\n", $content); - - $this->smartInfo("๐Ÿ‘๏ธ Viewing: " . basename($filePath)); + + $this->smartInfo('๐Ÿ‘๏ธ Viewing: '.basename($filePath)); $this->smartLine(str_repeat('โ”€', 50)); - + foreach (array_slice($lines, 0, 20) as $i => $line) { $this->smartLine(sprintf('%3d: %s', $i + 1, $line)); } - + if (count($lines) > 20) { $this->smartLine('... (truncated, showing first 20 lines)'); } - + $this->smartLine(str_repeat('โ”€', 50)); } @@ -538,25 +557,24 @@ private function editFile(string $filePath): void { $editor = getenv('EDITOR') ?: 'nano'; $this->smartInfo("โœ๏ธ Opening {$filePath} with {$editor}"); - // Use Process class for safer editor execution - $process = new \Symfony\Component\Process\Process([$editor, $filePath]); - $process->setTty(true); - $process->run(); + // Use Laravel Process for safer editor execution + Process::tty() + ->run(escapeshellcmd($editor).' '.escapeshellarg($filePath)); } private function showFileInfo(string $filePath): void { $stat = stat($filePath); $filename = basename($filePath); - + $this->smartInfo("โ„น๏ธ File Information: {$filename}"); $this->smartLine("๐Ÿ“„ Path: {$filePath}"); - $this->smartLine("๐Ÿ“Š Size: " . $this->formatSize($stat['size'])); - $this->smartLine("๐Ÿ“… Modified: " . Carbon::createFromTimestamp($stat['mtime'])->format('Y-m-d H:i:s')); - $this->smartLine("๐Ÿ” Permissions: " . $this->getPermissions($filePath)); - + $this->smartLine('๐Ÿ“Š Size: '.$this->formatSize($stat['size'])); + $this->smartLine('๐Ÿ“… Modified: '.Carbon::createFromTimestamp($stat['mtime'])->format('Y-m-d H:i:s')); + $this->smartLine('๐Ÿ” Permissions: '.$this->getPermissions($filePath)); + if (is_link($filePath)) { - $this->smartLine("๐Ÿ”— Symlink to: " . readlink($filePath)); + $this->smartLine('๐Ÿ”— Symlink to: '.readlink($filePath)); } } } diff --git a/app/Commands/QualityCommand.php b/app/Commands/QualityCommand.php new file mode 100644 index 0000000..404f4a4 --- /dev/null +++ b/app/Commands/QualityCommand.php @@ -0,0 +1,464 @@ + [], + 'stan' => [], + 'tests' => [], + ]; + + protected function executeCommand(): int + { + $this->title('๐Ÿ’ฉ THE SHIT Quality Checks'); + + $path = $this->option('path') ?? getcwd(); + + // Check if we're in a valid Laravel/PHP project + if (! file_exists($path.'/composer.json')) { + error('No composer.json found in '.$path); + note('Run this command in a Laravel/PHP project directory'); + + return self::FAILURE; + } + + // Check for vendor directory + if (! is_dir($path.'/vendor')) { + warning('No vendor directory found. Running composer install...'); + $this->task('Installing dependencies', function () use ($path) { + $result = Process::path($path)->run('composer install'); + + return $result->successful(); + }); + } + + $failed = false; + + // 1. Code Formatting with Pint + $pintResult = $this->task('๐Ÿ“ Checking code formatting (Laravel Pint)', function () use ($path) { + return $this->runPint($path); + }); + + if (! $pintResult) { + $failed = true; + $this->displayPintIssues(); + if (! $this->option('fix')) { + note('๐Ÿ’ก Run with --fix to auto-fix code style issues'); + } + } + + // 2. Static Analysis with Larastan + $stanResult = $this->task('๐Ÿ” Running static analysis (Larastan)', function () use ($path) { + return $this->runStan($path); + }); + + if (! $stanResult) { + $failed = true; + $this->displayStanIssues(); + } + + // 3. Tests with Pest (optional) + if (! $this->option('no-tests')) { + $testResult = $this->task('๐Ÿงช Running tests (Pest)', function () use ($path) { + return $this->runTests($path); + }); + + if (! $testResult) { + $failed = true; + $this->displayTestFailures(); + } + $this->results['tests'] = $testResult; + } else { + $this->results['tests'] = 'skipped'; + note('Skipping tests (--no-tests flag)'); + } + + $this->newLine(); + + if ($failed) { + error('โŒ Quality checks failed!'); + $this->newLine(); + info('Fix the issues above, then run:'); + $this->line(' php ๐Ÿ’ฉ quality --fix'); + + return self::FAILURE; + } + + $this->info('โœจ All quality checks passed!'); + $this->info('๐Ÿ’ฉ Your code is THE SHIT (in a good way)!'); + + if ($this->isNonInteractiveMode()) { + return $this->jsonResponse([ + 'results' => $this->results, + 'passed' => true, + 'issues' => $this->issues, + ]); + } + + return self::SUCCESS; + } + + private function runPint(string $path): bool + { + $autoFix = $this->option('fix'); + + // Check if Pint exists + $pintPath = $path.'/vendor/bin/pint'; + if (! file_exists($pintPath)) { + $this->results['pint'] = 'not-installed'; + + return true; // Don't fail if Pint isn't installed + } + + $command = $autoFix ? $pintPath : $pintPath.' --test'; + $result = Process::path($path)->timeout(60)->run($command); + + $this->results['pint'] = $result->successful(); + + if (! $result->successful() && ! $autoFix) { + $this->parsePintOutput($result->output()); + $this->results['pint_output'] = $result->output(); + } + + return $result->successful(); + } + + private function runStan(string $path): bool + { + // Check if PHPStan/Larastan exists + $stanPath = $path.'/vendor/bin/phpstan'; + if (! file_exists($stanPath)) { + $this->results['stan'] = 'not-installed'; + + return true; // Don't fail if not installed + } + + // Check for phpstan.neon config + $configPath = $path.'/phpstan.neon'; + $command = file_exists($configPath) + ? $stanPath.' analyse --memory-limit=512M' + : $stanPath.' analyse --memory-limit=512M --level=5 app'; + + $result = Process::path($path)->timeout(120)->run($command); + + $this->results['stan'] = $result->successful(); + + if (! $result->successful()) { + $this->parseStanOutput($result->output()); + $this->results['stan_output'] = $result->output(); + } + + return $result->successful(); + } + + private function runTests(string $path): bool + { + // Check if Pest exists + $pestPath = $path.'/vendor/bin/pest'; + if (! file_exists($pestPath)) { + // Try PHPUnit as fallback + $phpunitPath = $path.'/vendor/bin/phpunit'; + if (! file_exists($phpunitPath)) { + $this->results['tests'] = 'not-installed'; + + return true; // Don't fail if no test runner + } + $testCommand = $phpunitPath; + } else { + $testCommand = $pestPath; + } + + $result = Process::path($path)->timeout(180)->run($testCommand); + + $this->results['tests'] = $result->successful(); + + if (! $result->successful()) { + $this->parseTestOutput($result->output()); + $this->results['tests_output'] = $result->output(); + } + + return $result->successful(); + } + + private function parsePintOutput(string $output): void + { + // Parse Pint output for file issues + $lines = explode("\n", $output); + foreach ($lines as $line) { + // Look for lines with file paths and rule violations + if (preg_match('/^ โจฏ (.+?)\s+(.+)$/', $line, $matches)) { + $this->issues['pint'][] = [ + 'file' => trim($matches[1]), + 'rules' => trim($matches[2]), + ]; + } + } + } + + private function parseStanOutput(string $output): void + { + // Parse PHPStan/Larastan output - it uses a table format with borders + $lines = explode("\n", $output); + $currentFile = null; + + for ($i = 0; $i < count($lines); $i++) { + $line = $lines[$i]; + + // Look for the header pattern " Line filename.php " + if (preg_match('/^\s*Line\s+(.+\.php)\s*$/', $line, $matches)) { + $currentFile = trim($matches[1]); + // Skip the separator line after header + $i++; + + continue; + } + + // Parse error lines when we have a current file + if ($currentFile && preg_match('/^\s*(\d+)\s+(.+)$/', $line, $matches)) { + $lineNum = $matches[1]; + $message = trim($matches[2]); + + // Add the error + $errorIndex = count($this->issues['stan']); + $this->issues['stan'][] = [ + 'file' => $currentFile, + 'line' => $lineNum, + 'message' => $message, + ]; + + // Check for continuation lines (indented lines that follow) + while ($i + 1 < count($lines)) { + $nextLine = $lines[$i + 1]; + // If it's indented and not a line number, it's a continuation + if (preg_match('/^\s{2,}(?!\d)(.+)$/', $nextLine, $contMatches)) { + $content = trim($contMatches[1]); + // Skip emoji/badge lines + if (! str_starts_with($content, '๐Ÿชช') && ! str_starts_with($content, '------')) { + $this->issues['stan'][$errorIndex]['message'] .= ' '.$content; + } + $i++; + } else { + break; + } + } + } + + // Reset current file when we hit a separator after errors + if ($currentFile && preg_match('/^\s*-+\s*$/', $line)) { + $currentFile = null; + } + } + } + + private function parseTestOutput(string $output): void + { + // Parse Pest/PHPUnit output for failures + $lines = explode("\n", $output); + $currentTest = null; + + foreach ($lines as $line) { + // Look for FAILED test lines + if (preg_match('/FAILED\s+(.+?)\s+โ€บ\s+(.+)/', $line, $matches)) { + $this->issues['tests'][] = [ + 'file' => trim($matches[1]), + 'test' => trim($matches[2]), + ]; + } + // Look for assertion failures + elseif (preg_match('/Failed asserting that (.+)/', $line, $matches)) { + if (! empty($this->issues['tests'])) { + $lastIndex = count($this->issues['tests']) - 1; + $this->issues['tests'][$lastIndex]['assertion'] = trim($matches[1]); + } + } + } + } + + private function displayPintIssues(): void + { + if (empty($this->issues['pint'])) { + return; + } + + $this->newLine(); + warning('๐Ÿ“ Code Style Issues (Laravel Pint)'); + + $rows = []; + foreach ($this->issues['pint'] as $issue) { + $fullPath = getcwd().'/'.$issue['file']; + $file = str_replace(getcwd().'/', '', $issue['file']); + + // Create clickable link for the file + $clickableFile = $this->makeFileClickable($fullPath, $file); + + $rules = str_replace(',', ', ', $issue['rules']); + // Truncate long rule lists + if (strlen($rules) > 50) { + $rules = substr($rules, 0, 47).'...'; + } + $rows[] = [$clickableFile, $rules]; + } + + table( + headers: ['File', 'Rules'], + rows: $rows + ); + } + + private function displayStanIssues(): void + { + if (empty($this->issues['stan'])) { + return; + } + + $this->newLine(); + warning('๐Ÿ” Static Analysis Issues (Larastan)'); + + // Group by file + $byFile = []; + foreach ($this->issues['stan'] as $issue) { + $file = str_replace(getcwd().'/', '', $issue['file']); + if (! isset($byFile[$file])) { + $byFile[$file] = []; + } + $byFile[$file][] = $issue; + } + + foreach ($byFile as $file => $fileIssues) { + $fullPath = getcwd().'/'.$file; + $this->line(''); + + // Make file header clickable + $clickableFile = $this->makeFileClickable($fullPath, "๐Ÿ“ $file"); + info($clickableFile); + + $rows = []; + foreach ($fileIssues as $issue) { + // Create clickable line number that opens file at specific line + $clickableLine = $this->makeFileClickable($fullPath.':'.$issue['line'], "Line {$issue['line']}"); + + $message = $issue['message']; + // Truncate very long messages + if (strlen($message) > 80) { + $message = substr($message, 0, 77).'...'; + } + $rows[] = [$clickableLine, $message]; + } + + table( + headers: ['Location', 'Issue'], + rows: $rows + ); + } + + $totalErrors = count($this->issues['stan']); + $this->newLine(); + error("Found $totalErrors static analysis ".($totalErrors === 1 ? 'error' : 'errors')); + } + + private function displayTestFailures(): void + { + if (empty($this->issues['tests'])) { + return; + } + + $this->newLine(); + warning('๐Ÿงช Test Failures'); + + $rows = []; + foreach ($this->issues['tests'] as $failure) { + $fullPath = getcwd().'/'.$failure['file']; + $file = str_replace(getcwd().'/', '', $failure['file']); + + // Create clickable link for test file + $clickableFile = $this->makeFileClickable($fullPath, $file); + + $test = $failure['test']; + if (isset($failure['assertion'])) { + $test .= "\n โ†’ ".$failure['assertion']; + } + $rows[] = [$clickableFile, $test]; + } + + table( + headers: ['Test File', 'Failed Test'], + rows: $rows + ); + + $totalFailures = count($this->issues['tests']); + $this->newLine(); + error("$totalFailures test".($totalFailures === 1 ? '' : 's').' failed'); + } + + /** + * Create a clickable terminal link using OSC 8 hyperlinks + * Works in modern terminals like iTerm2, VS Code terminal, etc. + */ + private function makeFileClickable(string $path, string $text): string + { + // Check if terminal supports hyperlinks + if (! $this->terminalSupportsHyperlinks()) { + return $text; + } + + // Create file:// URL for the path + // Many editors register as handlers for file:// URLs with line numbers + $url = 'file://'.$path; + + // OSC 8 hyperlink format: \e]8;;URL\e\\TEXT\e]8;;\e\\ + return "\e]8;;{$url}\e\\{$text}\e]8;;\e\\"; + } + + /** + * Check if terminal supports hyperlinks + */ + private function terminalSupportsHyperlinks(): bool + { + // Check common terminal environment variables + $term = getenv('TERM_PROGRAM'); + $terminalApp = getenv('TERMINAL_EMULATOR'); + + // List of terminals known to support OSC 8 hyperlinks + $supportedTerminals = [ + 'iTerm.app', + 'vscode', + 'WezTerm', + 'Hyper', + 'Tabby', + 'Alacritty', // With config + 'kitty', + ]; + + foreach ($supportedTerminals as $supported) { + if (stripos($term, $supported) !== false || stripos($terminalApp, $supported) !== false) { + return true; + } + } + + // Check if we're in VS Code integrated terminal + if (getenv('VSCODE_GIT_IPC_HANDLE') || getenv('TERM_PROGRAM_VERSION')) { + return true; + } + + // Default to true for modern terminals, user can disable if needed + return true; + } +} diff --git a/app/Commands/TestHumanAiCommand.php b/app/Commands/TestHumanAiCommand.php index 121755f..30b8ef0 100644 --- a/app/Commands/TestHumanAiCommand.php +++ b/app/Commands/TestHumanAiCommand.php @@ -28,8 +28,9 @@ protected function executeCommand(): int default: true ); - if (!$shouldContinue) { + if (! $shouldContinue) { $this->smartInfo('Operation cancelled.'); + return self::SUCCESS; } @@ -39,7 +40,7 @@ protected function executeCommand(): int 'mode' => $this->isNonInteractiveMode() ? 'ai-agent' : 'human-interactive', 'user_agent' => $this->getUserAgent(), 'timestamp' => now()->toISOString(), - 'pattern_works' => true + 'pattern_works' => true, ]; if ($this->isNonInteractiveMode()) { @@ -48,6 +49,7 @@ protected function executeCommand(): int $this->smartOutput($result, 'โœ… Human-AI collaboration pattern works!'); $this->smartNewLine(); $this->smartLine('๐ŸŽ‰ This proves the architecture is sound!'); + return self::SUCCESS; } } @@ -60,7 +62,7 @@ protected function executeNonInteractive(): int 'user_agent' => $this->getUserAgent(), 'message' => 'AI agent executed specialized non-interactive logic', 'defaults_used' => true, - 'timestamp' => now()->toISOString() + 'timestamp' => now()->toISOString(), ]; return $this->jsonResponse($result); diff --git a/app/Providers/ComponentServiceProvider.php b/app/Providers/ComponentServiceProvider.php index ce4af8e..6186a6c 100644 --- a/app/Providers/ComponentServiceProvider.php +++ b/app/Providers/ComponentServiceProvider.php @@ -4,7 +4,6 @@ use App\Commands\ConduitCommand; use Illuminate\Support\ServiceProvider; -use Symfony\Component\Process\Process; class ComponentServiceProvider extends ServiceProvider { @@ -21,12 +20,12 @@ public function boot(): void private function discoverAndRegisterComponents(): void { $componentsPath = base_path('๐Ÿ’ฉ-components'); - - if (!is_dir($componentsPath)) { + + if (! is_dir($componentsPath)) { return; } - foreach (glob($componentsPath . '/*/๐Ÿ’ฉ.json') as $manifestPath) { + foreach (glob($componentsPath.'/*/๐Ÿ’ฉ.json') as $manifestPath) { $this->registerComponent(dirname($manifestPath)); } } @@ -34,16 +33,16 @@ private function discoverAndRegisterComponents(): void private function registerComponent(string $componentPath): void { try { - $config = json_decode(file_get_contents($componentPath . '/๐Ÿ’ฉ.json'), true); - - if (!isset($config['commands'])) { + $config = json_decode(file_get_contents($componentPath.'/๐Ÿ’ฉ.json'), true); + + if (! isset($config['commands'])) { return; } // Get the executable path $executable = $this->getComponentExecutable($componentPath, $config); - - if (!$executable || !file_exists($executable)) { + + if (! $executable || ! file_exists($executable)) { return; } @@ -57,23 +56,24 @@ private function registerComponent(string $componentPath): void } } - private function getComponentExecutable(string $componentPath, array $config): ?string + private function getComponentExecutable(string $componentPath, array $config): string { if (isset($config['executable'])) { - return $componentPath . '/bin/' . $config['executable']; + return $componentPath.'/bin/'.$config['executable']; } $componentName = basename($componentPath); - return $componentPath . '/bin/' . $componentName; + + return $componentPath.'/bin/'.$componentName; } private function createAndRegisterProxyCommand(string $command, string $executable, string $description): void { // Create a dynamic command class that extends ConduitCommand $className = $this->generateCommandClassName($command); - + eval($this->generateCommandClass($className, $command, $executable, $description)); - + // Register the command $this->commands([$className]); } @@ -81,14 +81,15 @@ private function createAndRegisterProxyCommand(string $command, string $executab private function generateCommandClassName(string $command): string { // Create a safe class name from the command - $className = 'Dynamic' . str_replace([':', '-', '.'], '', ucwords($command, ':-.')) . 'ProxyCommand'; + $className = 'Dynamic'.str_replace([':', '-', '.'], '', ucwords($command, ':-.')).'ProxyCommand'; + return "App\\Commands\\Dynamic\\{$className}"; } private function generateCommandClass(string $className, string $command, string $executable, string $description): string { $shortClassName = substr(strrchr($className, '\\'), 1); - + return " namespace App\Commands\Dynamic; @@ -153,4 +154,4 @@ private function executeComponentCommand(): int } }"; } -} \ No newline at end of file +} diff --git a/app/ValueObjects/ComponentResult.php b/app/ValueObjects/ComponentResult.php index 7401e64..c6a2705 100644 --- a/app/ValueObjects/ComponentResult.php +++ b/app/ValueObjects/ComponentResult.php @@ -52,4 +52,4 @@ public function toJson(): string { return json_encode($this->toArray(), JSON_PRETTY_PRINT); } -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 5046788..7155f36 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,11 @@ ], "require": { "php": "^8.2", - "laravel-zero/framework": "^12.0", - "jordanpartridge/conduit-interfaces": "^1.0" + "jordanpartridge/github-client": "^2.9", + "laravel-zero/framework": "^12.0" }, "require-dev": { + "larastan/larastan": "^3.6", "laravel/pint": "^1.22", "mockery/mockery": "^1.6.12", "pestphp/pest": "^3.8.2" @@ -45,6 +46,22 @@ "pestphp/pest-plugin": true } }, + "scripts": { + "quality": [ + "@pint", + "@stan", + "@test" + ], + "pint": "./vendor/bin/pint", + "pint:test": "./vendor/bin/pint --test", + "stan": "./vendor/bin/phpstan analyse --memory-limit=512M", + "test": "./vendor/bin/pest", + "check": [ + "@pint:test", + "@stan", + "@test" + ] + }, "minimum-stability": "stable", "prefer-stable": true, "bin": ["๐Ÿ’ฉ"] diff --git a/composer.lock b/composer.lock index 144a953..ed29a5f 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": "3a9cb937a366b1b40df19f1b7282c763", + "content-hash": "6f8c2177a22e9666b089320ed4796e3b", "packages": [ { "name": "brick/math", @@ -135,6 +135,65 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "conduit-ui/github-connector", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/conduit-io/github-connector.git", + "reference": "283623929d2f35ff8a877103eecb58b7838444a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/conduit-io/github-connector/zipball/283623929d2f35ff8a877103eecb58b7838444a4", + "reference": "283623929d2f35ff8a877103eecb58b7838444a4", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4", + "saloonphp/saloon": "^3.10" + }, + "require-dev": { + "laravel/pint": "^1.14", + "nunomaduro/collision": "^7.0||^8.0", + "pestphp/pest": "^2.34||^3.0", + "pestphp/pest-plugin-arch": "^2.7||^3.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "ConduitUi\\GitHubConnector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordan Partridge", + "email": "jordan.l.partridge@gmail.com", + "role": "Developer" + } + ], + "description": "Core GitHub API connector with authentication", + "homepage": "https://github.com/conduit-ui/github-connector", + "keywords": [ + "api", + "conduit-ui", + "connector", + "github" + ], + "support": { + "issues": "https://github.com/conduit-io/github-connector/issues", + "source": "https://github.com/conduit-io/github-connector/tree/v1.0.0" + }, + "time": "2025-06-28T05:59:10+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.10", @@ -293,16 +352,16 @@ }, { "name": "filp/whoops", - "version": "2.18.3", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "59a123a3d459c5a23055802237cb317f609867e5" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", - "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -352,7 +411,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.3" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -360,7 +419,70 @@ "type": "github" } ], - "time": "2025-06-16T00:02:10+00:00" + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" }, { "name": "graham-campbell/result-type", @@ -751,16 +873,16 @@ }, { "name": "illuminate/bus", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "37d2ab45b0d6e8cf2d43b2f2833d875c208787b5" + "reference": "774ff2a22a93643d94bae8aad74fdc39a8c7084c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/37d2ab45b0d6e8cf2d43b2f2833d875c208787b5", - "reference": "37d2ab45b0d6e8cf2d43b2f2833d875c208787b5", + "url": "https://api.github.com/repos/illuminate/bus/zipball/774ff2a22a93643d94bae8aad74fdc39a8c7084c", + "reference": "774ff2a22a93643d94bae8aad74fdc39a8c7084c", "shasum": "" }, "require": { @@ -800,20 +922,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-15T20:29:59+00:00" + "time": "2025-08-04T20:19:41+00:00" }, { "name": "illuminate/cache", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/cache.git", - "reference": "8293accd918e5f09195bab0aff001b493f931b52" + "reference": "44d5f41037f5d82344f861a639640407381245ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/cache/zipball/8293accd918e5f09195bab0aff001b493f931b52", - "reference": "8293accd918e5f09195bab0aff001b493f931b52", + "url": "https://api.github.com/repos/illuminate/cache/zipball/44d5f41037f5d82344f861a639640407381245ab", + "reference": "44d5f41037f5d82344f861a639640407381245ab", "shasum": "" }, "require": { @@ -862,20 +984,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-13T17:50:53+00:00" + "time": "2025-08-06T15:33:28+00:00" }, { "name": "illuminate/collections", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "a048b4fbbef4742ff2eee843971bb8278239c610" + "reference": "19fa59c43c0c0e85171fd3aa8ed3b89604e34b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/a048b4fbbef4742ff2eee843971bb8278239c610", - "reference": "a048b4fbbef4742ff2eee843971bb8278239c610", + "url": "https://api.github.com/repos/illuminate/collections/zipball/19fa59c43c0c0e85171fd3aa8ed3b89604e34b63", + "reference": "19fa59c43c0c0e85171fd3aa8ed3b89604e34b63", "shasum": "" }, "require": { @@ -919,11 +1041,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-15T20:29:59+00:00" + "time": "2025-08-04T20:12:58+00:00" }, { "name": "illuminate/conditionable", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -969,7 +1091,7 @@ }, { "name": "illuminate/config", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/config.git", @@ -1017,16 +1139,16 @@ }, { "name": "illuminate/console", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/console.git", - "reference": "a9873093f2d4cd0072d73bec27e96d44819273f4" + "reference": "e997deb5c4854e1990d59d4b58c9088a65657e54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/console/zipball/a9873093f2d4cd0072d73bec27e96d44819273f4", - "reference": "a9873093f2d4cd0072d73bec27e96d44819273f4", + "url": "https://api.github.com/repos/illuminate/console/zipball/e997deb5c4854e1990d59d4b58c9088a65657e54", + "reference": "e997deb5c4854e1990d59d4b58c9088a65657e54", "shasum": "" }, "require": { @@ -1079,20 +1201,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-16T13:18:38+00:00" + "time": "2025-08-04T20:23:05+00:00" }, { "name": "illuminate/container", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "3b0defd0daf88f5b21767ed0cad7e6e3a699c9e4" + "reference": "14864194c13703df0f9b90baf5b886dce9c2055c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/3b0defd0daf88f5b21767ed0cad7e6e3a699c9e4", - "reference": "3b0defd0daf88f5b21767ed0cad7e6e3a699c9e4", + "url": "https://api.github.com/repos/illuminate/container/zipball/14864194c13703df0f9b90baf5b886dce9c2055c", + "reference": "14864194c13703df0f9b90baf5b886dce9c2055c", "shasum": "" }, "require": { @@ -1130,20 +1252,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-20T18:31:22+00:00" + "time": "2025-08-03T15:10:34+00:00" }, { "name": "illuminate/contracts", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "c2eef857b808810f5cb187de58e23d25c1d443d9" + "reference": "458573a554b927e9594bb35baf9a7897dea03303" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/c2eef857b808810f5cb187de58e23d25c1d443d9", - "reference": "c2eef857b808810f5cb187de58e23d25c1d443d9", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/458573a554b927e9594bb35baf9a7897dea03303", + "reference": "458573a554b927e9594bb35baf9a7897dea03303", "shasum": "" }, "require": { @@ -1178,20 +1300,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-16T13:18:38+00:00" + "time": "2025-08-03T15:27:01+00:00" }, { "name": "illuminate/events", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5" + "reference": "312d796c424b1bf08eecd6d883dd57d4c0ca7bbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/bf1f121ea51e077e893d32e2848e102513d4b1b5", - "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5", + "url": "https://api.github.com/repos/illuminate/events/zipball/312d796c424b1bf08eecd6d883dd57d4c0ca7bbf", + "reference": "312d796c424b1bf08eecd6d883dd57d4c0ca7bbf", "shasum": "" }, "require": { @@ -1233,20 +1355,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-08-06T19:19:00+00:00" }, { "name": "illuminate/filesystem", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "c32b4c57893add1cc83cae077903d62f5b0cbc7e" + "reference": "d1c5bbb3b84649599def8ddd814c1f9543930055" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c32b4c57893add1cc83cae077903d62f5b0cbc7e", - "reference": "c32b4c57893add1cc83cae077903d62f5b0cbc7e", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/d1c5bbb3b84649599def8ddd814c1f9543930055", + "reference": "d1c5bbb3b84649599def8ddd814c1f9543930055", "shasum": "" }, "require": { @@ -1300,11 +1422,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-16T13:18:38+00:00" + "time": "2025-08-04T20:03:30+00:00" }, { "name": "illuminate/macroable", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -1350,23 +1472,27 @@ }, { "name": "illuminate/pipeline", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", - "reference": "a1039dfe54854470cdda37782bab0901aa588dd4" + "reference": "ff1e7672016def22c74f50c5a2d3858d6f5d4a96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pipeline/zipball/a1039dfe54854470cdda37782bab0901aa588dd4", - "reference": "a1039dfe54854470cdda37782bab0901aa588dd4", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/ff1e7672016def22c74f50c5a2d3858d6f5d4a96", + "reference": "ff1e7672016def22c74f50c5a2d3858d6f5d4a96", "shasum": "" }, "require": { "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", "php": "^8.2" }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, "type": "library", "extra": { "branch-alias": { @@ -1394,11 +1520,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-08-06T19:09:00+00:00" }, { "name": "illuminate/process", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/process.git", @@ -1449,16 +1575,16 @@ }, { "name": "illuminate/support", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "212103694b407b41dce5aae4fe41fbe6ee1bec49" + "reference": "bd4e42af1bae237cd0f9f70613eb3a90caea5711" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/212103694b407b41dce5aae4fe41fbe6ee1bec49", - "reference": "212103694b407b41dce5aae4fe41fbe6ee1bec49", + "url": "https://api.github.com/repos/illuminate/support/zipball/bd4e42af1bae237cd0f9f70613eb3a90caea5711", + "reference": "bd4e42af1bae237cd0f9f70613eb3a90caea5711", "shasum": "" }, "require": { @@ -1522,11 +1648,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-22T13:57:22+00:00" + "time": "2025-08-07T21:04:11+00:00" }, { "name": "illuminate/testing", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/testing.git", @@ -1585,16 +1711,16 @@ }, { "name": "illuminate/view", - "version": "v12.21.0", + "version": "v12.22.1", "source": { "type": "git", "url": "https://github.com/illuminate/view.git", - "reference": "ffce087bc252a90ce503568321394497cd274cb4" + "reference": "ab0346c57d5e07719083bfe8cbe5c1822a16addd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/view/zipball/ffce087bc252a90ce503568321394497cd274cb4", - "reference": "ffce087bc252a90ce503568321394497cd274cb4", + "url": "https://api.github.com/repos/illuminate/view/zipball/ab0346c57d5e07719083bfe8cbe5c1822a16addd", + "reference": "ab0346c57d5e07719083bfe8cbe5c1822a16addd", "shasum": "" }, "require": { @@ -1635,7 +1761,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-20T18:38:44+00:00" + "time": "2025-07-30T17:49:47+00:00" }, { "name": "jolicode/jolinotif", @@ -1754,6 +1880,97 @@ }, "time": "2023-12-03T12:46:03+00:00" }, + { + "name": "jordanpartridge/github-client", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/jordanpartridge/github-client.git", + "reference": "d0c36127aa4363d9f6f7a01b142c208e80b48c20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jordanpartridge/github-client/zipball/d0c36127aa4363d9f6f7a01b142c208e80b48c20", + "reference": "d0c36127aa4363d9f6f7a01b142c208e80b48c20", + "shasum": "" + }, + "require": { + "conduit-ui/github-connector": "^1.0", + "firebase/php-jwt": "^6.0", + "illuminate/contracts": "^10.0||^11.0||^12.0", + "php": "^8.2|^8.3|^8.4", + "saloonphp/saloon": "^3.10", + "spatie/laravel-package-tools": "^1.16|^2.0" + }, + "require-dev": { + "larastan/larastan": "^2.0", + "laravel/pint": "^1.14", + "nunomaduro/collision": "^7.0||^8.0||^9.0", + "orchestra/testbench": "^8.0||^9.0||^10.0||^11.0||^12.0", + "pestphp/pest": "^2.34||^3.0", + "pestphp/pest-plugin-arch": "^2.7||^3.0||^4.0", + "pestphp/pest-plugin-laravel": "^2.3||^3.0||^4.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Github": "JordanPartridge\\GithubClient\\Facades\\Github" + }, + "providers": [ + "JordanPartridge\\GithubClient\\GithubClientServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "JordanPartridge\\GithubClient\\": "src/", + "JordanPartridge\\GithubClient\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordan Partridge", + "email": "jordan.l.partridge@gmail.com", + "role": "Developer" + } + ], + "description": "A powerful, Laravel-first GitHub API client with auto-pagination, strong typing, and comprehensive GitHub integration for repositories, pull requests, issues, and more.", + "homepage": "https://github.com/jordanpartridge/github-client", + "keywords": [ + "API-Client", + "GitHub-API", + "conduit-ui", + "git", + "github", + "github-client", + "issues", + "laravel", + "php", + "pull-requests", + "repositories", + "saloon", + "version-control" + ], + "support": { + "issues": "https://github.com/jordanpartridge/github-client/issues", + "source": "https://github.com/jordanpartridge/github-client/tree/v2.9.0" + }, + "funding": [ + { + "url": "https://github.com/JordanPartridge", + "type": "github" + } + ], + "time": "2025-07-26T06:14:31+00:00" + }, { "name": "laravel-zero/foundation", "version": "v12.17.1", @@ -3345,6 +3562,148 @@ }, "time": "2025-06-25T14:20:11+00:00" }, + { + "name": "saloonphp/saloon", + "version": "v3.14.0", + "source": { + "type": "git", + "url": "https://github.com/saloonphp/saloon.git", + "reference": "b1868db2d7c2eea57592b78797f24e8cddfb86cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/saloonphp/saloon/zipball/b1868db2d7c2eea57592b78797f24e8cddfb86cc", + "reference": "b1868db2d7c2eea57592b78797f24e8cddfb86cc", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.6", + "guzzlehttp/promises": "^1.5 || ^2.0", + "guzzlehttp/psr7": "^2.0", + "php": "^8.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "sammyjo20/saloon": "*" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "illuminate/collections": "^9.39 || ^10.0", + "league/flysystem": "^3.0", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^2.1.13", + "saloonphp/xml-wrangler": "^1.1", + "spatie/ray": "^1.33", + "symfony/dom-crawler": "^6.0 || ^7.0", + "symfony/var-dumper": "^6.3 || ^7.0" + }, + "suggest": { + "illuminate/collections": "Required for the response collect() method.", + "saloonphp/xml-wrangler": "Required for the response xmlReader() method.", + "symfony/dom-crawler": "Required for the response dom() method.", + "symfony/var-dumper": "Required for default debugging drivers." + }, + "type": "library", + "autoload": { + "psr-4": { + "Saloon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Carrรฉ", + "email": "29132017+Sammyjo20@users.noreply.github.com", + "role": "Developer" + } + ], + "description": "Build beautiful API integrations and SDKs with Saloon", + "homepage": "https://github.com/saloonphp/saloon", + "keywords": [ + "api", + "api-integrations", + "saloon", + "sammyjo20", + "sdk" + ], + "support": { + "issues": "https://github.com/saloonphp/saloon/issues", + "source": "https://github.com/saloonphp/saloon/tree/v3.14.0" + }, + "funding": [ + { + "url": "https://github.com/sammyjo20", + "type": "github" + } + ], + "time": "2025-06-21T11:18:37+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-17T15:46:43+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", @@ -5285,68 +5644,454 @@ "time": "2024-08-06T10:04:20+00:00" }, { - "name": "hamcrest/hamcrest-php", - "version": "v2.1.1", + "name": "fruitcake/php-cors", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", "shasum": "" }, "require": { - "php": "^7.4|^8.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { - "classmap": [ - "hamcrest" - ] + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "This is the PHP port of Hamcrest Matchers", + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", "keywords": [ - "test" + "cors", + "laravel", + "symfony" ], "support": { - "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" }, - "time": "2025-04-30T06:54:44+00:00" + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" }, { - "name": "jean85/pretty-package-versions", - "version": "2.1.1", + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", "source": { "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-02-03T10:55:03+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "iamcal/sql-parser", + "version": "v0.6", + "source": { + "type": "git", + "url": "https://github.com/iamcal/SQLParser.git", + "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62", + "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62", + "shasum": "" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "iamcal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cal Henderson", + "email": "cal@iamcal.com" + } + ], + "description": "MySQL schema parser", + "support": { + "issues": "https://github.com/iamcal/SQLParser/issues", + "source": "https://github.com/iamcal/SQLParser/tree/v0.6" + }, + "time": "2025-03-17T16:59:46+00:00" + }, + { + "name": "illuminate/database", + "version": "v12.22.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "4d8d119044e1936df08c66371bb75338a1f69cc2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/4d8d119044e1936df08c66371bb75338a1f69cc2", + "reference": "4d8d119044e1936df08c66371bb75338a1f69cc2", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13", + "ext-pdo": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-08-06T20:17:15+00:00" + }, + { + "name": "illuminate/http", + "version": "v12.22.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "db1220feef0dc1df151496f3f9bcecacf99375d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/db1220feef0dc1df151496f3f9bcecacf99375d0", + "reference": "db1220feef0dc1df151496f3f9bcecacf99375d0", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.31" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-07-30T17:40:40+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.22.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/9d27155b34bca502fe3e1adc16035b46d5ef3ed8", + "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { @@ -5395,6 +6140,95 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "larastan/larastan", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/larastan/larastan.git", + "reference": "6431d010dd383a9279eb8874a76ddb571738564a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/larastan/larastan/zipball/6431d010dd383a9279eb8874a76ddb571738564a", + "reference": "6431d010dd383a9279eb8874a76ddb571738564a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "iamcal/sql-parser": "^0.6.0", + "illuminate/console": "^11.44.2 || ^12.4.1", + "illuminate/container": "^11.44.2 || ^12.4.1", + "illuminate/contracts": "^11.44.2 || ^12.4.1", + "illuminate/database": "^11.44.2 || ^12.4.1", + "illuminate/http": "^11.44.2 || ^12.4.1", + "illuminate/pipeline": "^11.44.2 || ^12.4.1", + "illuminate/support": "^11.44.2 || ^12.4.1", + "php": "^8.2", + "phpstan/phpstan": "^2.1.11" + }, + "require-dev": { + "doctrine/coding-standard": "^13", + "laravel/framework": "^11.44.2 || ^12.7.2", + "mockery/mockery": "^1.6.12", + "nikic/php-parser": "^5.4", + "orchestra/canvas": "^v9.2.2 || ^10.0.1", + "orchestra/testbench-core": "^9.12.0 || ^10.1", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpunit/phpunit": "^10.5.35 || ^11.5.15" + }, + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Larastan\\Larastan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Can Vural", + "email": "can9119@gmail.com" + } + ], + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" + ], + "support": { + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/canvural", + "type": "github" + } + ], + "time": "2025-07-11T06:52:52+00:00" + }, { "name": "laravel/pint", "version": "v1.24.0", @@ -5464,6 +6298,67 @@ }, "time": "2025-07-10T18:09:32+00:00" }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-03-19T13:51:03+00:00" + }, { "name": "mockery/mockery", "version": "1.6.12", @@ -6312,22 +7207,80 @@ }, "type": "library", "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + }, + "time": "2025-07-13T07:04:09+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.22", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, - "time": "2025-07-13T07:04:09+00:00" + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-08-04T19:17:37+00:00" }, { "name": "phpunit/php-code-coverage", @@ -7538,222 +8491,594 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:35:50+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "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": "2024-07-03T05:10:34+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { - "name": "sebastian/type", - "version": "5.1.2", + "name": "symfony/http-kernel", + "version": "v7.3.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + "url": "https://github.com/symfony/http-kernel.git", + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "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-03-18T13:35:50+00:00" + "time": "2025-07-31T10:45:04+00:00" }, { - "name": "sebastian/version", - "version": "5.0.2", + "name": "symfony/mime", + "version": "v7.3.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "url": "https://github.com/symfony/mime.git", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, + "type": "library", "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "source": "https://github.com/symfony/mime/tree/v7.3.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "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": "2024-10-09T05:16:32+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { - "name": "staabm/side-effects-detector", - "version": "1.0.5", + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/staabm/side-effects-detector.git", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": "^7.4 || ^8.0" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, - "require-dev": { - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.6", - "phpunit/phpunit": "^9.6.21", - "symfony/var-dumper": "^5.4.43", - "tomasvotruba/type-coverage": "1.0.0", - "tomasvotruba/unused-public": "1.0.0" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { - "classmap": [ - "lib/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A static analysis tool to detect side effects in PHP code", + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", "keywords": [ - "static analysis" + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/staabm/side-effects-detector/issues", - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" }, "funding": [ { - "url": "https://github.com/staabm", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-10-20T05:08:20+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -7867,12 +9192,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/config/conduit.php b/config/conduit.php new file mode 100644 index 0000000..e1f931f --- /dev/null +++ b/config/conduit.php @@ -0,0 +1,51 @@ + env('CONDUIT_USER_AGENT', 'human'), + + /* + |-------------------------------------------------------------------------- + | Component Configuration + |-------------------------------------------------------------------------- + | + | Default settings for component scaffolding and management. + | + */ + 'components' => [ + 'github_username' => env('CONDUIT_GITHUB_USERNAME', 'S-H-I-T'), + 'namespace' => env('CONDUIT_NAMESPACE', 'App'), + 'author_name' => env('CONDUIT_AUTHOR_NAME', 'Your Name'), + 'author_email' => env('CONDUIT_AUTHOR_EMAIL', 'you@example.com'), + 'directory' => env('CONDUIT_COMPONENTS_DIR', '๐Ÿ’ฉ-components'), + ], + + /* + |-------------------------------------------------------------------------- + | Detection Settings + |-------------------------------------------------------------------------- + | + | Configuration for smart detection features. + | + */ + 'detection' => [ + 'os_name' => env('CONDUIT_OS_NAME', PHP_OS), + ], +]; diff --git a/config/database.php b/config/database.php index 2329c1d..31682eb 100644 --- a/config/database.php +++ b/config/database.php @@ -14,11 +14,11 @@ 'todo' => [ 'driver' => 'sqlite', - 'database' => env('HOME') . '/.conduit/todo.db', + 'database' => env('HOME').'/.conduit/todo.db', 'prefix' => '', 'foreign_key_constraints' => true, ], ], 'migrations' => 'migrations', -]; \ No newline at end of file +]; diff --git a/config/github.php b/config/github.php new file mode 100644 index 0000000..088ddd6 --- /dev/null +++ b/config/github.php @@ -0,0 +1,80 @@ + env('GITHUB_AUTH_METHOD', 'token'), // token, app, oauth + + /* + |-------------------------------------------------------------------------- + | Personal Access Token Authentication + |-------------------------------------------------------------------------- + | + | The simplest authentication method. Create a token at: + | https://github.com/settings/tokens + | + */ + 'token' => env('GITHUB_TOKEN'), + + /* + |-------------------------------------------------------------------------- + | GitHub App Authentication + |-------------------------------------------------------------------------- + | + | For advanced use cases with higher rate limits. + | + */ + 'app' => [ + 'id' => env('GITHUB_APP_ID'), + 'private_key' => env('GITHUB_APP_PRIVATE_KEY'), + 'installation_id' => env('GITHUB_APP_INSTALLATION_ID'), + ], + + /* + |-------------------------------------------------------------------------- + | OAuth App Authentication + |-------------------------------------------------------------------------- + | + | For user-specific authentication flows. + | + */ + 'oauth' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + ], + + /* + |-------------------------------------------------------------------------- + | API Configuration + |-------------------------------------------------------------------------- + */ + 'api' => [ + 'base_url' => env('GITHUB_API_URL', 'https://api.github.com'), + 'version' => env('GITHUB_API_VERSION', 'v3'), + 'timeout' => env('GITHUB_API_TIMEOUT', 30), + ], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | Configure rate limit handling behavior. + | + */ + 'rate_limit' => [ + 'auto_retry' => env('GITHUB_AUTO_RETRY', true), + 'max_retries' => env('GITHUB_MAX_RETRIES', 3), + ], +]; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..52dcbba --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + paths: + - app + - config + + level: 5 + + excludePaths: + - ๐Ÿ’ฉ-components/* + - vendor/* \ No newline at end of file From 3301ae0c5d650346dc2d75e10af09c0ef075454e Mon Sep 17 00:00:00 2001 From: Jordan Partridge Date: Fri, 8 Aug 2025 22:16:52 -0700 Subject: [PATCH 2/2] fix: Update GitHub Actions to use v4 of upload-artifact and cache --- .github/workflows/quality.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index e295b49..4978148 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -33,7 +33,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: ๐Ÿ’พ Cache composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }} @@ -50,7 +50,7 @@ jobs: - name: ๐Ÿ“ Upload Quality Report (if failed) if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: quality-report-php-${{ matrix.php-version }} path: | @@ -88,7 +88,7 @@ jobs: - name: ๐Ÿ“ค Upload Pint report if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pint-report path: pint-report.json @@ -143,7 +143,7 @@ jobs: - name: ๐Ÿ“Š Upload test results if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-php-${{ matrix.php-version }} path: tests/_output/