Compare commits

...

2 commits

Author SHA1 Message Date
Benjamin Nussbaum
6d7968a7b0 feat: initial implementation of recipe catalog
Some checks are pending
tests / ci (push) Waiting to run
2025-09-01 23:56:42 +02:00
Benjamin Nussbaum
7434911275 chore: update dependencies 2025-09-01 19:16:57 +02:00
13 changed files with 763 additions and 165 deletions

View file

@ -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) {
</code-snippet>
=== 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
<code-snippet name="Pest Browser Test Example" lang="php">
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);
});
</code-snippet>
<code-snippet name="Pest Smoke Testing Example" lang="php">
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core

View file

@ -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) {
</code-snippet>
=== 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
<code-snippet name="Pest Browser Test Example" lang="php">
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);
});
</code-snippet>
<code-snippet name="Pest Smoke Testing Example" lang="php">
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core

View file

@ -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) {
</code-snippet>
=== 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
<code-snippet name="Pest Browser Test Example" lang="php">
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);
});
</code-snippet>
<code-snippet name="Pest Smoke Testing Example" lang="php">
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core

View file

@ -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) {
</code-snippet>
=== 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
<code-snippet name="Pest Browser Test Example" lang="php">
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);
});
</code-snippet>
<code-snippet name="Pest Smoke Testing Example" lang="php">
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core

View file

@ -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

View file

@ -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 = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
// 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.');

306
composer.lock generated
View file

@ -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",

View file

@ -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'),
];

View file

@ -0,0 +1,148 @@
<?php
use App\Services\PluginImportService;
use Livewire\Volt\Component;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Yaml\Yaml;
new class extends Component {
public array $catalogPlugins = [];
public string $installingPlugin = '';
public function mount(): void
{
$this->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 = '';
}
}
}; ?>
<div class="space-y-4">
@if(empty($catalogPlugins))
<div class="text-center py-8">
<flux:icon name="exclamation-triangle" class="mx-auto h-12 w-12 text-gray-400" />
<flux:heading class="mt-2">No plugins available</flux:heading>
<flux:subheading>Catalog is empty</flux:subheading>
</div>
@else
<div class="grid grid-cols-1 gap-4">
@error('installation')
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}" />
@enderror
@foreach($catalogPlugins as $plugin)
<div class="bg-white dark:bg-white/10 border border-zinc-200 dark:border-white/10 [:where(&)]:p-6 [:where(&)]:rounded-xl space-y-6">
<div class="flex items-start space-x-4">
@if($plugin['logo_url'])
<img src="{{ $plugin['logo_url'] }}" alt="{{ $plugin['name'] }}" class="w-12 h-12 rounded-lg object-cover">
@else
<div class="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
<flux:icon name="puzzle-piece" class="w-6 h-6 text-gray-400" />
</div>
@endif
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ $plugin['name'] }}</h3>
@if ($plugin['github'])
<p class="text-sm text-gray-500 dark:text-gray-400">by {{ $plugin['github'] }}</p>
@endif
</div>
<div class="flex items-center space-x-2">
@if($plugin['license'])
<flux:badge color="gray" size="sm">{{ $plugin['license'] }}</flux:badge>
@endif
@if($plugin['repo_url'])
<a href="{{ $plugin['repo_url'] }}" target="_blank" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<flux:icon name="github" class="w-5 h-5" />
</a>
@endif
</div>
</div>
@if($plugin['description'])
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">{{ $plugin['description'] }}</p>
@endif
<div class="mt-4 flex items-center space-x-3">
<flux:button
wire:click="installPlugin('{{ $plugin['id'] }}')"
variant="primary">
Install
</flux:button>
@if($plugin['learn_more_url'])
<flux:button
href="{{ $plugin['learn_more_url'] }}"
target="_blank"
variant="subtle">
Learn More
</flux:button>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>

View file

@ -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 {
<flux:button icon="chevron-down" variant="primary"></flux:button>
<flux:menu>
<flux:modal.trigger name="import-zip">
<flux:menu.item icon="archive-box">Import Recipe</flux:menu.item>
<flux:menu.item icon="archive-box">Import Recipe Archive</flux:menu.item>
</flux:modal.trigger>
<flux:modal.trigger name="import-from-catalog">
<flux:menu.item icon="book-open">Import from Catalog</flux:menu.item>
</flux:modal.trigger>
<flux:menu.item icon="beaker" wire:click="seedExamplePlugins">Seed Example Recipes</flux:menu.item>
</flux:menu>
@ -167,7 +168,7 @@ new class extends Component {
<form wire:submit="importZip">
<div class="mb-4">
<label for="zipFile" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">.zip Archive</label>
<flux:label for="zipFile">.zip Archive</flux:label>
<input
type="file"
wire:model="zipFile"
@ -175,7 +176,9 @@ new class extends Component {
accept=".zip"
class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 p-2.5"
/>
@error('zipFile') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
@error('zipFile')
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}" class="mt-2" />
@enderror
</div>
<div class="flex">
@ -186,6 +189,18 @@ new class extends Component {
</div>
</flux:modal>
<flux:modal name="import-from-catalog">
<div class="space-y-6">
<div>
<flux:heading size="lg">Import from Catalog
<flux:badge color="yellow" class="ml-2">Alpha</flux:badge>
</flux:heading>
<flux:subheading>Browse and install Recipes from the community. Add yours <a href="https://github.com/bnussbau/trmnl-recipe-catalog" class="underline" target="_blank">here</a>.</flux:subheading>
</div>
<livewire:catalog.index @plugin-installed="refreshPlugins" />
</div>
</flux:modal>
<flux:modal name="add-plugin" class="md:w-96">
<div class="space-y-6">
<div>

View file

@ -0,0 +1,102 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Livewire\Volt\Volt;
use Symfony\Component\Yaml\Yaml;
beforeEach(function () {
Cache::flush();
});
it('can render catalog component', function () {
// Mock empty catalog response
Http::fake([
config('app.catalog_url') => 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();
});

View file

@ -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 () {

View file

@ -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',