diff --git a/.gitignore b/.gitignore index 675f1d1f..1e9fe904 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ demo/ .build-cache.json .bashatimerc .wakatime-project +release.zip \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/CheckInterface.php b/app/BlueprintFramework/Doctor/CheckInterface.php new file mode 100644 index 00000000..0f320845 --- /dev/null +++ b/app/BlueprintFramework/Doctor/CheckInterface.php @@ -0,0 +1,10 @@ + */ + private array $rows = []; + + /** @var Issue[] */ + private array $issues = []; + + public function __construct( + public readonly string $name, + public readonly string $title, + ) { + } + + public function row(string $label, string $value, ?Severity $status = null): self + { + $this->rows[] = ['label' => $label, 'value' => $value, 'status' => $status]; + return $this; + } + + public function issue(Issue $issue): self + { + $this->issues[] = $issue; + return $this; + } + + public function ok(string $label, string $value): self + { + return $this->row($label, $value, Severity::INFO); + } + + public function warn(string $label, string $value): self + { + return $this->row($label, $value, Severity::WARNING); + } + + public function err(string $label, string $value): self + { + return $this->row($label, $value, Severity::ERROR); + } + + public function rows(): array + { + return $this->rows; + } + + /** @return Issue[] */ + public function issues(): array + { + return $this->issues; + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'title' => $this->title, + 'rows' => array_map(fn ($r) => [ + 'label' => $r['label'], + 'value' => $r['value'], + 'status' => $r['status']?->value, + ], $this->rows), + 'issues' => array_map(fn (Issue $i) => $i->toArray(), $this->issues), + ]; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/BlueprintCheck.php b/app/BlueprintFramework/Doctor/Checks/BlueprintCheck.php new file mode 100644 index 00000000..da4ed35e --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/BlueprintCheck.php @@ -0,0 +1,70 @@ +name(), $this->title()); + + $version = $this->placeholder->version(); + if ($version === 'unknown' || $version === '') { + $r->err('Version', 'unknown'); + $r->issue(new Issue( + Severity::CRITICAL, + 'Blueprint version could not be determined', + 'The BlueprintPlaceholderService did not return a version.', + 'Rerun the installer: blueprint -rerun-install', + )); + } else { + $r->ok('Version', $version); + } + + $isInstalled = file_exists(base_path('.blueprint/extensions/blueprint/private/db/is_installed')); + if ($isInstalled) { + $r->ok('Installation', 'Valid'); + } else { + $r->err('Installation', 'Incomplete'); + $r->issue(new Issue( + Severity::ERROR, + 'Blueprint installation incomplete', + 'The is_installed marker file is missing.', + 'Run: blueprint -rerun-install', + true, + )); + } + + $seeded = $this->blueprint->dbGet('blueprint', 'internal:seed', false); + if ($seeded) { + $r->ok('Configuration', 'Valid'); + } else { + $r->warn('Configuration', 'Not seeded'); + $r->issue(new Issue( + Severity::WARNING, + 'Blueprint configuration not seeded', + 'Database seed records are missing.', + 'Run: php artisan db:seed --class=BlueprintSeeder --force', + true, + )); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/CacheCheck.php b/app/BlueprintFramework/Doctor/Checks/CacheCheck.php new file mode 100644 index 00000000..15e6c81e --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/CacheCheck.php @@ -0,0 +1,57 @@ +name(), $this->title()); + + $cache = $this->blueprint->dbGet('blueprint', 'internal:cache', null); + if ($cache !== null) { + $r->ok('Blueprint cache', "v={$cache}"); + } else { + $r->warn('Blueprint cache', 'Missing'); + $r->issue(new Issue( + Severity::WARNING, + 'Blueprint cache not initialised', + 'Extensions use the cache value to bust stylesheet/script cache.', + 'Run: php artisan bp:cache', + true, + )); + } + + $configCache = base_path('bootstrap/cache/config.php'); + $envFile = base_path('.env'); + if (file_exists($configCache) && file_exists($envFile)) { + if (filemtime($configCache) < filemtime($envFile)) { + $r->warn('Config cache', 'Stale'); + $r->issue(new Issue( + Severity::WARNING, + 'Configuration cache is stale', + '.env was modified after the config cache was built.', + 'Run: php artisan config:clear && php artisan config:cache', + true, + )); + } else { + $r->ok('Config cache', 'Fresh'); + } + } else { + $r->ok('Config cache', 'Not cached'); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/ConfigurationCheck.php b/app/BlueprintFramework/Doctor/Checks/ConfigurationCheck.php new file mode 100644 index 00000000..386278b1 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/ConfigurationCheck.php @@ -0,0 +1,60 @@ +name(), $this->title()); + + $env = base_path('.env'); + if (file_exists($env)) { + $r->ok('.env', 'Present'); + } else { + $r->err('.env', 'Missing'); + $r->issue(new Issue( + Severity::CRITICAL, + '.env file missing', + 'The Pterodactyl environment file is missing.', + 'Restore .env from .env.example.', + )); + } + + $key = config('app.key'); + if ($key && $key !== 'base64:') { + $r->ok('APP_KEY', 'Set'); + } else { + $r->err('APP_KEY', 'Not set'); + $r->issue(new Issue( + Severity::CRITICAL, + 'APP_KEY not set', + 'Pterodactyl cannot encrypt sessions.', + 'Run: php artisan key:generate', + )); + } + + $url = config('app.url'); + if ($url) { + $r->ok('APP_URL', (string) $url); + } else { + $r->warn('APP_URL', 'Not set'); + $r->issue(new Issue( + Severity::WARNING, + 'APP_URL not set', + 'URLs generated by Blueprint may be wrong.', + 'Set APP_URL in .env.', + )); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/DatabaseCheck.php b/app/BlueprintFramework/Doctor/Checks/DatabaseCheck.php new file mode 100644 index 00000000..2b06a3dc --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/DatabaseCheck.php @@ -0,0 +1,91 @@ +name(), $this->title()); + + try { + DB::connection()->getPdo(); + $r->ok('Connection', 'OK'); + } catch (\Throwable $e) { + $r->err('Connection', 'Failed'); + $r->issue(new Issue( + Severity::CRITICAL, + 'Database connection failed', + $e->getMessage(), + 'Check your .env database credentials.', + )); + return $r; + } + + $required = ['settings', 'extension_cached_metadata']; + $missing = []; + foreach ($required as $table) { + if (!Schema::hasTable($table)) { + $missing[] = $table; + } + } + + if (empty($missing)) { + $r->ok('Schema', 'OK'); + } else { + $r->err('Schema', 'Missing: ' . implode(', ', $missing)); + $r->issue(new Issue( + Severity::ERROR, + 'Required database tables missing', + 'Missing: ' . implode(', ', $missing), + 'Run: php artisan migrate --force', + true, + )); + } + + try { + $pending = $this->pendingMigrationCount(); + if ($pending === 0) { + $r->ok('Migrations', 'Up to date'); + } else { + $r->warn('Migrations', "{$pending} pending"); + $r->issue(new Issue( + Severity::WARNING, + 'Pending database migrations', + "{$pending} Laravel migrations have not been run.", + 'Run: php artisan migrate --force', + true, + )); + } + } catch (\Throwable $e) { + $r->warn('Migrations', 'unable to determine'); + } + + return $r; + } + + private function pendingMigrationCount(): int + { + try { + $migrator = app('migrator'); + $ran = $migrator->getRepository()->getRan(); + $files = array_map( + fn ($f) => basename($f), + array_keys($migrator->getMigrationFiles([database_path('migrations')])) + ); + return count(array_diff($files, $ran)); + } catch (\Throwable $e) { + return 0; + } + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/EnvironmentCheck.php b/app/BlueprintFramework/Doctor/Checks/EnvironmentCheck.php new file mode 100644 index 00000000..c391093f --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/EnvironmentCheck.php @@ -0,0 +1,67 @@ +name(), $this->title()); + + $php = PHP_VERSION; + if (version_compare($php, '8.1.0', '>=')) { + $r->ok('PHP', $php); + } else { + $r->err('PHP', $php); + $r->issue(new Issue( + Severity::ERROR, + 'Unsupported PHP version', + "Blueprint requires PHP >= 8.1, found {$php}.", + 'Upgrade PHP to 8.1 or newer.', + )); + } + + $node = $this->capture('node -v'); + if ($node) { + $major = (int) ltrim(explode('.', trim($node))[0], 'v'); + if ($major >= 22) { + $r->ok('Node', trim($node)); + } else { + $r->warn('Node', trim($node)); + $r->issue(new Issue( + Severity::WARNING, + 'Outdated Node.js', + "Blueprint requires Node >= 22, found {$node}.", + 'Upgrade Node.js.', + )); + } + } else { + $r->warn('Node', 'not found'); + $r->issue(new Issue( + Severity::WARNING, + 'Node.js not found', + 'Node.js is required for building panel assets.', + 'Install Node.js >= 22.', + )); + } + + $yarn = $this->capture('yarn -v'); + $r->row('Yarn', $yarn ?: 'not found', $yarn ? Severity::INFO : Severity::WARNING); + + return $r; + } + + private function capture(string $cmd): string + { + $out = @shell_exec($cmd . ' 2>/dev/null'); + return $out ? trim((string) $out) : ''; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php b/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php new file mode 100644 index 00000000..b9db2fce --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php @@ -0,0 +1,150 @@ +name(), $this->title()); + + $current = $this->placeholder->version(); + $installed = array_filter($this->blueprint->extensions()); + + if (empty($installed)) { + $r->row('Installed', 'None', Severity::INFO); + return $r; + } + + foreach ($installed as $id) { + $base = base_path(".blueprint/extensions/{$id}"); + $manifest = "{$base}/private/.store/conf.yml"; + + if (!is_dir($base)) { + $r->err($id, 'Directory missing'); + $r->issue(new Issue( + Severity::ERROR, + "Extension '{$id}' directory missing", + "Registered in installed_extensions but no folder at {$base}.", + "Reinstall the extension or remove its entry.", + )); + continue; + } + + if (!file_exists($manifest)) { + $r->err($id, 'Invalid manifest'); + $r->issue(new Issue( + Severity::ERROR, + "Extension '{$id}' has no manifest", + "Expected manifest at {$manifest}.", + "Reinstall the extension.", + )); + continue; + } + + try { + $conf = Yaml::parseFile($manifest); + } catch (\Throwable $e) { + $r->err($id, 'Unreadable manifest'); + $r->issue(new Issue( + Severity::ERROR, + "Extension '{$id}' manifest unreadable", + $e->getMessage(), + )); + continue; + } + + $info = $conf['info'] ?? []; + $version = $info['version'] ?? 'unknown'; + $target = $info['target'] ?? ''; + + $compatible = true; + $reason = ''; + if ($target !== '' && $current !== 'rolling' && $target !== $current) { + if (version_compare($target, $current, '<')) { + $compatible = false; + $reason = "targets {$target}, running {$current}"; + } + } + + if ($compatible) { + $r->ok($id, $version); + } else { + $r->warn($id, $version); + $r->issue(new Issue( + Severity::WARNING, + "Extension '{$id}' may be incompatible", + $reason, + "Update '{$id}' or run with --force.", + false, + $id, + )); + } + + $requires = $conf['requires'] ?? []; + foreach ((array) $requires as $dep => $constraint) { + if (!is_string($dep)) { + continue; + } + if (str_starts_with($dep, 'blueprint/')) { + $satisfied = $this->satisfiesBlueprintConstraint((string) $constraint, $current); + if (!$satisfied) { + $r->issue(new Issue( + Severity::ERROR, + "Extension '{$id}' requires {$dep} {$constraint}", + "Current Blueprint version is {$current}.", + "Update Blueprint or the extension.", + false, + $id, + )); + } + } else { + + $depId = str_replace('extension/', '', $dep); + if (!$this->blueprint->extension($depId)) { + $r->issue(new Issue( + Severity::ERROR, + "Extension '{$id}' is missing a dependency", + "Requires {$dep} {$constraint} which is not installed.", + "Install '{$depId}'.", + false, + $id, + )); + } + } + } + } + + return $r; + } + + private function satisfiesBlueprintConstraint(string $constraint, string $version): bool + { + if ($version === 'rolling') { + return true; + } + + if (preg_match('/^(>=|<=|>|<|=)?\s*(.+)$/', $constraint, $m)) { + $op = $m[1] ?: '>='; + return version_compare($version, trim($m[2]), $op); + } + return true; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/IntegrityCheck.php b/app/BlueprintFramework/Doctor/Checks/IntegrityCheck.php new file mode 100644 index 00000000..b801175e --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/IntegrityCheck.php @@ -0,0 +1,86 @@ +name(), $this->title()); + + $required = [ + 'blueprint.sh', + 'app/BlueprintFramework/GetExtensionSchedules.php', + 'app/BlueprintFramework/Services/PlaceholderService/BlueprintPlaceholderService.php', + 'app/BlueprintFramework/Libraries/ExtensionLibrary/BlueprintBaseLibrary.php', + '.blueprint/extensions/blueprint/private/db/installed_extensions', + '.blueprint/extensions/blueprint/private/db/version', + 'scripts/libraries/lock.sh', + 'scripts/libraries/logFormat.sh', + ]; + + $missing = []; + foreach ($required as $path) { + if (!file_exists(base_path($path))) { + $missing[] = $path; + } + } + + if (empty($missing)) { + $r->ok('Files', 'OK'); + } else { + $r->err('Files', count($missing) . ' missing'); + $r->issue(new Issue( + Severity::ERROR, + 'Required Blueprint files missing', + 'Missing: ' . implode(', ', $missing), + 'Rerun the installer: blueprint -rerun-install', + )); + } + + $protected = [ + 'app/Console/Kernel.php', + 'app/Providers/AppServiceProvider.php', + 'resources/views/layouts/admin.blade.php', + 'resources/views/templates/wrapper.blade.php', + ]; + $modified = []; + foreach ($protected as $path) { + $full = base_path($path); + if (!file_exists($full)) { + $modified[] = $path; + continue; + } + $content = (string) file_get_contents($full); + if (!str_contains($content, 'Blueprint') && !str_contains($content, 'blueprint')) { + $modified[] = $path; + } + } + if (empty($modified)) { + $r->ok('Protected files', 'OK'); + } else { + $r->warn('Protected files', count($modified) . ' suspicious'); + $r->issue(new Issue( + Severity::WARNING, + 'Blueprint hook removal detected', + 'Files: ' . implode(', ', $modified), + 'Rerun: blueprint -upgrade', + )); + } + + $metadataOk = true; + foreach (['internal:seed', 'internal:cache'] as $key) { + // I hate this :,D + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/LogCheck.php b/app/BlueprintFramework/Doctor/Checks/LogCheck.php new file mode 100644 index 00000000..f45f0a87 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/LogCheck.php @@ -0,0 +1,77 @@ +name(), $this->title()); + + $logFile = storage_path('logs/laravel.log'); + if (!file_exists($logFile)) { + $r->row('Laravel log', 'not present', Severity::INFO); + return $r; + } + + $size = (int) @filesize($logFile); + $r->row('Laravel log', round($size / 1024, 1) . ' KB', Severity::INFO); + + $lines = @file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($lines === false) { + $r->warn('Recent errors', 'unable to read'); + return $r; + } + $tail = array_slice($lines, -500); + + $errorCount = 0; + $criticalCount = 0; + $lastError = null; + foreach ($tail as $line) { + if (str_contains($line, '.ERROR')) { + $errorCount++; + $lastError = $line; + } + if (str_contains($line, '.CRITICAL') || str_contains($line, '.EMERGENCY')) { + $criticalCount++; + $lastError = $line; + } + } + + if ($criticalCount > 0) { + $r->err('Recent errors', "{$criticalCount} critical"); + $r->issue(new Issue( + Severity::ERROR, + 'Critical errors in recent logs', + 'Last: ' . mb_substr((string) $lastError, 0, 200), + 'Review storage/logs/laravel.log', + )); + } elseif ($errorCount > 0) { + $r->warn('Recent errors', "{$errorCount} errors"); + $r->issue(new Issue( + Severity::WARNING, + 'Errors in recent logs', + 'Last: ' . mb_substr((string) $lastError, 0, 200), + 'Review storage/logs/laravel.log', + )); + } else { + $r->ok('Recent errors', 'none'); + } + + $bpLog = base_path('.blueprint/extensions/blueprint/private/debug/logs.txt'); + if (file_exists($bpLog)) { + $bpSize = (int) @filesize($bpLog); + $r->row('Blueprint log', round($bpSize / 1024, 1) . ' KB', Severity::INFO); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php b/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php new file mode 100644 index 00000000..f8c139e4 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php @@ -0,0 +1,94 @@ +name(), $this->title()); + + $writable = [ + 'storage' => storage_path(), + 'storage/logs' => storage_path('logs'), + 'storage/framework' => storage_path('framework'), + 'storage/framework/cache' => storage_path('framework/cache'), + 'storage/framework/sessions' => storage_path('framework/sessions'), + 'storage/framework/views' => storage_path('framework/views'), + 'bootstrap/cache' => base_path('bootstrap/cache'), + ]; + + $notWritable = []; + foreach ($writable as $label => $path) { + if (!is_dir($path)) { + @mkdir($path, 0755, true); + } + if (!is_writable($path)) { + $notWritable[] = $label; + } + } + + if (empty($notWritable)) { + $r->ok('Writable dirs', 'OK'); + } else { + $r->err('Writable dirs', count($notWritable) . ' not writable'); + $r->issue(new Issue( + Severity::ERROR, + 'Some directories are not writable', + 'Not writable: ' . implode(', ', $notWritable), + 'Fix with: chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache', + true, + )); + } + + $readable = [ + '.env' => base_path('.env'), + ]; + $notReadable = []; + foreach ($readable as $label => $path) { + if (!is_readable($path)) { + $notReadable[] = $label; + } + } + if (empty($notReadable)) { + $r->ok('Readable files', 'OK'); + } else { + $r->err('Readable files', implode(', ', $notReadable)); + $r->issue(new Issue( + Severity::ERROR, + 'Some files are not readable', + 'Not readable: ' . implode(', ', $notReadable), + 'Fix file ownership and permissions.', + )); + } + + $logDir = storage_path('logs'); + if (is_dir($logDir)) { + $size = 0; + foreach (glob($logDir . '/*.log') as $file) { + $size += (int) @filesize($file); + } + $mb = round($size / 1024 / 1024, 1); + $r->row('Log size', "{$mb} MB", $mb > 200 ? Severity::WARNING : Severity::INFO); + if ($mb > 200) { + $r->issue(new Issue( + Severity::WARNING, + 'Log directory is large', + "The logs directory is {$mb} MB. This can fill up the disk.", + 'Truncate the log files: truncate -s 0 storage/logs/*.log', + true, + )); + } + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/PterodactylCheck.php b/app/BlueprintFramework/Doctor/Checks/PterodactylCheck.php new file mode 100644 index 00000000..b14a945a --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/PterodactylCheck.php @@ -0,0 +1,48 @@ +name(), $this->title()); + + $version = config('app.version', 'unknown'); + $r->row('Version', (string) $version, Severity::INFO); + + if ($version === 'unknown' || $version === '') { + $r->issue(new Issue( + Severity::WARNING, + 'Pterodactyl version unknown', + 'APP_VERSION is not set in the environment.', + 'Set APP_VERSION in your .env file.', + )); + } elseif (version_compare($version, '1.10.0', '<')) { + $r->issue(new Issue( + Severity::WARNING, + 'Old Pterodactyl version', + "Blueprint is designed for Pterodactyl 1.10+. Detected {$version}.", + 'Upgrade Pterodactyl.', + )); + } + + if (!file_exists(base_path('artisan'))) { + $r->issue(new Issue( + Severity::CRITICAL, + 'Not a Laravel/Pterodactyl installation', + 'The artisan file is missing from the base path.', + )); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/QueueCheck.php b/app/BlueprintFramework/Doctor/Checks/QueueCheck.php new file mode 100644 index 00000000..ade31031 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/QueueCheck.php @@ -0,0 +1,73 @@ +name(), $this->title()); + + $driver = (string) config('queue.default', 'sync'); + if ($driver !== 'sync') { + $r->ok('Driver', $driver); + } else { + $r->warn('Driver', $driver); + $r->issue(new Issue( + Severity::WARNING, + 'Queue driver is "sync"', + 'Jobs run immediately instead of being queued. Long jobs (backups, installs) can timeout.', + 'Set QUEUE_CONNECTION=redis in .env.', + )); + } + + try { + $failed = DB::table('failed_jobs')->count(); + if ($failed === 0) { + $r->ok('Failed jobs', '0'); + } else { + $r->warn('Failed jobs', (string) $failed); + $r->issue(new Issue( + Severity::WARNING, + "{$failed} failed jobs", + 'Failed jobs usually mean the queue worker is not running or a job crashed.', + 'Run: php artisan queue:failed', + )); + } + } catch (\Throwable $e) { + $r->warn('Failed jobs', 'unable to check'); + } + + try { + $hasWorker = false; + if (function_exists('shell_exec')) { + $out = @shell_exec('ps aux 2>/dev/null | grep -E "queue:(work|listen)" | grep -v grep'); + $hasWorker = !empty(trim((string) $out)); + } + if ($hasWorker) { + $r->ok('Worker', 'running'); + } else { + $r->warn('Worker', 'not detected'); + $r->issue(new Issue( + Severity::WARNING, + 'Queue worker not detected', + 'No `queue:work` or `queue:listen` process was found.', + 'Start the worker: php artisan queue:work --daemon', + )); + } + } catch (\Throwable $e) { + $r->row('Worker', 'unable to check', Severity::WARNING); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php b/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php new file mode 100644 index 00000000..3b052496 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php @@ -0,0 +1,71 @@ +name(), $this->title()); + + $free = @disk_free_space(base_path()); + $total = @disk_total_space(base_path()); + if ($free !== false && $total !== false && $total > 0) { + $freeGb = round($free / 1024 / 1024 / 1024, 2); + $totalGb = round($total / 1024 / 1024 / 1024, 2); + + $status = Severity::INFO; + if ($freeGb < 1) { + $status = Severity::ERROR; + } elseif ($freeGb < 5) { + $status = Severity::WARNING; + } + + $r->row('Disk free', "{$freeGb} GB / {$totalGb} GB", $status); + + if ($freeGb < 1) { + $r->issue(new Issue( + Severity::ERROR, + 'Very low disk space', + "Only {$freeGb} GB free. Panels typically need at least 1 GB free.", + 'Free disk space before continuing.', + true, + )); + } elseif ($freeGb < 5) { + $r->issue(new Issue( + Severity::WARNING, + 'Low disk space', + "Only {$freeGb} GB free.", + 'Consider freeing disk space.', + true, + )); + } + } else { + $r->row('Disk free', 'unable to determine', Severity::INFO); + } + + $memoryLimit = ini_get('memory_limit'); + $r->row('PHP memory', $memoryLimit ?: 'unknown', Severity::INFO); + + $maxExec = ini_get('max_execution_time'); + $r->row('PHP max exec', $maxExec ?: 'unknown', Severity::INFO); + if ((int) $maxExec > 0 && (int) $maxExec < 60) { + $r->issue(new Issue( + Severity::WARNING, + 'Low max_execution_time', + "Current value is {$maxExec}s. Long operations may be killed.", + 'Set max_execution_time=300 or higher in php.ini.', + )); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/RouteCheck.php b/app/BlueprintFramework/Doctor/Checks/RouteCheck.php new file mode 100644 index 00000000..fd5b7f7e --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/RouteCheck.php @@ -0,0 +1,99 @@ +name(), $this->title()); + + $routeFiles = [ + 'routes/blueprint.php', + 'routes/blueprint/web.php', + 'routes/blueprint/client.php', + 'routes/blueprint/application.php', + ]; + $missing = []; + foreach ($routeFiles as $file) { + if (!file_exists(base_path($file))) { + $missing[] = $file; + } + } + if (empty($missing)) { + $r->ok('Route files', 'present'); + } else { + $r->err('Route files', count($missing) . ' missing'); + $r->issue(new Issue( + Severity::ERROR, + 'Blueprint route files missing', + 'Missing: ' . implode(', ', $missing), + 'Rerun the installer: blueprint -rerun-install', + )); + } + + try { + $kernel = app(\Pterodactyl\Http\Kernel::class); + $reflection = new \ReflectionClass($kernel); + $prop = $reflection->getProperty('middlewareGroups'); + $prop->setAccessible(true); + $groups = (array) $prop->getValue($kernel); + + $requiredGroups = ['blueprint', 'blueprint/api', 'blueprint/application-api', 'blueprint/client-api']; + $missingGroups = []; + foreach ($requiredGroups as $group) { + if (!isset($groups[$group])) { + $missingGroups[] = $group; + } + } + if (empty($missingGroups)) { + $r->ok('Middleware', 'OK'); + } else { + $r->warn('Middleware', count($missingGroups) . ' missing'); + $r->issue(new Issue( + Severity::WARNING, + 'Blueprint middleware groups missing', + 'Missing: ' . implode(', ', $missingGroups), + 'Check app/Http/Kernel.php', + )); + } + } catch (\Throwable $e) { + $r->row('Middleware', 'unable to check', Severity::WARNING); + } + + try { + $routes = app('router')->getRoutes(); + $hasAdminExtensions = false; + foreach ($routes as $route) { + if (str_contains($route->uri(), 'admin/extensions')) { + $hasAdminExtensions = true; + break; + } + } + if ($hasAdminExtensions) { + $r->ok('Admin route', 'registered'); + } else { + $r->warn('Admin route', 'missing'); + $r->issue(new Issue( + Severity::WARNING, + 'Admin extensions route not registered', + '/admin/extensions is not in the route list.', + 'Clear route cache: php artisan route:clear', + true, + )); + } + } catch (\Throwable $e) { + $r->row('Admin route', 'unable to check', Severity::WARNING); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php b/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php new file mode 100644 index 00000000..998b11cc --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php @@ -0,0 +1,94 @@ +name(), $this->title()); + + $debug = config('app.debug', false); + if ($debug === false) { + $r->ok('APP_DEBUG', 'false'); + } else { + $r->err('APP_DEBUG', 'true'); + $r->issue(new Issue( + Severity::CRITICAL, + 'APP_DEBUG is enabled', + 'Debug mode leaks sensitive data (DB credentials, API keys) in error pages.', + 'Set APP_DEBUG=false in .env and run: php artisan config:clear', + )); + } + + $env = (string) config('app.env', 'production'); + if ($env === 'production') { + $r->ok('APP_ENV', $env); + } else { + $r->warn('APP_ENV', $env); + $r->issue(new Issue( + Severity::WARNING, + 'APP_ENV is not "production"', + "Current value: {$env}", + 'Set APP_ENV=production in .env.', + )); + } + + $url = (string) config('app.url', ''); + if (str_starts_with($url, 'https://')) { + $r->ok('APP_URL', 'HTTPS'); + } elseif (str_starts_with($url, 'http://')) { + $r->warn('APP_URL', 'HTTP'); + $r->issue(new Issue( + Severity::WARNING, + 'APP_URL does not use HTTPS', + 'Cookies and sessions may be transmitted in cleartext.', + 'Set APP_URL to an https:// URL in .env.', + )); + } else { + $r->warn('APP_URL', $url ?: 'not set'); + } + + if (str_starts_with($url, 'https://')) { + $secure = config('session.secure', false); + if ($secure) { + $r->ok('Secure cookies', 'enabled'); + } else { + $r->warn('Secure cookies', 'disabled'); + $r->issue(new Issue( + Severity::WARNING, + 'SESSION_SECURE_COOKIE is disabled', + 'Cookies may be sent over HTTP.', + 'Set SESSION_SECURE_COOKIE=true in .env.', + )); + } + } + + $envFile = base_path('.env'); + if (file_exists($envFile)) { + $perms = substr(sprintf('%o', fileperms($envFile)), -4); + if (in_array($perms, ['0600', '0640', '0660'], true)) { + $r->ok('.env perms', $perms); + } else { + $r->warn('.env perms', $perms); + $r->issue(new Issue( + Severity::WARNING, + '.env file permissions are too permissive', + "Current: {$perms}, recommended: 0600 or 0640", + 'Run: chmod 640 .env', + true, + )); + } + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/StorageCheck.php b/app/BlueprintFramework/Doctor/Checks/StorageCheck.php new file mode 100644 index 00000000..a85417a0 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/StorageCheck.php @@ -0,0 +1,87 @@ +name(), $this->title()); + + $publicStorage = public_path('storage'); + if (is_link($publicStorage) || is_dir($publicStorage)) { + $r->ok('public/storage', 'linked'); + } else { + $r->warn('public/storage', 'missing'); + $r->issue(new Issue( + Severity::WARNING, + 'public/storage link is missing', + 'The Laravel storage link has not been created.', + 'Run: php artisan storage:link', + true, + )); + } + + $bpPublic = public_path('extensions/blueprint'); + if (is_link($bpPublic) || is_dir($bpPublic)) { + $r->ok('Blueprint public', 'linked'); + } else { + $r->warn('Blueprint public', 'missing'); + $r->issue(new Issue( + Severity::WARNING, + 'Blueprint public link is missing', + 'The Blueprint extension public directory is not linked.', + 'Rerun the installer: blueprint -rerun-install', + true, + )); + } + + $schedules = app_path('BlueprintFramework/Schedules'); + if (is_dir($schedules)) { + $r->ok('Schedules dir', 'exists'); + } else { + $r->warn('Schedules dir', 'missing'); + $r->issue(new Issue( + Severity::WARNING, + 'Schedules directory missing', + 'Extension schedules cannot be registered.', + 'Create the directory: mkdir -p app/BlueprintFramework/Schedules', + true, + )); + } + + try { + $disks = config('filesystems.disks', []); + $hasBlueprintDisk = false; + foreach (array_keys($disks) as $name) { + if (str_starts_with((string) $name, 'blueprint')) { + $hasBlueprintDisk = true; + break; + } + } + if ($hasBlueprintDisk) { + $r->ok('ExtensionFS disks', 'configured'); + } else { + $r->warn('ExtensionFS disks', 'none'); + $r->issue(new Issue( + Severity::WARNING, + 'No Blueprint filesystem disks configured', + 'ExtensionFS disks start with "blueprint".', + 'Check .blueprint/extensions/blueprint/private/extensionfs.php', + )); + } + } catch (\Throwable $e) { + $r->row('ExtensionFS disks', 'unable to check', Severity::WARNING); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Checks/VersionCheck.php b/app/BlueprintFramework/Doctor/Checks/VersionCheck.php new file mode 100644 index 00000000..91d17020 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/VersionCheck.php @@ -0,0 +1,68 @@ +name(), $this->title()); + + $current = $this->placeholder->version(); + $r->row('Current', $current ?: 'unknown', Severity::INFO); + + if ($current === 'rolling') { + $r->issue(new Issue( + Severity::INFO, + 'Running rolling version', + 'This is a development build. Do not use in production.', + )); + return $r; + } + + $latest = $this->blueprint->dbGet('blueprint', 'internal:version:latest', null); + if ($latest === null || $latest === 'unknown' || $latest === '') { + $r->warn('Latest', 'unknown'); + $r->issue(new Issue( + Severity::INFO, + 'Latest version unknown', + 'Could not determine the latest available version.', + 'Run: php artisan bp:version:cache', + true, + )); + return $r; + } + + $r->row('Latest', (string) $latest, Severity::INFO); + + if ($current === $latest) { + $r->ok('Up to date', 'yes'); + } else { + $r->warn('Up to date', 'no'); + $r->issue(new Issue( + Severity::WARNING, + 'Blueprint is out of date', + "Running {$current}, latest is {$latest}.", + 'Run: blueprint -upgrade', + )); + } + + return $r; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/DoctorReport.php b/app/BlueprintFramework/Doctor/DoctorReport.php new file mode 100644 index 00000000..035d7785 --- /dev/null +++ b/app/BlueprintFramework/Doctor/DoctorReport.php @@ -0,0 +1,66 @@ +results[] = $result; + } + + /** @return CheckResult[] */ + public function results(): array + { + return $this->results; + } + + /** @return Issue[] */ + public function allIssues(): array + { + $all = []; + foreach ($this->results as $result) { + foreach ($result->issues() as $issue) { + $all[] = $issue; + } + } + return $all; + } + + public function countBySeverity(Severity $severity): int + { + return count(array_filter($this->allIssues(), fn (Issue $i) => $i->severity === $severity)); + } + + public function hasErrors(): bool + { + foreach ($this->allIssues() as $issue) { + if ($issue->severity->weight() >= Severity::ERROR->weight()) { + return true; + } + } + return false; + } + + public function autoFixableIssues(): array + { + return array_values(array_filter($this->allIssues(), fn (Issue $i) => $i->autoFixable)); + } + + public function toArray(): array + { + return [ + 'generated_at' => now()->toIso8601String(), + 'results' => array_map(fn (CheckResult $r) => $r->toArray(), $this->results), + 'summary' => [ + 'info' => $this->countBySeverity(Severity::INFO), + 'warning' => $this->countBySeverity(Severity::WARNING), + 'error' => $this->countBySeverity(Severity::ERROR), + 'critical' => $this->countBySeverity(Severity::CRITICAL), + ], + ]; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/DoctorService.php b/app/BlueprintFramework/Doctor/DoctorService.php new file mode 100644 index 00000000..e91d41fb --- /dev/null +++ b/app/BlueprintFramework/Doctor/DoctorService.php @@ -0,0 +1,308 @@ +checks = [ + $environment, + $blueprint, + $pterodactyl, + $configuration, + $database, + $cache, + $extensions, + $integrity, + $queue, + $permissions, + $storage, + $logs, + $security, + $routes, + $version, + $resources, + ]; + } + + public function run(): DoctorReport + { + $report = new DoctorReport(); + foreach ($this->checks as $check) { + try { + $report->add($check->run()); + } catch (\Throwable $e) { + $result = new CheckResult($check->name(), $check->title()); + $result->err('Check failed', $e->getMessage()); + $result->issue(new Issue( + Severity::ERROR, + "Check '{$check->name()}' crashed", + $e->getMessage(), + )); + $report->add($result); + } + } + return $report; + } + + /** + * @return array + */ + public function fix(DoctorReport $report): array + { + $actions = []; + + foreach ($report->autoFixableIssues() as $issue) { + $action = $this->applyFix($issue); + if ($action !== null) { + $actions[] = $action; + } else { + $actions[] = [ + 'label' => $issue->title, + 'ok' => false, + 'detail' => 'Manual intervention required.', + ]; + } + } + + return $actions; + } + + private function applyFix(Issue $issue): ?array + { + $title = $issue->title; + + if (str_contains($title, 'Configuration cache')) { + try { + Artisan::call('config:clear'); + Artisan::call('config:cache'); + return ['label' => 'Rebuilt configuration cache', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Rebuild configuration cache', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Blueprint cache')) { + try { + Artisan::call('bp:cache'); + return ['label' => 'Rebuilt Blueprint cache', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Rebuild Blueprint cache', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Pending database migrations')) { + try { + Artisan::call('migrate', ['--force' => true]); + return ['label' => 'Applied pending database migrations', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Apply pending database migrations', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'installation incomplete')) { + $path = base_path('.blueprint/extensions/blueprint/private/db/is_installed'); + $dir = dirname($path); + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + } + if (@touch($path)) { + return ['label' => 'Recreated is_installed marker', 'ok' => true, 'detail' => null]; + } + return ['label' => 'Recreate is_installed marker', 'ok' => false, 'detail' => 'Permission denied']; + } + + if (str_contains($title, 'configuration not seeded')) { + try { + Artisan::call('db:seed', ['--class' => 'BlueprintSeeder', '--force' => true]); + return ['label' => 'Seeded Blueprint configuration', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Seed Blueprint configuration', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'public/storage link')) { + try { + Artisan::call('storage:link'); + return ['label' => 'Created public/storage link', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Create public/storage link', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Admin extensions route not registered')) { + try { + Artisan::call('route:clear'); + return ['label' => 'Cleared route cache', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Clear route cache', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Latest version unknown')) { + try { + Artisan::call('bp:version:cache'); + return ['label' => 'Cached latest Blueprint version', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Cache latest Blueprint version', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Log directory is large')) { + try { + $logs = glob(storage_path('logs/*.log')); + $bytes = 0; + $count = 0; + foreach ($logs as $log) { + $bytes += (int) @filesize($log); + if (@file_put_contents($log, '') !== false) { + $count++; + } + } + $mb = round($bytes / 1024 / 1024, 1); + return [ + 'label' => "Truncated {$count} log file(s) ({$mb} MB freed)", + 'ok' => true, + 'detail' => null, + ]; + } catch (\Throwable $e) { + return ['label' => 'Truncate log files', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'directories are not writable')) { + if (PHP_OS_FAMILY === 'Windows') { + return ['label' => 'Fix directory permissions', 'ok' => false, 'detail' => 'Not supported on Windows']; + } + try { + $paths = [ + storage_path(), + storage_path('logs'), + storage_path('framework'), + storage_path('framework/cache'), + storage_path('framework/sessions'), + storage_path('framework/views'), + base_path('bootstrap/cache'), + ]; + $fixed = 0; + foreach ($paths as $p) { + if (is_dir($p)) { + if (@chmod($p, 0775)) { + $fixed++; + } + } + } + return [ + 'label' => "Applied chmod 775 to {$fixed} directory/ies", + 'ok' => true, + 'detail' => null, + ]; + } catch (\Throwable $e) { + return ['label' => 'Apply chmod 775', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Blueprint public link is missing')) { + try { + $source = base_path('.blueprint/extensions/blueprint/public'); + $target = public_path('extensions/blueprint'); + if (!is_dir($source)) { + return ['label' => 'Create Blueprint public link', 'ok' => false, 'detail' => 'Source missing: ' . $source]; + } + if (is_link($target)) { + @unlink($target); + } elseif (is_dir($target)) { + $items = @scandir($target); + if ($items !== false && count($items) > 2) { + return ['label' => 'Create Blueprint public link', 'ok' => false, 'detail' => 'Target directory is not empty']; + } + @rmdir($target); + } + @mkdir(dirname($target), 0755, true); + if (@symlink($source, $target)) { + return ['label' => 'Created Blueprint public symlink', 'ok' => true, 'detail' => null]; + } + return ['label' => 'Create Blueprint public symlink', 'ok' => false, 'detail' => 'symlink() failed']; + } catch (\Throwable $e) { + return ['label' => 'Create Blueprint public symlink', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, 'Schedules directory missing')) { + try { + $path = app_path('BlueprintFramework/Schedules'); + if (@mkdir($path, 0755, true) || is_dir($path)) { + return ['label' => 'Created Schedules directory', 'ok' => true, 'detail' => null]; + } + return ['label' => 'Create Schedules directory', 'ok' => false, 'detail' => 'mkdir() failed']; + } catch (\Throwable $e) { + return ['label' => 'Create Schedules directory', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + if (str_contains($title, '.env file permissions are too permissive')) { + if (PHP_OS_FAMILY === 'Windows') { + return ['label' => 'Fix .env permissions', 'ok' => false, 'detail' => 'Not supported on Windows']; + } + $envFile = base_path('.env'); + if (!file_exists($envFile)) { + return ['label' => 'Fix .env permissions', 'ok' => false, 'detail' => '.env not found']; + } + if (@chmod($envFile, 0640)) { + return ['label' => 'Set .env permissions to 0640', 'ok' => true, 'detail' => null]; + } + return ['label' => 'Set .env permissions', 'ok' => false, 'detail' => 'chmod() failed']; + } + + if (str_contains($title, 'Low disk space') || str_contains($title, 'Very low disk space')) { + try { + Artisan::call('cache:clear'); + Artisan::call('view:clear'); + Artisan::call('route:clear'); + return ['label' => 'Cleared caches to free disk space', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Clear caches', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Issue.php b/app/BlueprintFramework/Doctor/Issue.php new file mode 100644 index 00000000..4fac5d75 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Issue.php @@ -0,0 +1,28 @@ + $this->severity->value, + 'title' => $this->title, + 'detail' => $this->detail, + 'fix' => $this->fix, + 'auto_fixable' => $this->autoFixable, + 'context' => $this->context, + ]; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/Severity.php b/app/BlueprintFramework/Doctor/Severity.php new file mode 100644 index 00000000..059ee9da --- /dev/null +++ b/app/BlueprintFramework/Doctor/Severity.php @@ -0,0 +1,35 @@ +value); + } + + public function symbol(): string + { + return match ($this) { + self::INFO => '·', + self::WARNING => '!', + self::ERROR, self::CRITICAL => '×', + }; + } + + public function weight(): int + { + return match ($this) { + self::INFO => 0, + self::WARNING => 1, + self::ERROR => 2, + self::CRITICAL => 3, + }; + } +} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/DoctorCommand.php b/app/Console/Commands/BlueprintFramework/DoctorCommand.php new file mode 100644 index 00000000..1e2c212b --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/DoctorCommand.php @@ -0,0 +1,106 @@ +doctor->run(); + + if ($this->option('json')) { + $this->line(json_encode($report->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + return $report->hasErrors() ? 1 : 0; + } + + $this->renderReport($report); + + if ($this->option('fix')) { + $this->newLine(); + $this->line('Fixing issues...'); + $this->newLine(); + + $actions = $this->doctor->fix($report); + + if (empty($actions)) { + $this->line(' No automatically fixable issues found.'); + } + foreach ($actions as $action) { + $prefix = $action['ok'] ? '✓' : '✗'; + $line = " {$prefix} {$action['label']}"; + if (!empty($action['detail'])) { + $line .= " ({$action['detail']})"; + } + $this->line($line); + } + } + + return $report->hasErrors() ? 1 : 0; + } + + private function renderReport(DoctorReport $report): void + { + $this->newLine(); + $this->line('Blueprint Doctor'); + $this->line('────────────────────────────────'); + $this->newLine(); + + foreach ($report->results() as $result) { + $this->line('' . $result->title . ''); + foreach ($result->rows() as $row) { + $marker = ' '; + $markerColor = 'gray'; + if ($row['status'] === Severity::INFO) { $marker = '✓'; $markerColor = 'green'; } + elseif ($row['status'] === Severity::WARNING) { $marker = '⚠'; $markerColor = 'yellow'; } + elseif ($row['status'] === Severity::ERROR || $row['status'] === Severity::CRITICAL) { $marker = '✗'; $markerColor = 'red'; } + + $this->line(sprintf( + ' %-18s %-30s %s', + $row['label'], + $row['value'], + $markerColor, + $marker, + )); + } + + foreach ($result->issues() as $issue) { + $color = match ($issue->severity) { + Severity::INFO => 'gray', + Severity::WARNING => 'yellow', + Severity::ERROR, Severity::CRITICAL => 'red', + }; + $this->newLine(); + $this->line(" [{$issue->severity->label()}] {$issue->title}"); + $this->line(" {$issue->detail}"); + if ($issue->fix) { + $this->line(' Suggested fix: ' . $issue->fix); + } + } + + $this->newLine(); + } + + $this->line('────────────────────────────────'); + $this->line('Result:'); + $warnings = $report->countBySeverity(Severity::WARNING); + $errors = $report->countBySeverity(Severity::ERROR) + $report->countBySeverity(Severity::CRITICAL); + $this->line(" {$warnings} warning" . ($warnings === 1 ? '' : 's')); + $this->line(" {$errors} error" . ($errors === 1 ? '' : 's')); + $this->newLine(); + } +} \ No newline at end of file diff --git a/blueprint.sh b/blueprint.sh index fc113ac0..e3f3d0bb 100644 --- a/blueprint.sh +++ b/blueprint.sh @@ -42,7 +42,7 @@ if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then -export) opts="expose" ;; -upgrade) opts="remote" ;; - *) opts="-install -add -remove -query -init -build -export -wipe -version -help -info -debug -upgrade -unlock -rerun-install -dist" ;; + *) opts="-install -add -remove -query -init -build -export -wipe -version -help -info -debug -upgrade -unlock -rerun-install -dist -doctor" ;; esac if [[ ${cur} == * ]]; then @@ -472,6 +472,7 @@ case "$cmd" in -rerun-install) source ./scripts/commands/advanced/rerun-install.sh ;; -upgrade) source ./scripts/commands/advanced/upgrade.sh ;; -unlock) source ./scripts/commands/advanced/unlock.sh ;; + -doctor) source ./scripts/commands/advanced/doctor.sh ;; esac shift 2 diff --git a/scripts/commands/advanced/doctor.sh b/scripts/commands/advanced/doctor.sh new file mode 100644 index 00000000..5f37e07f --- /dev/null +++ b/scripts/commands/advanced/doctor.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +Command() { + cd "$FOLDER" || cdhalt + + php artisan bp:doctor "$@" + exit $? +} \ No newline at end of file diff --git a/scripts/commands/misc/help.sh b/scripts/commands/misc/help.sh index f7805a67..721fb385 100644 --- a/scripts/commands/misc/help.sh +++ b/scripts/commands/misc/help.sh @@ -35,6 +35,7 @@ ${help_dev_primary}Developer${help_dev_status}\x1b[0m${help_dev_secondary} \x1b[0m \x1b[34;1mAdvanced\x1b[0m\x1b[34m + -doctor (--fix --json) run Blueprint diagnostics -unlock remove lockfile -upgrade (remote ) update/reset to another release -rerun-install rerun the blueprint installation script