diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc index 9464f06..6e21fa7 100644 --- a/.cursor/rules/laravel-boost.mdc +++ b/.cursor/rules/laravel-boost.mdc @@ -11,7 +11,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.3.24 +- php - 8.4.12 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - livewire/flux (FLUXUI_FREE) - v2 @@ -19,7 +19,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/volt (VOLT) - v1 - larastan/larastan (LARASTAN) - v3 - laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 +- pestphp/pest (PEST) - v4 - tailwindcss (TAILWINDCSS) - v4 @@ -465,6 +465,53 @@ it('has emails', function (string $email) { +=== pest/v4 rules === + +## Pest 4 + +- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage. +- Browser testing is incredibly powerful and useful for this project. +- Browser tests should live in `tests/Browser/`. +- Use the `search-docs` tool for detailed guidance on utilizing these features. + +### Browser Testing +- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test. +- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test. +- If requested, test on multiple browsers (Chrome, Firefox, Safari). +- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints). +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging when appropriate. + +### Example Tests + + +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); // Visit on a real browser... + + $page->assertSee('Sign In') + ->assertNoJavascriptErrors() // or ->assertNoConsoleLogs() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!') + + Notification::assertSent(ResetPassword::class); +}); + + + + + +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); + + + === tailwindcss/core rules === ## Tailwind Core diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a331541..be2748d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.3.24 +- php - 8.4.12 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - livewire/flux (FLUXUI_FREE) - v2 @@ -16,7 +16,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/volt (VOLT) - v1 - larastan/larastan (LARASTAN) - v3 - laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 +- pestphp/pest (PEST) - v4 - tailwindcss (TAILWINDCSS) - v4 @@ -462,6 +462,53 @@ it('has emails', function (string $email) { +=== pest/v4 rules === + +## Pest 4 + +- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage. +- Browser testing is incredibly powerful and useful for this project. +- Browser tests should live in `tests/Browser/`. +- Use the `search-docs` tool for detailed guidance on utilizing these features. + +### Browser Testing +- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test. +- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test. +- If requested, test on multiple browsers (Chrome, Firefox, Safari). +- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints). +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging when appropriate. + +### Example Tests + + +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); // Visit on a real browser... + + $page->assertSee('Sign In') + ->assertNoJavascriptErrors() // or ->assertNoConsoleLogs() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!') + + Notification::assertSent(ResetPassword::class); +}); + + + + + +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); + + + === tailwindcss/core rules === ## Tailwind Core diff --git a/.junie/guidelines.md b/.junie/guidelines.md index a331541..be2748d 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.3.24 +- php - 8.4.12 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - livewire/flux (FLUXUI_FREE) - v2 @@ -16,7 +16,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/volt (VOLT) - v1 - larastan/larastan (LARASTAN) - v3 - laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 +- pestphp/pest (PEST) - v4 - tailwindcss (TAILWINDCSS) - v4 @@ -462,6 +462,53 @@ it('has emails', function (string $email) { +=== pest/v4 rules === + +## Pest 4 + +- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage. +- Browser testing is incredibly powerful and useful for this project. +- Browser tests should live in `tests/Browser/`. +- Use the `search-docs` tool for detailed guidance on utilizing these features. + +### Browser Testing +- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test. +- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test. +- If requested, test on multiple browsers (Chrome, Firefox, Safari). +- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints). +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging when appropriate. + +### Example Tests + + +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); // Visit on a real browser... + + $page->assertSee('Sign In') + ->assertNoJavascriptErrors() // or ->assertNoConsoleLogs() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!') + + Notification::assertSent(ResetPassword::class); +}); + + + + + +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); + + + === tailwindcss/core rules === ## Tailwind Core diff --git a/CLAUDE.md b/CLAUDE.md index a331541..be2748d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.3.24 +- php - 8.4.12 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - livewire/flux (FLUXUI_FREE) - v2 @@ -16,7 +16,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - livewire/volt (VOLT) - v1 - larastan/larastan (LARASTAN) - v3 - laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 +- pestphp/pest (PEST) - v4 - tailwindcss (TAILWINDCSS) - v4 @@ -462,6 +462,53 @@ it('has emails', function (string $email) { +=== pest/v4 rules === + +## Pest 4 + +- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage. +- Browser testing is incredibly powerful and useful for this project. +- Browser tests should live in `tests/Browser/`. +- Use the `search-docs` tool for detailed guidance on utilizing these features. + +### Browser Testing +- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test. +- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test. +- If requested, test on multiple browsers (Chrome, Firefox, Safari). +- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints). +- Switch color schemes (light/dark mode) when appropriate. +- Take screenshots or pause tests for debugging when appropriate. + +### Example Tests + + +it('may reset the password', function () { + Notification::fake(); + + $this->actingAs(User::factory()->create()); + + $page = visit('/sign-in'); // Visit on a real browser... + + $page->assertSee('Sign In') + ->assertNoJavascriptErrors() // or ->assertNoConsoleLogs() + ->click('Forgot Password?') + ->fill('email', 'nuno@laravel.com') + ->click('Send Reset Link') + ->assertSee('We have emailed your password reset link!') + + Notification::assertSent(ResetPassword::class); +}); + + + + + +$pages = visit(['/', '/about', '/contact']); + +$pages->assertNoJavascriptErrors()->assertNoConsoleLogs(); + + + === tailwindcss/core rules === ## Tailwind Core diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 7290381..3079ab7 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -236,11 +236,11 @@ class Plugin extends Model $template = preg_replace_callback( '/{%\s*for\s+(\w+)\s+in\s+([^|]+)\s*\|\s*([^}]+)%}/', function ($matches) { - $variableName = trim($matches[1]); - $collection = trim($matches[2]); - $filter = trim($matches[3]); - $tempVarName = '_temp_' . uniqid(); - + $variableName = mb_trim($matches[1]); + $collection = mb_trim($matches[2]); + $filter = mb_trim($matches[3]); + $tempVarName = '_temp_'.uniqid(); + return "{% assign {$tempVarName} = {$collection} | {$filter} %}{% for {$variableName} in {$tempVarName} %}"; }, $template diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index dbd8ec8..29b5688 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -7,6 +7,7 @@ use App\Models\User; use Exception; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -48,6 +49,130 @@ class PluginImportService // Find the required files (settings.yml and full.liquid/full.blade.php) $filePaths = $this->findRequiredFiles($tempDir); + // Validate that we found the required files + if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { + throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php + } + + // Parse settings.yml + $settingsYaml = File::get($filePaths['settingsYamlPath']); + $settings = Yaml::parse($settingsYaml); + + // Read full.liquid content + $fullLiquid = File::get($filePaths['fullLiquidPath']); + + // Prepend shared.liquid content if available + if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedLiquid = File::get($filePaths['sharedLiquidPath']); + $fullLiquid = $sharedLiquid."\n".$fullLiquid; + } + + $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + + // Check if the file ends with .liquid to set markup language + $markupLanguage = 'blade'; + if (pathinfo($filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } + + // Ensure custom_fields is properly formatted + if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) { + $settings['custom_fields'] = []; + } + + // Create configuration template with the custom fields + $configurationTemplate = [ + 'custom_fields' => $settings['custom_fields'], + ]; + + $plugin_updated = isset($settings['id']) + && Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists(); + // Create a new plugin + $plugin = Plugin::updateOrCreate( + [ + 'user_id' => $user->id, 'trmnlp_id' => $settings['id'] ?? Uuid::v7(), + ], + [ + 'user_id' => $user->id, + 'name' => $settings['name'] ?? 'Imported Plugin', + 'trmnlp_id' => $settings['id'] ?? Uuid::v7(), + 'data_stale_minutes' => $settings['refresh_interval'] ?? 15, + 'data_strategy' => $settings['strategy'] ?? 'static', + 'polling_url' => $settings['polling_url'] ?? null, + 'polling_verb' => $settings['polling_verb'] ?? 'get', + 'polling_header' => isset($settings['polling_headers']) + ? str_replace('=', ':', $settings['polling_headers']) + : null, + 'polling_body' => $settings['polling_body'] ?? null, + 'markup_language' => $markupLanguage, + 'render_markup' => $fullLiquid, + 'configuration_template' => $configurationTemplate, + 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), + ]); + + if (! $plugin_updated) { + // Extract default values from custom_fields and populate configuration + $configuration = []; + foreach ($settings['custom_fields'] as $field) { + if (isset($field['keyname']) && isset($field['default'])) { + $configuration[$field['keyname']] = $field['default']; + } + } + // set only if plugin is new + $plugin->update([ + 'configuration' => $configuration, + ]); + } + $plugin['trmnlp_yaml'] = $settingsYaml; + + return $plugin; + + } finally { + // Clean up temporary directory + Storage::deleteDirectory($tempDirName); + } + } + + /** + * Import a plugin from a ZIP URL + * + * @param string $zipUrl The URL to the ZIP file + * @param User $user The user importing the plugin + * @return Plugin The created plugin instance + * + * @throws Exception If the ZIP file is invalid or required files are missing + */ + public function importFromUrl(string $zipUrl, User $user): Plugin + { + // Download the ZIP file + $response = Http::timeout(60)->get($zipUrl); + + if (! $response->successful()) { + throw new Exception('Could not download the ZIP file from the provided URL.'); + } + + // Create a temporary file + $tempDirName = 'temp/'.uniqid('plugin_import_', true); + Storage::makeDirectory($tempDirName); + $tempDir = Storage::path($tempDirName); + $zipPath = $tempDir.'/plugin.zip'; + + // Save the downloaded content to a temporary file + File::put($zipPath, $response->body()); + + try { + // Extract the ZIP file using ZipArchive + $zip = new ZipArchive(); + if ($zip->open($zipPath) !== true) { + throw new Exception('Could not open the downloaded ZIP file.'); + } + + $zip->extractTo($tempDir); + $zip->close(); + + // Find the required files (settings.yml and full.liquid/full.blade.php) + $filePaths = $this->findRequiredFiles($tempDir); + // Validate that we found the required files if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.'); diff --git a/composer.lock b/composer.lock index 79f70e1..2ad26ef 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.356.5", + "version": "3.356.8", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5872ccb5100c4afb0dae3db0bd46636f63ae8147" + "reference": "3efa8c62c11fedb17b90f60b2d3a9f815b406e63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5872ccb5100c4afb0dae3db0bd46636f63ae8147", - "reference": "5872ccb5100c4afb0dae3db0bd46636f63ae8147", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3efa8c62c11fedb17b90f60b2d3a9f815b406e63", + "reference": "3efa8c62c11fedb17b90f60b2d3a9f815b406e63", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.356.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.356.8" }, - "time": "2025-08-26T18:05:04+00:00" + "time": "2025-08-29T18:06:18+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -1691,16 +1691,16 @@ }, { "name": "laravel/framework", - "version": "v12.26.2", + "version": "v12.26.4", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "56c5fc46cfb1005d0aaa82c7592d63edb776a787" + "reference": "085a367a32ba86fcfa647bfc796098ae6f795b09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/56c5fc46cfb1005d0aaa82c7592d63edb776a787", - "reference": "56c5fc46cfb1005d0aaa82c7592d63edb776a787", + "url": "https://api.github.com/repos/laravel/framework/zipball/085a367a32ba86fcfa647bfc796098ae6f795b09", + "reference": "085a367a32ba86fcfa647bfc796098ae6f795b09", "shasum": "" }, "require": { @@ -1740,8 +1740,8 @@ "symfony/http-kernel": "^7.2.0", "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.31", - "symfony/polyfill-php84": "^1.31", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", "symfony/polyfill-php85": "^1.33", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", @@ -1810,7 +1810,7 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.6.0", + "orchestra/testbench-core": "^10.6.3", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -1904,7 +1904,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-08-26T18:04:56+00:00" + "time": "2025-08-29T14:15:53+00:00" }, { "name": "laravel/prompts", @@ -4973,16 +4973,16 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", "shasum": "" }, "require": { @@ -5047,7 +5047,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.3.3" }, "funding": [ { @@ -5067,7 +5067,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "symfony/css-selector", @@ -5284,16 +5284,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", "shasum": "" }, "require": { @@ -5344,7 +5344,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" }, "funding": [ { @@ -5355,12 +5355,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-22T09:11:45+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5508,16 +5512,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", "shasum": "" }, "require": { @@ -5567,7 +5571,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" }, "funding": [ { @@ -5587,20 +5591,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-20T08:04:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" + "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", - "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b", + "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b", "shasum": "" }, "require": { @@ -5685,7 +5689,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.3" }, "funding": [ { @@ -5705,20 +5709,20 @@ "type": "tidelift" } ], - "time": "2025-07-31T10:45:04+00:00" + "time": "2025-08-29T08:23:45+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b" + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/d43e84d9522345f96ad6283d5dfccc8c1cfc299b", - "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575", + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575", "shasum": "" }, "require": { @@ -5769,7 +5773,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.2" + "source": "https://github.com/symfony/mailer/tree/v7.3.3" }, "funding": [ { @@ -5789,7 +5793,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/mime", @@ -6710,16 +6714,16 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", "shasum": "" }, "require": { @@ -6751,7 +6755,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.3.3" }, "funding": [ { @@ -6762,12 +6766,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-08-18T09:42:54+00:00" }, { "name": "symfony/routing", @@ -6939,16 +6947,16 @@ }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", "shasum": "" }, "require": { @@ -7006,7 +7014,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v7.3.3" }, "funding": [ { @@ -7026,20 +7034,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "symfony/translation", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90" + "reference": "e0837b4cbcef63c754d89a4806575cada743a38d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/81b48f4daa96272efcce9c7a6c4b58e629df3c90", - "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90", + "url": "https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d", + "reference": "e0837b4cbcef63c754d89a4806575cada743a38d", "shasum": "" }, "require": { @@ -7106,7 +7114,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.2" + "source": "https://github.com/symfony/translation/tree/v7.3.3" }, "funding": [ { @@ -7126,7 +7134,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:31:46+00:00" + "time": "2025-08-01T21:02:37+00:00" }, { "name": "symfony/translation-contracts", @@ -7282,16 +7290,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "53205bea27450dc5c65377518b3275e126d45e75" + "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", - "reference": "53205bea27450dc5c65377518b3275e126d45e75", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", "shasum": "" }, "require": { @@ -7345,7 +7353,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" }, "funding": [ { @@ -7365,20 +7373,20 @@ "type": "tidelift" } ], - "time": "2025-07-29T20:02:46+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "05b3e90654c097817325d6abd284f7938b05f467" + "reference": "d4dfcd2a822cbedd7612eb6fbd260e46f87b7137" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/05b3e90654c097817325d6abd284f7938b05f467", - "reference": "05b3e90654c097817325d6abd284f7938b05f467", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/d4dfcd2a822cbedd7612eb6fbd260e46f87b7137", + "reference": "d4dfcd2a822cbedd7612eb6fbd260e46f87b7137", "shasum": "" }, "require": { @@ -7426,7 +7434,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.2" + "source": "https://github.com/symfony/var-exporter/tree/v7.3.3" }, "funding": [ { @@ -7446,20 +7454,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-18T13:10:53+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" + "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", - "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", + "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", + "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", "shasum": "" }, "require": { @@ -7502,7 +7510,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.2" + "source": "https://github.com/symfony/yaml/tree/v7.3.3" }, "funding": [ { @@ -7522,7 +7530,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-27T11:34:33+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -7885,16 +7893,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.11.2", + "version": "v7.12.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "521aa381c212816d0dc2f04f1532a5831969cb5e" + "reference": "6a34ddb12a3bd5bd07d831ce95f111087f3bcbd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/521aa381c212816d0dc2f04f1532a5831969cb5e", - "reference": "521aa381c212816d0dc2f04f1532a5831969cb5e", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/6a34ddb12a3bd5bd07d831ce95f111087f3bcbd8", + "reference": "6a34ddb12a3bd5bd07d831ce95f111087f3bcbd8", "shasum": "" }, "require": { @@ -7904,11 +7912,11 @@ "ext-simplexml": "*", "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.3.0 || ~8.4.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", "phpunit/php-code-coverage": "^12.3.2", "phpunit/php-file-iterator": "^6", "phpunit/php-timer": "^8", - "phpunit/phpunit": "^12.3.5", + "phpunit/phpunit": "^12.3.6", "sebastian/environment": "^8.0.3", "symfony/console": "^6.4.20 || ^7.3.2", "symfony/process": "^6.4.20 || ^7.3.0" @@ -7963,7 +7971,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.11.2" + "source": "https://github.com/paratestphp/paratest/tree/v7.12.0" }, "funding": [ { @@ -7975,7 +7983,7 @@ "type": "paypal" } ], - "time": "2025-08-19T09:24:27+00:00" + "time": "2025-08-29T05:28:31+00:00" }, { "name": "doctrine/deprecations", @@ -8463,16 +8471,16 @@ }, { "name": "laravel/boost", - "version": "v1.0.18", + "version": "v1.0.20", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "df2a62b5864759ea8cce8a4b7575b657e9c7d4ab" + "reference": "c2ac67ce42c39ffe6c3c073c9202d54a96eaa5b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/df2a62b5864759ea8cce8a4b7575b657e9c7d4ab", - "reference": "df2a62b5864759ea8cce8a4b7575b657e9c7d4ab", + "url": "https://api.github.com/repos/laravel/boost/zipball/c2ac67ce42c39ffe6c3c073c9202d54a96eaa5b5", + "reference": "c2ac67ce42c39ffe6c3c073c9202d54a96eaa5b5", "shasum": "" }, "require": { @@ -8481,13 +8489,13 @@ "illuminate/contracts": "^10.0|^11.0|^12.0", "illuminate/routing": "^10.0|^11.0|^12.0", "illuminate/support": "^10.0|^11.0|^12.0", - "laravel/mcp": "^0.1.0", + "laravel/mcp": "^0.1.1", "laravel/prompts": "^0.1.9|^0.3", - "laravel/roster": "^0.2", - "php": "^8.1|^8.2" + "laravel/roster": "^0.2.4", + "php": "^8.1" }, "require-dev": { - "laravel/pint": "^1.14|^1.23", + "laravel/pint": "^1.14", "mockery/mockery": "^1.6", "orchestra/testbench": "^8.22.0|^9.0|^10.0", "pestphp/pest": "^2.0|^3.0", @@ -8524,7 +8532,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-08-16T09:10:03+00:00" + "time": "2025-08-28T14:46:17+00:00" }, { "name": "laravel/mcp", @@ -8740,16 +8748,16 @@ }, { "name": "laravel/roster", - "version": "v0.2.3", + "version": "v0.2.5", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "caeed7609b02c00c3f1efec52812d8d87c5d4096" + "reference": "0252fa419733c61b3ebeba8e4e2b9ad2a63f3a17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/caeed7609b02c00c3f1efec52812d8d87c5d4096", - "reference": "caeed7609b02c00c3f1efec52812d8d87c5d4096", + "url": "https://api.github.com/repos/laravel/roster/zipball/0252fa419733c61b3ebeba8e4e2b9ad2a63f3a17", + "reference": "0252fa419733c61b3ebeba8e4e2b9ad2a63f3a17", "shasum": "" }, "require": { @@ -8797,20 +8805,20 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2025-08-13T15:00:25+00:00" + "time": "2025-08-29T07:47:42+00:00" }, { "name": "laravel/sail", - "version": "v1.44.0", + "version": "v1.45.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe" + "reference": "019a2933ff4a9199f098d4259713f9bc266a874e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", - "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "url": "https://api.github.com/repos/laravel/sail/zipball/019a2933ff4a9199f098d4259713f9bc266a874e", + "reference": "019a2933ff4a9199f098d4259713f9bc266a874e", "shasum": "" }, "require": { @@ -8860,7 +8868,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-07-04T16:17:06+00:00" + "time": "2025-08-25T19:28:31+00:00" }, { "name": "mockery/mockery", @@ -9106,16 +9114,16 @@ }, { "name": "pestphp/pest", - "version": "v4.0.3", + "version": "v4.0.4", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "e54e4a0178889209a928f7bee63286149d4eb707" + "reference": "47fb1d77631d608022cc7af96cac90ac741c8394" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/e54e4a0178889209a928f7bee63286149d4eb707", - "reference": "e54e4a0178889209a928f7bee63286149d4eb707", + "url": "https://api.github.com/repos/pestphp/pest/zipball/47fb1d77631d608022cc7af96cac90ac741c8394", + "reference": "47fb1d77631d608022cc7af96cac90ac741c8394", "shasum": "" }, "require": { @@ -9127,12 +9135,12 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.0.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.3.6", + "phpunit/phpunit": "^12.3.7", "symfony/process": "^7.3.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.3.6", + "phpunit/phpunit": ">12.3.7", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, @@ -9206,7 +9214,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.0.3" + "source": "https://github.com/pestphp/pest/tree/v4.0.4" }, "funding": [ { @@ -9218,7 +9226,7 @@ "type": "github" } ], - "time": "2025-08-24T14:17:23+00:00" + "time": "2025-08-28T18:19:42+00:00" }, { "name": "pestphp/pest-plugin", @@ -9930,16 +9938,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", "shasum": "" }, "require": { @@ -9971,9 +9979,9 @@ "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" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "time": "2025-07-13T07:04:09+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { "name": "phpstan/phpstan", @@ -10035,34 +10043,34 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.3.2", + "version": "12.3.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "086553c5b2e0e1e20293d782d788ab768202b621" + "reference": "96dc0466673e215bf5536301039017f03cd45c6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/086553c5b2e0e1e20293d782d788ab768202b621", - "reference": "086553c5b2e0e1e20293d782d788ab768202b621", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/96dc0466673e215bf5536301039017f03cd45c6b", + "reference": "96dc0466673e215bf5536301039017f03cd45c6b", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.6.1", "php": ">=8.3", "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0", + "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^12.1" + "phpunit/phpunit": "^12.3.7" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -10100,7 +10108,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.2" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.5" }, "funding": [ { @@ -10120,7 +10128,7 @@ "type": "tidelift" } ], - "time": "2025-07-29T06:19:24+00:00" + "time": "2025-09-01T08:07:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10369,16 +10377,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.3.6", + "version": "12.3.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a2cab3224f687150ac2f3cc13d99b64ba1e1d088" + "reference": "b8fa997c49682979ad6bfaa0d7fb25f54954965e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2cab3224f687150ac2f3cc13d99b64ba1e1d088", - "reference": "a2cab3224f687150ac2f3cc13d99b64ba1e1d088", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b8fa997c49682979ad6bfaa0d7fb25f54954965e", + "reference": "b8fa997c49682979ad6bfaa0d7fb25f54954965e", "shasum": "" }, "require": { @@ -10392,7 +10400,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.3.2", + "phpunit/php-code-coverage": "^12.3.3", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", @@ -10446,7 +10454,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.7" }, "funding": [ { @@ -10470,7 +10478,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:43:23+00:00" + "time": "2025-08-28T05:15:46+00:00" }, { "name": "sebastian/cli-parser", @@ -10902,16 +10910,16 @@ }, { "name": "sebastian/global-state", - "version": "8.0.0", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", - "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { @@ -10952,15 +10960,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2025-02-07T04:56:59+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", diff --git a/config/app.php b/config/app.php index 98eaee9..73bcaaf 100644 --- a/config/app.php +++ b/config/app.php @@ -152,4 +152,5 @@ return [ 'version' => env('APP_VERSION', null), + 'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'), ]; diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php new file mode 100644 index 0000000..4725e68 --- /dev/null +++ b/resources/views/livewire/catalog/index.blade.php @@ -0,0 +1,148 @@ +loadCatalogPlugins(); + } + + private function loadCatalogPlugins(): void + { + $catalogUrl = config('app.catalog_url'); + + $this->catalogPlugins = Cache::remember('catalog_plugins', 43200, function () use ($catalogUrl) { + try { + $response = Http::get($catalogUrl); + $catalogContent = $response->body(); + $catalog = Yaml::parse($catalogContent); + + return collect($catalog)->map(function ($plugin, $key) { + return [ + 'id' => $key, + 'name' => $plugin['name'] ?? 'Unknown Plugin', + 'description' => $plugin['author_bio']['description'] ?? '', + 'author' => $plugin['author']['name'] ?? 'Unknown Author', + 'github' => $plugin['author']['github'] ?? null, + 'license' => $plugin['license'] ?? null, + 'zip_url' => $plugin['trmnlp']['zip_url'] ?? null, + 'repo_url' => $plugin['trmnlp']['repo'] ?? null, + 'logo_url' => $plugin['logo_url'] ?? null, + 'screenshot_url' => $plugin['screenshot_url'] ?? null, + 'learn_more_url' => $plugin['author_bio']['learn_more_url'] ?? null, + ]; + })->toArray(); + } catch (\Exception $e) { + Log::error('Failed to load catalog from URL: ' . $e->getMessage()); + return []; + } + }); + } + + public function installPlugin(string $pluginId, PluginImportService $pluginImportService): void + { + abort_unless(auth()->user() !== null, 403); + + $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); + + if (!$plugin || !$plugin['zip_url']) { + $this->addError('installation', 'Plugin not found or no download URL available.'); + return; + } + + $this->installingPlugin = $pluginId; + + try { + $importedPlugin = $pluginImportService->importFromUrl($plugin['zip_url'], auth()->user()); + + $this->dispatch('plugin-installed'); + Flux::modal('import-from-catalog')->close(); + + } catch (\Exception $e) { + $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } finally { + $this->installingPlugin = ''; + } + } +}; ?> + +
+ @if(empty($catalogPlugins)) +
+ + No plugins available + Catalog is empty +
+ @else +
+ @error('installation') + + @enderror + + @foreach($catalogPlugins as $plugin) +
+
+ @if($plugin['logo_url']) + {{ $plugin['name'] }} + @else +
+ +
+ @endif + +
+
+
+

{{ $plugin['name'] }}

+ @if ($plugin['github']) +

by {{ $plugin['github'] }}

+ @endif +
+
+ @if($plugin['license']) + {{ $plugin['license'] }} + @endif + @if($plugin['repo_url']) + + + + @endif +
+
+ + @if($plugin['description']) +

{{ $plugin['description'] }}

+ @endif + +
+ + Install + + + @if($plugin['learn_more_url']) + + Learn More + + @endif +
+
+
+
+ @endforeach +
+ @endif +
diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index 9a5dd69..828e051 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -36,7 +36,7 @@ new class extends Component { 'polling_body' => 'nullable|string', ]; - private function refreshPlugins(): void + public function refreshPlugins(): void { $userPlugins = auth()->user()?->plugins?->map(function ($plugin) { return $plugin->toArray(); @@ -96,10 +96,8 @@ new class extends Component { $this->reset(['zipFile']); Flux::modal('import-zip')->close(); - $this->dispatch('notify', ['type' => 'success', 'message' => 'Plugin imported successfully!']); - } catch (\Exception $e) { - $this->dispatch('notify', ['type' => 'error', 'message' => 'Error importing plugin: ' . $e->getMessage()]); + $this->addError('zipFile', 'Error installing plugin: ' . $e->getMessage()); } } @@ -120,7 +118,10 @@ new class extends Component { - Import Recipe + Import Recipe Archive + + + Import from Catalog Seed Example Recipes @@ -167,7 +168,7 @@ new class extends Component {
- + .zip Archive - @error('zipFile') {{ $message }} @enderror + @error('zipFile') + + @enderror
@@ -186,6 +189,18 @@ new class extends Component {
+ +
+
+ Import from Catalog + Alpha + + Browse and install Recipes from the community. Add yours here. +
+ +
+
+
diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php new file mode 100644 index 0000000..7defd78 --- /dev/null +++ b/tests/Feature/Livewire/Catalog/IndexTest.php @@ -0,0 +1,102 @@ + Http::response('', 200), + ]); + + $component = Volt::test('catalog.index'); + + $component->assertSee('No plugins available'); +}); + +it('loads plugins from catalog URL', function () { + // Clear cache first to ensure fresh data + Cache::forget('catalog_plugins'); + + // Mock the HTTP response for the catalog URL + $catalogData = [ + 'test-plugin' => [ + 'name' => 'Test Plugin', + 'author' => ['name' => 'Test Author', 'github' => 'testuser'], + 'author_bio' => [ + 'description' => 'A test plugin', + 'learn_more_url' => 'https://example.com', + ], + 'license' => 'MIT', + 'trmnlp' => [ + 'zip_url' => 'https://example.com/plugin.zip', + ], + 'logo_url' => 'https://example.com/logo.png', + ], + ]; + + $yamlContent = Yaml::dump($catalogData); + + // Override the default mock with specific data + Http::fake([ + config('app.catalog_url') => Http::response($yamlContent, 200), + ]); + + $component = Volt::test('catalog.index'); + + $component->assertSee('Test Plugin'); + $component->assertSee('testuser'); + $component->assertSee('A test plugin'); + $component->assertSee('MIT'); +}); + +it('shows error when plugin not found', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $component = Volt::test('catalog.index'); + + $component->call('installPlugin', 'non-existent-plugin'); + + // The component should dispatch an error notification + $component->assertHasErrors(); +}); + +it('shows error when zip_url is missing', function () { + $user = User::factory()->create(); + + // Mock the HTTP response for the catalog URL without zip_url + $catalogData = [ + 'test-plugin' => [ + 'name' => 'Test Plugin', + 'author' => ['name' => 'Test Author'], + 'author_bio' => ['description' => 'A test plugin'], + 'license' => 'MIT', + 'trmnlp' => [], + ], + ]; + + $yamlContent = Yaml::dump($catalogData); + + Http::fake([ + config('app.catalog_url') => Http::response($yamlContent, 200), + ]); + + $this->actingAs($user); + + $component = Volt::test('catalog.index'); + + $component->call('installPlugin', 'test-plugin'); + + // The component should dispatch an error notification + $component->assertHasErrors(); + +}); diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php index 9aeda6e..25325d2 100644 --- a/tests/Feature/PluginImportTest.php +++ b/tests/Feature/PluginImportTest.php @@ -94,7 +94,7 @@ it('throws exception for missing required files', function () { $pluginImportService = new PluginImportService(); expect(fn () => $pluginImportService->importFromZip($zipFile, $user)) - ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.'); + ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); }); it('sets default values when settings are missing', function () { diff --git a/tests/Feature/PluginLiquidWhereFilterTest.php b/tests/Feature/PluginLiquidWhereFilterTest.php index 22a2fa5..c165109 100644 --- a/tests/Feature/PluginLiquidWhereFilterTest.php +++ b/tests/Feature/PluginLiquidWhereFilterTest.php @@ -12,7 +12,6 @@ use App\Models\Plugin; * to: * {% assign _temp_xxx = collection | filter: "key", "value" %}{% for item in _temp_xxx %} */ - test('where filter works when assigned to variable first', function () { $plugin = Plugin::factory()->create([ 'markup_language' => 'liquid',