Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Concerns;
trait IsProvidedByFlare
{
public function solutionProvidedByName(): string
{
return 'Flare';
}
public function solutionProvidedByLink(): string
{
return 'https://flareapp.io';
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Facades\Artisan;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class GenerateAppKeySolution implements RunnableSolution
{
use IsProvidedByFlare;
public function getSolutionTitle(): string
{
return 'Your app key is missing';
}
public function getDocumentationLinks(): array
{
return [
'Laravel installation' => 'https://laravel.com/docs/master/installation#configuration',
];
}
public function getSolutionActionDescription(): string
{
return 'Generate your application encryption key using `php artisan key:generate`.';
}
public function getRunButtonText(): string
{
return 'Generate app key';
}
public function getSolutionDescription(): string
{
return $this->getSolutionActionDescription();
}
public function getRunParameters(): array
{
return [];
}
public function run(array $parameters = []): void
{
Artisan::call('key:generate');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Livewire\LivewireComponentsFinder;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class LivewireDiscoverSolution implements RunnableSolution
{
use IsProvidedByFlare;
protected string $customTitle;
public function __construct(string $customTitle = '')
{
$this->customTitle = $customTitle;
}
public function getSolutionTitle(): string
{
return $this->customTitle;
}
public function getSolutionDescription(): string
{
return 'You might have forgotten to discover your Livewire components.';
}
public function getDocumentationLinks(): array
{
return [
'Livewire: Artisan Commands' => 'https://laravel-livewire.com/docs/2.x/artisan-commands',
];
}
public function getRunParameters(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
return 'You can discover your Livewire components using `php artisan livewire:discover`.';
}
public function getRunButtonText(): string
{
return 'Run livewire:discover';
}
public function run(array $parameters = []): void
{
app(LivewireComponentsFinder::class)->build();
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class MakeViewVariableOptionalSolution implements RunnableSolution
{
use IsProvidedByFlare;
protected ?string $variableName;
protected ?string $viewFile;
public function __construct(?string $variableName = null, ?string $viewFile = null)
{
$this->variableName = $variableName;
$this->viewFile = $viewFile;
}
public function getSolutionTitle(): string
{
return "$$this->variableName is undefined";
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
$output = [
'Make the variable optional in the blade template.',
"Replace `{{ $$this->variableName }}` with `{{ $$this->variableName ?? '' }}`",
];
return implode(PHP_EOL, $output);
}
public function getRunButtonText(): string
{
return 'Make variable optional';
}
public function getSolutionDescription(): string
{
return $this->getSolutionActionDescription();
}
public function getRunParameters(): array
{
return [
'variableName' => $this->variableName,
'viewFile' => $this->viewFile,
];
}
/**
* @param array<string, mixed> $parameters
*
* @return bool
*/
public function isRunnable(array $parameters = []): bool
{
return $this->makeOptional($this->getRunParameters()) !== false;
}
/**
* @param array<string, string> $parameters
*
* @return void
*/
public function run(array $parameters = []): void
{
$output = $this->makeOptional($parameters);
if ($output !== false) {
file_put_contents($parameters['viewFile'], $output);
}
}
protected function isSafePath(string $path): bool
{
if (! Str::startsWith($path, ['/', './'])) {
return false;
}
if (! Str::endsWith($path, '.blade.php')) {
return false;
}
return true;
}
/**
* @param array<string, string> $parameters
*
* @return bool|string
*/
public function makeOptional(array $parameters = []): bool|string
{
if (! $this->isSafePath($parameters['viewFile'])) {
return false;
}
$originalContents = (string)file_get_contents($parameters['viewFile']);
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
$originalTokens = token_get_all(Blade::compileString($originalContents));
$newTokens = token_get_all(Blade::compileString($newContents));
$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
if ($expectedTokens !== $newTokens) {
return false;
}
return $newContents;
}
/**
* @param array<int, mixed> $originalTokens
* @param string $variableName
*
* @return array<int, mixed>
*/
protected function generateExpectedTokens(array $originalTokens, string $variableName): array
{
$expectedTokens = [];
foreach ($originalTokens as $token) {
$expectedTokens[] = $token;
if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_COALESCE, '??', $token[2]];
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];
}
}
return $expectedTokens;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Facades\Artisan;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class RunMigrationsSolution implements RunnableSolution
{
use IsProvidedByFlare;
protected string $customTitle;
public function __construct(string $customTitle = '')
{
$this->customTitle = $customTitle;
}
public function getSolutionTitle(): string
{
return $this->customTitle;
}
public function getSolutionDescription(): string
{
return 'You might have forgotten to run your database migrations.';
}
public function getDocumentationLinks(): array
{
return [
'Database: Running Migrations docs' => 'https://laravel.com/docs/master/migrations#running-migrations',
];
}
public function getRunParameters(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
return 'You can try to run your migrations using `php artisan migrate`.';
}
public function getRunButtonText(): string
{
return 'Run migrations';
}
public function run(array $parameters = []): void
{
Artisan::call('migrate');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestLivewireMethodNameSolution implements Solution
{
use IsProvidedByFlare;
public function __construct(
protected string $methodName,
protected string $componentClass,
protected string $suggested
) {
}
public function getSolutionTitle(): string
{
return "Possible typo `{$this->componentClass}::{$this->methodName}`";
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionDescription(): string
{
return "Did you mean `{$this->componentClass}::{$this->suggested}`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestLivewirePropertyNameSolution implements Solution
{
use IsProvidedByFlare;
public function __construct(
protected string $variableName,
protected string $componentClass,
protected string $suggested,
) {
}
public function getSolutionTitle(): string
{
return "Possible typo {$this->variableName}";
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionDescription(): string
{
return "Did you mean `$this->suggested`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestUsingCorrectDbNameSolution implements Solution
{
use IsProvidedByFlare;
public function getSolutionTitle(): string
{
return 'Database name seems incorrect';
}
public function getSolutionDescription(): string
{
$defaultDatabaseName = env('DB_DATABASE');
return "You're using the default database name `$defaultDatabaseName`. This database does not exist.\n\nEdit the `.env` file and use the correct database name in the `DB_DATABASE` key.";
}
/** @return array<string, string> */
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestUsingMariadbDatabaseSolution implements Solution
{
use IsProvidedByFlare;
public function getSolutionTitle(): string
{
return 'Database is not a MariaDB database';
}
public function getSolutionDescription(): string
{
return "Laravel 11 changed the default collation for MySQL and MariaDB. It seems you are trying to use the MariaDB collation `utf8mb4_uca1400_ai_ci` with a MySQL database.\n\nEdit the `.env` file and use the correct database in the `DB_CONNECTION` key.";
}
/** @return array<string, string> */
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestUsingMysql8DatabaseSolution implements Solution
{
use IsProvidedByFlare;
public function getSolutionTitle(): string
{
return 'Database is not a MySQL 8 database';
}
public function getSolutionDescription(): string
{
return "Laravel 11 changed the default collation for MySQL and MariaDB. It seems you are trying to use the MySQL 8 collation `utf8mb4_0900_ai_ci` with a MariaDB or MySQL 5.7 database.\n\nEdit the `.env` file and use the correct database in the `DB_CONNECTION` key.";
}
/** @return array<string, string> */
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Str;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class UseDefaultValetDbCredentialsSolution implements RunnableSolution
{
use IsProvidedByFlare;
public function getSolutionActionDescription(): string
{
return 'Pressing the button will change `DB_USER` and `DB_PASSWORD` in your `.env` file.';
}
public function getRunButtonText(): string
{
return 'Use default Valet credentials';
}
public function getSolutionTitle(): string
{
return 'Could not connect to database';
}
public function run(array $parameters = []): void
{
if (! file_exists(base_path('.env'))) {
return;
}
$this->ensureLineExists('DB_USERNAME', 'root');
$this->ensureLineExists('DB_PASSWORD', '');
}
protected function ensureLineExists(string $key, string $value): void
{
$envPath = base_path('.env');
$envLines = array_map(fn (string $envLine) => Str::startsWith($envLine, $key)
? "{$key}={$value}".PHP_EOL
: $envLine, file($envPath) ?: []);
file_put_contents($envPath, implode('', $envLines));
}
public function getRunParameters(): array
{
return [];
}
public function getDocumentationLinks(): array
{
return [
'Valet documentation' => 'https://laravel.com/docs/master/valet',
];
}
public function getSolutionDescription(): string
{
return 'You seem to be using Valet, but the .env file does not contain the right default database credentials.';
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\OpenAi;
use Psr\SimpleCache\CacheInterface;
class DummyCache implements CacheInterface
{
public function get(string $key, mixed $default = null): mixed
{
return null;
}
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool
{
return true;
}
public function delete(string $key): bool
{
return true;
}
public function clear(): bool
{
return true;
}
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
return [];
}
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool
{
return true;
}
public function deleteMultiple(iterable $keys): bool
{
return true;
}
public function has(string $key): bool
{
return false;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\OpenAi;
class OpenAiPromptViewModel
{
public function __construct(
protected string $file,
protected string $exceptionMessage,
protected string $exceptionClass,
protected string $snippet,
protected string $line,
protected string|null $applicationType = null,
) {
}
public function file(): string
{
return $this->file;
}
public function line(): string
{
return $this->line;
}
public function snippet(): string
{
return $this->snippet;
}
public function exceptionMessage(): string
{
return $this->exceptionMessage;
}
public function exceptionClass(): string
{
return $this->exceptionClass;
}
public function applicationType(): string|null
{
return $this->applicationType;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\OpenAi;
use OpenAI;
use Psr\SimpleCache\CacheInterface;
use Spatie\Backtrace\Backtrace;
use Spatie\Backtrace\Frame;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
use Spatie\ErrorSolutions\Support\AiPromptRenderer;
use Throwable;
class OpenAiSolution implements Solution
{
use IsProvidedByFlare;
public bool $aiGenerated = true;
protected string $prompt;
protected OpenAiSolutionResponse $openAiSolutionResponse;
public function __construct(
protected Throwable $throwable,
protected string $openAiKey,
protected CacheInterface|null $cache = null,
protected int|null $cacheTtlInSeconds = 60,
protected string|null $applicationType = null,
protected string|null $applicationPath = null,
protected string|null $openAiModel = null,
) {
$this->prompt = $this->generatePrompt();
$this->openAiSolutionResponse = $this->getAiSolution();
}
public function getSolutionTitle(): string
{
return 'AI Generated Solution';
}
public function getSolutionDescription(): string
{
return $this->openAiSolutionResponse->description();
}
public function getDocumentationLinks(): array
{
return $this->openAiSolutionResponse->links();
}
public function getAiSolution(): ?OpenAiSolutionResponse
{
$solution = $this->cache->get($this->getCacheKey());
if ($solution) {
return new OpenAiSolutionResponse($solution);
}
$solutionText = OpenAI::client($this->openAiKey)
->chat()
->create([
'model' => $this->getModel(),
'messages' => [['role' => 'user', 'content' => $this->prompt]],
'max_tokens' => 1000,
'temperature' => 0,
])->choices[0]->message->content;
$this->cache->set($this->getCacheKey(), $solutionText, $this->cacheTtlInSeconds);
return new OpenAiSolutionResponse($solutionText);
}
protected function getCacheKey(): string
{
$hash = sha1($this->prompt);
return "ignition-solution-{$hash}";
}
protected function generatePrompt(): string
{
$viewPath = __DIR__.'/../../../resources/views/aiPrompt.php';
$viewModel = new OpenAiPromptViewModel(
file: $this->throwable->getFile(),
exceptionMessage: $this->throwable->getMessage(),
exceptionClass: get_class($this->throwable),
snippet: $this->getApplicationFrame($this->throwable)->getSnippetAsString(15),
line: $this->throwable->getLine(),
applicationType: $this->applicationType,
);
return (new AiPromptRenderer())->renderAsString(
['viewModel' => $viewModel],
$viewPath,
);
}
protected function getModel(): string
{
return $this->openAiModel ?? 'gpt-3.5-turbo';
}
protected function getApplicationFrame(Throwable $throwable): ?Frame
{
$backtrace = Backtrace::createForThrowable($throwable);
if ($this->applicationPath) {
$backtrace->applicationPath($this->applicationPath);
}
$frames = $backtrace->frames();
return $frames[$backtrace->firstApplicationFrameIndex()] ?? null;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\OpenAi;
use Psr\SimpleCache\CacheInterface;
use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable;
use Throwable;
class OpenAiSolutionProvider implements HasSolutionsForThrowable
{
public function __construct(
protected string $openAiKey,
protected ?CacheInterface $cache = null,
protected int $cacheTtlInSeconds = 60 * 60,
protected string|null $applicationType = null,
protected string|null $applicationPath = null,
protected string $openAiModel = 'gpt-3.5-turbo',
) {
$this->cache ??= new DummyCache();
}
public function canSolve(Throwable $throwable): bool
{
return true;
}
public function getSolutions(Throwable $throwable): array
{
return [
new OpenAiSolution(
$throwable,
$this->openAiKey,
$this->cache,
$this->cacheTtlInSeconds,
$this->applicationType,
$this->applicationPath,
$this->openAiModel
),
];
}
public function applicationType(string $applicationType): self
{
$this->applicationType = $applicationType;
return $this;
}
public function applicationPath(string $applicationPath): self
{
$this->applicationPath = $applicationPath;
return $this;
}
public function useCache(CacheInterface $cache, int $cacheTtlInSeconds = 60 * 60): self
{
$this->cache = $cache;
$this->cacheTtlInSeconds = $cacheTtlInSeconds;
return $this;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\OpenAi;
use Illuminate\Support\Str;
class OpenAiSolutionResponse
{
protected string $rawText;
public function __construct(string $rawText)
{
$this->rawText = trim($rawText);
}
public function description(): string
{
return $this->between('FIX', 'ENDFIX', $this->rawText);
}
public function links(): array
{
$rawText = Str::finish($this->rawText, 'ENDLINKS');
$textLinks = $this->between('LINKS', 'ENDLINKS', $rawText);
$textLinks = explode(PHP_EOL, $textLinks);
$textLinks = array_map(function ($textLink) {
$textLink = str_replace('\\', '\\\\', $textLink);
$textLink = str_replace('\\\\\\', '\\\\', $textLink);
return json_decode($textLink, true);
}, $textLinks);
array_filter($textLinks);
$links = [];
foreach ($textLinks as $textLink) {
if (isset($textLink['title']) && isset($textLink['url'])) {
$links[$textLink['title']] = $textLink['url'];
}
}
return $links;
}
protected function between(string $start, string $end, string $text): string
{
$startPosition = strpos($text, $start);
if ($startPosition === false) {
return "";
}
$startPosition += strlen($start);
$endPosition = strpos($text, $end, $startPosition);
if ($endPosition === false) {
return "";
}
return trim(substr($text, $startPosition, $endPosition - $startPosition));
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Spatie\ErrorSolutions\Solutions;
use Illuminate\Contracts\Support\Arrayable;
use Spatie\ErrorSolutions\Contracts\Solution;
/** @implements Arrayable<string, array<string,string>|string|false> */
class SolutionTransformer implements Arrayable
{
protected Solution $solution;
public function __construct(Solution $solution)
{
$this->solution = $solution;
}
/** @return array<string, array<string,string>|string|false> */
public function toArray(): array
{
return [
'class' => get_class($this->solution),
'title' => $this->solution->getSolutionTitle(),
'links' => $this->solution->getDocumentationLinks(),
'description' => $this->solution->getSolutionDescription(),
'is_runnable' => false,
'ai_generated' => $this->solution->aiGenerated ?? false,
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Spatie\ErrorSolutions\Solutions;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestCorrectVariableNameSolution implements Solution
{
use IsProvidedByFlare;
protected ?string $variableName;
protected ?string $viewFile;
protected ?string $suggested;
public function __construct(?string $variableName = null, ?string $viewFile = null, ?string $suggested = null)
{
$this->variableName = $variableName;
$this->viewFile = $viewFile;
$this->suggested = $suggested;
}
public function getSolutionTitle(): string
{
return 'Possible typo $'.$this->variableName;
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionDescription(): string
{
return "Did you mean `$$this->suggested`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Spatie\ErrorSolutions\Solutions;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestImportSolution implements Solution
{
use IsProvidedByFlare;
protected string $class;
public function __construct(string $class)
{
$this->class = $class;
}
public function getSolutionTitle(): string
{
return 'A class import is missing';
}
public function getSolutionDescription(): string
{
return 'You have a missing class import. Try importing this class: `'.$this->class.'`.';
}
public function getDocumentationLinks(): array
{
return [];
}
}