From a6c52001f20816e27cef394c9df2bcf158874bf7 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:57:41 +0200 Subject: [PATCH 01/11] feat(doctor): add Blueprint Doctor diagnostic system --- .../Doctor/CheckInterface.php | 10 ++ app/BlueprintFramework/Doctor/CheckResult.php | 70 ++++++++ .../Doctor/Checks/BlueprintCheck.php | 70 ++++++++ .../Doctor/Checks/CacheCheck.php | 57 +++++++ .../Doctor/Checks/ConfigurationCheck.php | 60 +++++++ .../Doctor/Checks/DatabaseCheck.php | 91 ++++++++++ .../Doctor/Checks/EnvironmentCheck.php | 67 ++++++++ .../Doctor/Checks/ExtensionCheck.php | 153 +++++++++++++++++ .../Doctor/Checks/IntegrityCheck.php | 86 ++++++++++ .../Doctor/Checks/MigrationCheck.php | 68 ++++++++ .../Doctor/Checks/PterodactylCheck.php | 48 ++++++ .../Doctor/DoctorReport.php | 66 ++++++++ .../Doctor/DoctorService.php | 156 ++++++++++++++++++ app/BlueprintFramework/Doctor/Issue.php | 28 ++++ app/BlueprintFramework/Doctor/Severity.php | 35 ++++ .../BlueprintFramework/DoctorCommand.php | 104 ++++++++++++ scripts/commands/advanced/doctor.sh | 9 + 17 files changed, 1178 insertions(+) create mode 100644 app/BlueprintFramework/Doctor/CheckInterface.php create mode 100644 app/BlueprintFramework/Doctor/CheckResult.php create mode 100644 app/BlueprintFramework/Doctor/Checks/BlueprintCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/CacheCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/ConfigurationCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/DatabaseCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/EnvironmentCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/IntegrityCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/MigrationCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/PterodactylCheck.php create mode 100644 app/BlueprintFramework/Doctor/DoctorReport.php create mode 100644 app/BlueprintFramework/Doctor/DoctorService.php create mode 100644 app/BlueprintFramework/Doctor/Issue.php create mode 100644 app/BlueprintFramework/Doctor/Severity.php create mode 100644 app/Console/Commands/BlueprintFramework/DoctorCommand.php create mode 100644 scripts/commands/advanced/doctor.sh 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..d2b565c6 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php @@ -0,0 +1,153 @@ +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'] ?? ''; + + // Compatibility + $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, + )); + } + + // Required dependencies (if declared) + $requires = $conf['requires'] ?? []; + foreach ((array) $requires as $dep => $constraint) { + if (!is_string($dep)) { + continue; + } + if (str_starts_with($dep, 'blueprint/')) { + // Blueprint core dependency + $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/MigrationCheck.php b/app/BlueprintFramework/Doctor/Checks/MigrationCheck.php new file mode 100644 index 00000000..6cfffd6f --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/MigrationCheck.php @@ -0,0 +1,68 @@ +name(), $this->title()); + + try { + $current = $this->placeholder->version(); + $pending = $this->manager->pending($current); + + if (empty($pending)) { + $r->ok('State', 'Up to date'); + } else { + $r->warn('State', count($pending) . ' pending'); + $r->issue(new Issue( + Severity::WARNING, + 'Pending Blueprint migrations', + 'Migrations: ' . implode(', ', array_map( + fn ($m) => $m->id(), + $pending + )), + 'Run: blueprint migrate', + true, + )); + } + + $failed = $this->manager->failed(); + if (!empty($failed)) { + $r->err('Failed', count($failed) . ' failed'); + $r->issue(new Issue( + Severity::ERROR, + 'Failed Blueprint migrations detected', + 'Run `blueprint migration history` for details.', + 'Resolve the failure and rerun `blueprint migrate`.', + )); + } + } catch (\Throwable $e) { + $r->warn('State', 'Unavailable'); + $r->issue(new Issue( + Severity::WARNING, + 'Migration state unavailable', + $e->getMessage(), + )); + } + + 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/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..498d02c3 --- /dev/null +++ b/app/BlueprintFramework/Doctor/DoctorService.php @@ -0,0 +1,156 @@ +checks = [ + $environment, + $blueprint, + $pterodactyl, + $configuration, + $database, + $cache, + $extensions, + $integrity, + $migrations, + ]; + } + + 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; + } + + /** + * Apply safe, deterministic fixes only. + * + * @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; + + // Stale config cache + 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()]; + } + } + + // Blueprint cache + 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()]; + } + } + + // Pending Laravel migrations + 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()]; + } + } + + // Pending Blueprint migrations + if (str_contains($title, 'Pending Blueprint migrations')) { + try { + Artisan::call('bp:migrate', ['--force' => true]); + return ['label' => 'Applied pending Blueprint migrations', 'ok' => true, 'detail' => null]; + } catch (\Throwable $e) { + return ['label' => 'Apply pending Blueprint migrations', 'ok' => false, 'detail' => $e->getMessage()]; + } + } + + // Install marker + if (str_contains($title, 'installation incomplete')) { + $path = base_path('.blueprint/extensions/blueprint/private/db/is_installed'); + if (@touch($path)) { + return ['label' => 'Recreated is_installed marker', 'ok' => true, 'detail' => null]; + } + return ['label' => 'Recreate is_installed marker', 'ok' => false, 'detail' => 'Permission denied']; + } + + // Blueprint seed + 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()]; + } + } + + 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..18ba92c1 --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/DoctorCommand.php @@ -0,0 +1,104 @@ +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 %-22s <%s>%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:'); + $this->line(' ' . $report->countBySeverity(Severity::WARNING) . ' warning' . ($report->countBySeverity(Severity::WARNING) === 1 ? '' : 's')); + $this->line(' ' . ($report->countBySeverity(Severity::ERROR) + $report->countBySeverity(Severity::CRITICAL)) . ' error' . (($report->countBySeverity(Severity::ERROR) + $report->countBySeverity(Severity::CRITICAL)) === 1 ? '' : 's')); + $this->newLine(); + } +} \ No newline at end of file diff --git a/scripts/commands/advanced/doctor.sh b/scripts/commands/advanced/doctor.sh new file mode 100644 index 00000000..f673b7c0 --- /dev/null +++ b/scripts/commands/advanced/doctor.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +Command() { + cd "$FOLDER" || cdhalt + + # Forward arguments directly to the artisan command. + php artisan bp:doctor "$@" + exit $? +} \ No newline at end of file From 7b57617ca50e16fe7a96da75209690cecb5dbfef Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:57:42 +0200 Subject: [PATCH 02/11] feat(migrations): add Blueprint Migration Manager --- .../Migrations/AbstractMigration.php | 26 ++++ .../BlueprintMigrationInterface.php | 25 ++++ .../Migrations/MigrationContext.php | 20 +++ .../Migrations/MigrationLock.php | 49 +++++++ .../Migrations/MigrationManager.php | 129 ++++++++++++++++++ .../Migrations/MigrationRecord.php | 20 +++ .../Migrations/MigrationRepository.php | 64 +++++++++ .../Migrations/MigrationResolver.php | 40 ++++++ .../Migrations/MigrationRunner.php | 110 +++++++++++++++ .../Migrations/Registry.php | 38 ++++++ .../BlueprintFramework/MigrateCommand.php | 64 +++++++++ .../MigrationHistoryCommand.php | 41 ++++++ .../MigrationRollbackCommand.php | 55 ++++++++ .../MigrationShowCommand.php | 43 ++++++ .../MigrationStatusCommand.php | 72 ++++++++++ app/Models/BlueprintMigration.php | 21 +++ ...0250_create_blueprint_migrations_table.php | 33 +++++ scripts/commands/advanced/migrate.sh | 18 +++ scripts/commands/advanced/migration.sh | 19 +++ 19 files changed, 887 insertions(+) create mode 100644 app/BlueprintFramework/Migrations/AbstractMigration.php create mode 100644 app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php create mode 100644 app/BlueprintFramework/Migrations/MigrationContext.php create mode 100644 app/BlueprintFramework/Migrations/MigrationLock.php create mode 100644 app/BlueprintFramework/Migrations/MigrationManager.php create mode 100644 app/BlueprintFramework/Migrations/MigrationRecord.php create mode 100644 app/BlueprintFramework/Migrations/MigrationRepository.php create mode 100644 app/BlueprintFramework/Migrations/MigrationResolver.php create mode 100644 app/BlueprintFramework/Migrations/MigrationRunner.php create mode 100644 app/BlueprintFramework/Migrations/Registry.php create mode 100644 app/Console/Commands/BlueprintFramework/MigrateCommand.php create mode 100644 app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php create mode 100644 app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php create mode 100644 app/Console/Commands/BlueprintFramework/MigrationShowCommand.php create mode 100644 app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php create mode 100644 app/Models/BlueprintMigration.php create mode 100644 database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php create mode 100644 scripts/commands/advanced/migrate.sh create mode 100644 scripts/commands/advanced/migration.sh diff --git a/app/BlueprintFramework/Migrations/AbstractMigration.php b/app/BlueprintFramework/Migrations/AbstractMigration.php new file mode 100644 index 00000000..a34a4cee --- /dev/null +++ b/app/BlueprintFramework/Migrations/AbstractMigration.php @@ -0,0 +1,26 @@ +fromVersion() === null) { + return true; + } + return $this->fromVersion() === $installedVersion; + } + + public function down(MigrationContext $context): void + { + throw new \RuntimeException( + 'Migration ' . $this->id() . ' does not implement rollback.' + ); + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php b/app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php new file mode 100644 index 00000000..72efe2eb --- /dev/null +++ b/app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php @@ -0,0 +1,25 @@ +lockPath = base_path('.blueprint/migration.lock'); + } + + public function acquire(): bool + { + $dir = dirname($this->lockPath); + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + } + + if (file_exists($this->lockPath)) { + // Detect stale lock (> 4 hours old) + $age = time() - (int) @filemtime($this->lockPath); + if ($age > 14400) { + @unlink($this->lockPath); + } else { + return false; + } + } + + $handle = @fopen($this->lockPath, 'x'); + if ($handle === false) { + return false; + } + fwrite($handle, (string) getmypid()); + fclose($handle); + return true; + } + + public function release(): void + { + @unlink($this->lockPath); + } + + public function isHeld(): bool + { + return file_exists($this->lockPath); + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationManager.php b/app/BlueprintFramework/Migrations/MigrationManager.php new file mode 100644 index 00000000..b5843201 --- /dev/null +++ b/app/BlueprintFramework/Migrations/MigrationManager.php @@ -0,0 +1,129 @@ +resolver->pending($currentVersion); + } + + /** @return array */ + public function describePending(string $currentVersion): array + { + return array_map(fn (BlueprintMigrationInterface $m) => [ + 'id' => $m->id(), + 'from' => $m->fromVersion(), + 'to' => $m->toVersion(), + 'description' => $m->description(), + 'rollbackable' => $m->isRollbackable(), + ], $this->pending($currentVersion)); + } + + public function failed(): array + { + return $this->repository->failed(); + } + + public function history(): array + { + return $this->repository->all(); + } + + public function find(string $id): ?BlueprintMigrationInterface + { + return $this->resolver->find($id); + } + + /** + * @return array{ok:bool,ran:string[],failed:?string,error:?string} + */ + public function migrate(?string $fromVersion = null, ?string $targetVersion = null, ?callable $logger = null): array + { + if ($this->lock->isHeld()) { + return ['ok' => false, 'ran' => [], 'failed' => null, 'error' => 'A migration is already running.']; + } + if (!$this->lock->acquire()) { + return ['ok' => false, 'ran' => [], 'failed' => null, 'error' => 'Could not acquire migration lock.']; + } + + $ran = []; + try { + $current = $fromVersion ?? $this->placeholder->version(); + $pending = $this->pending($current); + + foreach ($pending as $migration) { + $target = $targetVersion ?? $migration->toVersion() ?? $current; + $ok = $this->runner->run($migration, $current, $target, $logger); + if (!$ok) { + return ['ok' => false, 'ran' => $ran, 'failed' => $migration->id(), 'error' => 'Migration failed.']; + } + $ran[] = $migration->id(); + $current = $target; + } + } finally { + $this->lock->release(); + } + + return ['ok' => true, 'ran' => $ran, 'failed' => null, 'error' => null]; + } + + /** + * @return array{ok:bool,id:?string,error:?string} + */ + public function rollback(?string $id = null): array + { + if ($this->lock->isHeld()) { + return ['ok' => false, 'id' => null, 'error' => 'A migration is already running.']; + } + if (!$this->lock->acquire()) { + return ['ok' => false, 'id' => null, 'error' => 'Could not acquire migration lock.']; + } + + try { + $migration = null; + if ($id !== null) { + $migration = $this->resolver->find($id); + if (!$migration) { + return ['ok' => false, 'id' => $id, 'error' => "Migration '{$id}' not found."]; + } + if (!$migration->isRollbackable()) { + return ['ok' => false, 'id' => $id, 'error' => 'Migration is not rollbackable.']; + } + } else { + // Most recent successful rollbackable migration + $history = array_reverse($this->repository->all()); + foreach ($history as $record) { + if ($record->status === 'success' && $record->rollbackable) { + $migration = $this->resolver->find($record->migration_id); + if ($migration) { + $id = $migration->id(); + break; + } + } + } + if (!$migration) { + return ['ok' => false, 'id' => null, 'error' => 'No rollbackable migration found.']; + } + } + + $ok = $this->runner->rollback($migration); + return ['ok' => $ok, 'id' => $migration->id(), 'error' => $ok ? null : 'Rollback failed.']; + } finally { + $this->lock->release(); + } + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationRecord.php b/app/BlueprintFramework/Migrations/MigrationRecord.php new file mode 100644 index 00000000..11aef029 --- /dev/null +++ b/app/BlueprintFramework/Migrations/MigrationRecord.php @@ -0,0 +1,20 @@ + $record->id], + [ + 'from_version' => $record->fromVersion, + 'to_version' => $record->toVersion, + 'status' => $record->status, + 'started_at' => $record->startedAt, + 'completed_at' => $record->completedAt, + 'duration_ms' => $record->durationMs, + 'rollbackable' => $record->rollbackable, + 'error' => $record->error, + 'meta' => $record->meta, + ] + ); + } + + public function find(string $id): ?BlueprintMigration + { + return BlueprintMigration::where('migration_id', $id)->first(); + } + + /** @return BlueprintMigration[] */ + public function all(): array + { + return BlueprintMigration::orderBy('id')->get()->all(); + } + + public function hasRun(string $id): bool + { + return BlueprintMigration::where('migration_id', $id) + ->where('status', 'success') + ->exists(); + } + + public function hasFailed(string $id): bool + { + return BlueprintMigration::where('migration_id', $id) + ->where('status', 'failed') + ->exists(); + } + + /** @return BlueprintMigration[] */ + public function failed(): array + { + return BlueprintMigration::where('status', 'failed')->get()->all(); + } + + public function lastSuccessful(): ?BlueprintMigration + { + return BlueprintMigration::where('status', 'success') + ->orderByDesc('id') + ->first(); + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationResolver.php b/app/BlueprintFramework/Migrations/MigrationResolver.php new file mode 100644 index 00000000..e00fe53f --- /dev/null +++ b/app/BlueprintFramework/Migrations/MigrationResolver.php @@ -0,0 +1,40 @@ +registry->all() as $migration) { + if ($this->repository->hasRun($migration->id())) { + continue; + } + if ($migration->appliesTo($currentVersion)) { + $pending[] = $migration; + } + } + return $pending; + } + + public function find(string $id): ?BlueprintMigrationInterface + { + return $this->registry->find($id); + } + + /** @return BlueprintMigrationInterface[] */ + public function all(): array + { + return $this->registry->all(); + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationRunner.php b/app/BlueprintFramework/Migrations/MigrationRunner.php new file mode 100644 index 00000000..69d34982 --- /dev/null +++ b/app/BlueprintFramework/Migrations/MigrationRunner.php @@ -0,0 +1,110 @@ +id(); + $started = now(); + $start = microtime(true); + + $this->repository->record(new MigrationRecord( + id: $id, + fromVersion: $fromVersion, + toVersion: $toVersion, + status: 'running', + startedAt: $started->toDateTimeString(), + rollbackable: $migration->isRollbackable(), + )); + + $context = new MigrationContext( + fromVersion: $fromVersion, + toVersion: $toVersion, + blueprint: $this->blueprint, + ); + + try { + $migration->up($context); + } catch (\Throwable $e) { + $duration = (int) ((microtime(true) - $start) * 1000); + $this->repository->record(new MigrationRecord( + id: $id, + fromVersion: $fromVersion, + toVersion: $toVersion, + status: 'failed', + startedAt: $started->toDateTimeString(), + completedAt: now()->toDateTimeString(), + durationMs: $duration, + rollbackable: $migration->isRollbackable(), + error: $e->getMessage(), + )); + + Log::error("Blueprint migration {$id} failed: {$e->getMessage()}"); + if ($logger) { + $logger("Migration {$id} failed: {$e->getMessage()}"); + } + return false; + } + + $duration = (int) ((microtime(true) - $start) * 1000); + $this->repository->record(new MigrationRecord( + id: $id, + fromVersion: $fromVersion, + toVersion: $toVersion, + status: 'success', + startedAt: $started->toDateTimeString(), + completedAt: now()->toDateTimeString(), + durationMs: $duration, + rollbackable: $migration->isRollbackable(), + )); + + Log::info("Blueprint migration {$id} succeeded ({$duration}ms)"); + if ($logger) { + $logger("Migration {$id} succeeded"); + } + return true; + } + + public function rollback(BlueprintMigrationInterface $migration): bool + { + if (!$migration->isRollbackable()) { + return false; + } + + $context = new MigrationContext( + fromVersion: $migration->toVersion() ?? '', + toVersion: $migration->fromVersion() ?? '', + blueprint: $this->blueprint, + ); + + try { + $migration->down($context); + } catch (\Throwable $e) { + Log::error("Rollback of {$migration->id()} failed: {$e->getMessage()}"); + return false; + } + + $record = $this->repository->find($migration->id()); + if ($record) { + $record->status = 'rolled_back'; + $record->save(); + } + + return true; + } +} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/Registry.php b/app/BlueprintFramework/Migrations/Registry.php new file mode 100644 index 00000000..f63a1fd5 --- /dev/null +++ b/app/BlueprintFramework/Migrations/Registry.php @@ -0,0 +1,38 @@ +classes() as $class) { + $instance = app()->make($class); + if ($instance instanceof BlueprintMigrationInterface) { + $instances[$instance->id()] = $instance; + } + } + ksort($instances); + return array_values($instances); + } + + public function find(string $id): ?BlueprintMigrationInterface + { + foreach ($this->all() as $m) { + if ($m->id() === $id) { + return $m; + } + } + return null; + } +} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrateCommand.php b/app/Console/Commands/BlueprintFramework/MigrateCommand.php new file mode 100644 index 00000000..72d22a3b --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/MigrateCommand.php @@ -0,0 +1,64 @@ +option('from') ?: $this->placeholder->version(); + $to = $this->option('to') ?: null; + + $pending = $this->manager->describePending($from); + + if (empty($pending)) { + $this->info('No pending Blueprint migrations.'); + return 0; + } + + $this->line('Pending Blueprint migrations:'); + foreach ($pending as $p) { + $this->line(sprintf(' %-40s %s', $p['id'], $p['description'])); + } + $this->newLine(); + + if (!$this->option('force') && !$this->confirm('Apply these migrations?', true)) { + $this->warn('Aborted.'); + return 1; + } + + $result = $this->manager->migrate( + fromVersion: $from, + targetVersion: $to, + logger: fn (string $m) => $this->line(" {$m}"), + ); + + if (!$result['ok']) { + $this->error($result['error'] ?? 'Migration failed.'); + if ($result['failed']) { + $this->error('Failed migration: ' . $result['failed']); + } + return 1; + } + + $this->info('Applied ' . count($result['ran']) . ' migration(s).'); + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php b/app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php new file mode 100644 index 00000000..d5763b44 --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php @@ -0,0 +1,41 @@ +manager->history(); + + if (empty($rows)) { + $this->line('No migrations have been run.'); + return 0; + } + + $this->table( + ['ID', 'From', 'To', 'Status', 'Run at', 'Duration'], + array_map(fn ($r) => [ + $r->id, + $r->from_version ?? '—', + $r->to_version ?? '—', + strtoupper($r->status), + optional($r->completed_at)->toDateTimeString() ?? '—', + $r->duration_ms !== null ? $r->duration_ms . 'ms' : '—', + ], $rows) + ); + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php b/app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php new file mode 100644 index 00000000..1ed7ba37 --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php @@ -0,0 +1,55 @@ +argument('id'); + + if ($id !== null) { + $migration = $this->manager->find($id); + if (!$migration) { + $this->error("Migration '{$id}' not found."); + return 1; + } + if (!$migration->isRollbackable()) { + $this->error('Migration cannot be automatically rolled back.'); + $this->newLine(); + $this->line('Reason:'); + $this->line(' This migration contains an irreversible data transformation.'); + $this->newLine(); + $this->line('Manual recovery information:'); + $this->line(' Restore from a Blueprint snapshot, or reinstall Blueprint at the desired version.'); + return 1; + } + } + + if (!$this->confirm('Proceed with rollback?', false)) { + $this->line('Aborted.'); + return 1; + } + + $result = $this->manager->rollback($id); + + if (!$result['ok']) { + $this->error($result['error'] ?? 'Rollback failed.'); + return 1; + } + + $this->info("Rolled back migration: {$result['id']}"); + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationShowCommand.php b/app/Console/Commands/BlueprintFramework/MigrationShowCommand.php new file mode 100644 index 00000000..449ef999 --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/MigrationShowCommand.php @@ -0,0 +1,43 @@ +argument('id'); + $record = $this->repository->find($id); + + if (!$record) { + $this->error("Migration '{$id}' not found."); + return 1; + } + + $this->line("ID: {$record->migration_id}"); + $this->line("From: {$record->from_version}"); + $this->line("To: {$record->to_version}"); + $this->line("Status: {$record->status}"); + $this->line("Started at: " . optional($record->started_at)->toDateTimeString()); + $this->line("Completed at: " . optional($record->completed_at)->toDateTimeString()); + $this->line("Duration: " . ($record->duration_ms !== null ? $record->duration_ms . 'ms' : '—')); + $this->line("Rollbackable: " . ($record->rollbackable ? 'yes' : 'no')); + if ($record->error) { + $this->newLine(); + $this->line("Error: {$record->error}"); + } + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php b/app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php new file mode 100644 index 00000000..47065107 --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php @@ -0,0 +1,72 @@ +placeholder->version(); + $this->line("Current version: {$current}"); + $this->newLine(); + + $pending = $this->manager->describePending($current); + if (empty($pending)) { + $this->line('Pending:'); + $this->line(' none'); + } else { + $this->line('Pending:'); + foreach ($pending as $p) { + $this->line(" {$p['id']} ({$p['description']})"); + } + } + $this->newLine(); + + $failed = $this->manager->failed(); + if (!empty($failed)) { + $this->line('Failed:'); + foreach ($failed as $f) { + $this->line(" {$f->migration_id} {$f->error}"); + } + $this->newLine(); + } + + $this->line('Extensions:'); + foreach (array_filter($this->blueprint->extensions()) as $id) { + $conf = $this->blueprint->extensionConfig($id); + $target = $conf['info']['target'] ?? ''; + if ($target === '' || $current === 'rolling' || $target === $current) { + $this->line(" ✓ {$id}"); + } else { + $this->line(" ⚠ {$id} (targets {$target})"); + } + } + $this->newLine(); + + $status = 'READY'; + if (!empty($failed)) { + $status = 'FAILED'; + } elseif (!empty($pending)) { + $status = 'PENDING'; + } + $this->line("Status: {$status}"); + + return 0; + } +} \ No newline at end of file diff --git a/app/Models/BlueprintMigration.php b/app/Models/BlueprintMigration.php new file mode 100644 index 00000000..f05d598b --- /dev/null +++ b/app/Models/BlueprintMigration.php @@ -0,0 +1,21 @@ + 'datetime', + 'completed_at' => 'datetime', + 'rollbackable' => 'boolean', + 'meta' => 'array', + ]; +} \ No newline at end of file diff --git a/database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php b/database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php new file mode 100644 index 00000000..6ed5d13a --- /dev/null +++ b/database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('migration_id')->unique(); + $table->string('from_version')->nullable(); + $table->string('to_version')->nullable(); + $table->string('status')->default('pending'); // pending|running|success|failed|rolled_back + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->unsignedInteger('duration_ms')->nullable(); + $table->boolean('rollbackable')->default(false); + $table->text('error')->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); + $table->index('status'); + $table->index('from_version'); + }); + } + + public function down(): void + { + Schema::dropIfExists('blueprint_migrations'); + } +}; \ No newline at end of file diff --git a/scripts/commands/advanced/migrate.sh b/scripts/commands/advanced/migrate.sh new file mode 100644 index 00000000..a03c8740 --- /dev/null +++ b/scripts/commands/advanced/migrate.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +Command() { + cd "$FOLDER" || cdhalt + + args=() + for arg in "$@"; do + case "$arg" in + --force|-f) args+=("--force") ;; + --from=*) args+=("$arg") ;; + --to=*) args+=("$arg") ;; + *) ;; + esac + done + + php artisan bp:migrate "${args[@]}" + exit $? +} \ No newline at end of file diff --git a/scripts/commands/advanced/migration.sh b/scripts/commands/advanced/migration.sh new file mode 100644 index 00000000..2a416963 --- /dev/null +++ b/scripts/commands/advanced/migration.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +Command() { + cd "$FOLDER" || cdhalt + + action="$1"; shift || true + + case "$action" in + status) php artisan bp:migration:status ;; + history) php artisan bp:migration:history ;; + show) php artisan bp:migration:show "$@" ;; + rollback) php artisan bp:migration:rollback "$@" ;; + *) + PRINT FATAL "Unknown migration action '$action'. Use: status|history|show|rollback." + exit 2 + ;; + esac + exit $? +} \ No newline at end of file From aaa46cc843517f4dfd6c6048290bea0433f05541 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:57:43 +0200 Subject: [PATCH 03/11] feat(cli): wire doctor and migration commands --- blueprint.sh | 5 ++++- scripts/commands/advanced/upgrade.sh | 16 ++++++++++++++++ scripts/commands/misc/help.sh | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/blueprint.sh b/blueprint.sh index fc113ac0..e0072db1 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 -migrate -migration" ;; esac if [[ ${cur} == * ]]; then @@ -472,6 +472,9 @@ 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 ;; + -migrate) source ./scripts/commands/advanced/migrate.sh ;; + -migration) source ./scripts/commands/advanced/migration.sh ;; esac shift 2 diff --git a/scripts/commands/advanced/upgrade.sh b/scripts/commands/advanced/upgrade.sh index 86d7a5f7..0b5dcbb2 100644 --- a/scripts/commands/advanced/upgrade.sh +++ b/scripts/commands/advanced/upgrade.sh @@ -140,6 +140,8 @@ Command() { ((PROGRESS_NOW++)) + echo "$VERSION" > .update/old_version + # Delete files PRINT INFO "Deleting files.." rm -rf .blueprint @@ -179,6 +181,20 @@ Command() { ((PROGRESS_NOW++)) + if [[ -f ".update/old_version" ]]; then + OLD_VERSION="$(cat .update/old_version)" + if [[ -n "$OLD_VERSION" ]]; then + PRINT INFO "Checking for pending Blueprint migrations from $OLD_VERSION.." + hide_progress + php artisan bp:migrate --force --from="$OLD_VERSION" + migration_exit=$? + if [[ $migration_exit -ne 0 ]]; then + update_fail="true" + PRINT FATAL "Blueprint migrations failed; please resolve before continuing." + fi + fi + fi + cleanup # Tell user that update has finished diff --git a/scripts/commands/misc/help.sh b/scripts/commands/misc/help.sh index f7805a67..f5826946 100644 --- a/scripts/commands/misc/help.sh +++ b/scripts/commands/misc/help.sh @@ -35,6 +35,9 @@ ${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 + -migrate (--force) run pending Blueprint migrations + -migration status|history|rollback|show -unlock remove lockfile -upgrade (remote ) update/reset to another release -rerun-install rerun the blueprint installation script From 5b9841d45d66498b59a3139ba77cf21dbe65a9a6 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:32:14 +0200 Subject: [PATCH 04/11] Fixed title line --- app/Console/Commands/BlueprintFramework/DoctorCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/BlueprintFramework/DoctorCommand.php b/app/Console/Commands/BlueprintFramework/DoctorCommand.php index 18ba92c1..f20f0273 100644 --- a/app/Console/Commands/BlueprintFramework/DoctorCommand.php +++ b/app/Console/Commands/BlueprintFramework/DoctorCommand.php @@ -61,7 +61,7 @@ private function renderReport(DoctorReport $report): void $this->newLine(); foreach ($report->results() as $result) { - $this->line('' . $result->title() . ''); + $this->line('' . $result->title . ''); foreach ($result->rows() as $row) { $marker = ' '; $markerColor = 'gray'; From 58d4f210631de50aec33c8fffba1ebe29eebb4f2 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:55:41 +0200 Subject: [PATCH 05/11] Remove migrate functions (Useless) --- .../Migrations/AbstractMigration.php | 26 ---- .../BlueprintMigrationInterface.php | 25 ---- .../Migrations/MigrationContext.php | 20 --- .../Migrations/MigrationLock.php | 49 ------- .../Migrations/MigrationManager.php | 129 ------------------ .../Migrations/MigrationRecord.php | 20 --- .../Migrations/MigrationRepository.php | 64 --------- .../Migrations/MigrationResolver.php | 40 ------ .../Migrations/MigrationRunner.php | 110 --------------- .../Migrations/Registry.php | 38 ------ .../BlueprintFramework/MigrateCommand.php | 64 --------- .../MigrationHistoryCommand.php | 41 ------ .../MigrationRollbackCommand.php | 55 -------- .../MigrationShowCommand.php | 43 ------ .../MigrationStatusCommand.php | 72 ---------- app/Models/BlueprintMigration.php | 21 --- blueprint.sh | 2 - ...0250_create_blueprint_migrations_table.php | 33 ----- scripts/commands/advanced/migrate.sh | 18 --- scripts/commands/advanced/migration.sh | 19 --- scripts/commands/misc/help.sh | 2 - 21 files changed, 891 deletions(-) delete mode 100644 app/BlueprintFramework/Migrations/AbstractMigration.php delete mode 100644 app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationContext.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationLock.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationManager.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationRecord.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationRepository.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationResolver.php delete mode 100644 app/BlueprintFramework/Migrations/MigrationRunner.php delete mode 100644 app/BlueprintFramework/Migrations/Registry.php delete mode 100644 app/Console/Commands/BlueprintFramework/MigrateCommand.php delete mode 100644 app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php delete mode 100644 app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php delete mode 100644 app/Console/Commands/BlueprintFramework/MigrationShowCommand.php delete mode 100644 app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php delete mode 100644 app/Models/BlueprintMigration.php delete mode 100644 database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php delete mode 100644 scripts/commands/advanced/migrate.sh delete mode 100644 scripts/commands/advanced/migration.sh diff --git a/app/BlueprintFramework/Migrations/AbstractMigration.php b/app/BlueprintFramework/Migrations/AbstractMigration.php deleted file mode 100644 index a34a4cee..00000000 --- a/app/BlueprintFramework/Migrations/AbstractMigration.php +++ /dev/null @@ -1,26 +0,0 @@ -fromVersion() === null) { - return true; - } - return $this->fromVersion() === $installedVersion; - } - - public function down(MigrationContext $context): void - { - throw new \RuntimeException( - 'Migration ' . $this->id() . ' does not implement rollback.' - ); - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php b/app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php deleted file mode 100644 index 72efe2eb..00000000 --- a/app/BlueprintFramework/Migrations/BlueprintMigrationInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -lockPath = base_path('.blueprint/migration.lock'); - } - - public function acquire(): bool - { - $dir = dirname($this->lockPath); - if (!is_dir($dir)) { - @mkdir($dir, 0755, true); - } - - if (file_exists($this->lockPath)) { - // Detect stale lock (> 4 hours old) - $age = time() - (int) @filemtime($this->lockPath); - if ($age > 14400) { - @unlink($this->lockPath); - } else { - return false; - } - } - - $handle = @fopen($this->lockPath, 'x'); - if ($handle === false) { - return false; - } - fwrite($handle, (string) getmypid()); - fclose($handle); - return true; - } - - public function release(): void - { - @unlink($this->lockPath); - } - - public function isHeld(): bool - { - return file_exists($this->lockPath); - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationManager.php b/app/BlueprintFramework/Migrations/MigrationManager.php deleted file mode 100644 index b5843201..00000000 --- a/app/BlueprintFramework/Migrations/MigrationManager.php +++ /dev/null @@ -1,129 +0,0 @@ -resolver->pending($currentVersion); - } - - /** @return array */ - public function describePending(string $currentVersion): array - { - return array_map(fn (BlueprintMigrationInterface $m) => [ - 'id' => $m->id(), - 'from' => $m->fromVersion(), - 'to' => $m->toVersion(), - 'description' => $m->description(), - 'rollbackable' => $m->isRollbackable(), - ], $this->pending($currentVersion)); - } - - public function failed(): array - { - return $this->repository->failed(); - } - - public function history(): array - { - return $this->repository->all(); - } - - public function find(string $id): ?BlueprintMigrationInterface - { - return $this->resolver->find($id); - } - - /** - * @return array{ok:bool,ran:string[],failed:?string,error:?string} - */ - public function migrate(?string $fromVersion = null, ?string $targetVersion = null, ?callable $logger = null): array - { - if ($this->lock->isHeld()) { - return ['ok' => false, 'ran' => [], 'failed' => null, 'error' => 'A migration is already running.']; - } - if (!$this->lock->acquire()) { - return ['ok' => false, 'ran' => [], 'failed' => null, 'error' => 'Could not acquire migration lock.']; - } - - $ran = []; - try { - $current = $fromVersion ?? $this->placeholder->version(); - $pending = $this->pending($current); - - foreach ($pending as $migration) { - $target = $targetVersion ?? $migration->toVersion() ?? $current; - $ok = $this->runner->run($migration, $current, $target, $logger); - if (!$ok) { - return ['ok' => false, 'ran' => $ran, 'failed' => $migration->id(), 'error' => 'Migration failed.']; - } - $ran[] = $migration->id(); - $current = $target; - } - } finally { - $this->lock->release(); - } - - return ['ok' => true, 'ran' => $ran, 'failed' => null, 'error' => null]; - } - - /** - * @return array{ok:bool,id:?string,error:?string} - */ - public function rollback(?string $id = null): array - { - if ($this->lock->isHeld()) { - return ['ok' => false, 'id' => null, 'error' => 'A migration is already running.']; - } - if (!$this->lock->acquire()) { - return ['ok' => false, 'id' => null, 'error' => 'Could not acquire migration lock.']; - } - - try { - $migration = null; - if ($id !== null) { - $migration = $this->resolver->find($id); - if (!$migration) { - return ['ok' => false, 'id' => $id, 'error' => "Migration '{$id}' not found."]; - } - if (!$migration->isRollbackable()) { - return ['ok' => false, 'id' => $id, 'error' => 'Migration is not rollbackable.']; - } - } else { - // Most recent successful rollbackable migration - $history = array_reverse($this->repository->all()); - foreach ($history as $record) { - if ($record->status === 'success' && $record->rollbackable) { - $migration = $this->resolver->find($record->migration_id); - if ($migration) { - $id = $migration->id(); - break; - } - } - } - if (!$migration) { - return ['ok' => false, 'id' => null, 'error' => 'No rollbackable migration found.']; - } - } - - $ok = $this->runner->rollback($migration); - return ['ok' => $ok, 'id' => $migration->id(), 'error' => $ok ? null : 'Rollback failed.']; - } finally { - $this->lock->release(); - } - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationRecord.php b/app/BlueprintFramework/Migrations/MigrationRecord.php deleted file mode 100644 index 11aef029..00000000 --- a/app/BlueprintFramework/Migrations/MigrationRecord.php +++ /dev/null @@ -1,20 +0,0 @@ - $record->id], - [ - 'from_version' => $record->fromVersion, - 'to_version' => $record->toVersion, - 'status' => $record->status, - 'started_at' => $record->startedAt, - 'completed_at' => $record->completedAt, - 'duration_ms' => $record->durationMs, - 'rollbackable' => $record->rollbackable, - 'error' => $record->error, - 'meta' => $record->meta, - ] - ); - } - - public function find(string $id): ?BlueprintMigration - { - return BlueprintMigration::where('migration_id', $id)->first(); - } - - /** @return BlueprintMigration[] */ - public function all(): array - { - return BlueprintMigration::orderBy('id')->get()->all(); - } - - public function hasRun(string $id): bool - { - return BlueprintMigration::where('migration_id', $id) - ->where('status', 'success') - ->exists(); - } - - public function hasFailed(string $id): bool - { - return BlueprintMigration::where('migration_id', $id) - ->where('status', 'failed') - ->exists(); - } - - /** @return BlueprintMigration[] */ - public function failed(): array - { - return BlueprintMigration::where('status', 'failed')->get()->all(); - } - - public function lastSuccessful(): ?BlueprintMigration - { - return BlueprintMigration::where('status', 'success') - ->orderByDesc('id') - ->first(); - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationResolver.php b/app/BlueprintFramework/Migrations/MigrationResolver.php deleted file mode 100644 index e00fe53f..00000000 --- a/app/BlueprintFramework/Migrations/MigrationResolver.php +++ /dev/null @@ -1,40 +0,0 @@ -registry->all() as $migration) { - if ($this->repository->hasRun($migration->id())) { - continue; - } - if ($migration->appliesTo($currentVersion)) { - $pending[] = $migration; - } - } - return $pending; - } - - public function find(string $id): ?BlueprintMigrationInterface - { - return $this->registry->find($id); - } - - /** @return BlueprintMigrationInterface[] */ - public function all(): array - { - return $this->registry->all(); - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/MigrationRunner.php b/app/BlueprintFramework/Migrations/MigrationRunner.php deleted file mode 100644 index 69d34982..00000000 --- a/app/BlueprintFramework/Migrations/MigrationRunner.php +++ /dev/null @@ -1,110 +0,0 @@ -id(); - $started = now(); - $start = microtime(true); - - $this->repository->record(new MigrationRecord( - id: $id, - fromVersion: $fromVersion, - toVersion: $toVersion, - status: 'running', - startedAt: $started->toDateTimeString(), - rollbackable: $migration->isRollbackable(), - )); - - $context = new MigrationContext( - fromVersion: $fromVersion, - toVersion: $toVersion, - blueprint: $this->blueprint, - ); - - try { - $migration->up($context); - } catch (\Throwable $e) { - $duration = (int) ((microtime(true) - $start) * 1000); - $this->repository->record(new MigrationRecord( - id: $id, - fromVersion: $fromVersion, - toVersion: $toVersion, - status: 'failed', - startedAt: $started->toDateTimeString(), - completedAt: now()->toDateTimeString(), - durationMs: $duration, - rollbackable: $migration->isRollbackable(), - error: $e->getMessage(), - )); - - Log::error("Blueprint migration {$id} failed: {$e->getMessage()}"); - if ($logger) { - $logger("Migration {$id} failed: {$e->getMessage()}"); - } - return false; - } - - $duration = (int) ((microtime(true) - $start) * 1000); - $this->repository->record(new MigrationRecord( - id: $id, - fromVersion: $fromVersion, - toVersion: $toVersion, - status: 'success', - startedAt: $started->toDateTimeString(), - completedAt: now()->toDateTimeString(), - durationMs: $duration, - rollbackable: $migration->isRollbackable(), - )); - - Log::info("Blueprint migration {$id} succeeded ({$duration}ms)"); - if ($logger) { - $logger("Migration {$id} succeeded"); - } - return true; - } - - public function rollback(BlueprintMigrationInterface $migration): bool - { - if (!$migration->isRollbackable()) { - return false; - } - - $context = new MigrationContext( - fromVersion: $migration->toVersion() ?? '', - toVersion: $migration->fromVersion() ?? '', - blueprint: $this->blueprint, - ); - - try { - $migration->down($context); - } catch (\Throwable $e) { - Log::error("Rollback of {$migration->id()} failed: {$e->getMessage()}"); - return false; - } - - $record = $this->repository->find($migration->id()); - if ($record) { - $record->status = 'rolled_back'; - $record->save(); - } - - return true; - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Migrations/Registry.php b/app/BlueprintFramework/Migrations/Registry.php deleted file mode 100644 index f63a1fd5..00000000 --- a/app/BlueprintFramework/Migrations/Registry.php +++ /dev/null @@ -1,38 +0,0 @@ -classes() as $class) { - $instance = app()->make($class); - if ($instance instanceof BlueprintMigrationInterface) { - $instances[$instance->id()] = $instance; - } - } - ksort($instances); - return array_values($instances); - } - - public function find(string $id): ?BlueprintMigrationInterface - { - foreach ($this->all() as $m) { - if ($m->id() === $id) { - return $m; - } - } - return null; - } -} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrateCommand.php b/app/Console/Commands/BlueprintFramework/MigrateCommand.php deleted file mode 100644 index 72d22a3b..00000000 --- a/app/Console/Commands/BlueprintFramework/MigrateCommand.php +++ /dev/null @@ -1,64 +0,0 @@ -option('from') ?: $this->placeholder->version(); - $to = $this->option('to') ?: null; - - $pending = $this->manager->describePending($from); - - if (empty($pending)) { - $this->info('No pending Blueprint migrations.'); - return 0; - } - - $this->line('Pending Blueprint migrations:'); - foreach ($pending as $p) { - $this->line(sprintf(' %-40s %s', $p['id'], $p['description'])); - } - $this->newLine(); - - if (!$this->option('force') && !$this->confirm('Apply these migrations?', true)) { - $this->warn('Aborted.'); - return 1; - } - - $result = $this->manager->migrate( - fromVersion: $from, - targetVersion: $to, - logger: fn (string $m) => $this->line(" {$m}"), - ); - - if (!$result['ok']) { - $this->error($result['error'] ?? 'Migration failed.'); - if ($result['failed']) { - $this->error('Failed migration: ' . $result['failed']); - } - return 1; - } - - $this->info('Applied ' . count($result['ran']) . ' migration(s).'); - return 0; - } -} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php b/app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php deleted file mode 100644 index d5763b44..00000000 --- a/app/Console/Commands/BlueprintFramework/MigrationHistoryCommand.php +++ /dev/null @@ -1,41 +0,0 @@ -manager->history(); - - if (empty($rows)) { - $this->line('No migrations have been run.'); - return 0; - } - - $this->table( - ['ID', 'From', 'To', 'Status', 'Run at', 'Duration'], - array_map(fn ($r) => [ - $r->id, - $r->from_version ?? '—', - $r->to_version ?? '—', - strtoupper($r->status), - optional($r->completed_at)->toDateTimeString() ?? '—', - $r->duration_ms !== null ? $r->duration_ms . 'ms' : '—', - ], $rows) - ); - - return 0; - } -} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php b/app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php deleted file mode 100644 index 1ed7ba37..00000000 --- a/app/Console/Commands/BlueprintFramework/MigrationRollbackCommand.php +++ /dev/null @@ -1,55 +0,0 @@ -argument('id'); - - if ($id !== null) { - $migration = $this->manager->find($id); - if (!$migration) { - $this->error("Migration '{$id}' not found."); - return 1; - } - if (!$migration->isRollbackable()) { - $this->error('Migration cannot be automatically rolled back.'); - $this->newLine(); - $this->line('Reason:'); - $this->line(' This migration contains an irreversible data transformation.'); - $this->newLine(); - $this->line('Manual recovery information:'); - $this->line(' Restore from a Blueprint snapshot, or reinstall Blueprint at the desired version.'); - return 1; - } - } - - if (!$this->confirm('Proceed with rollback?', false)) { - $this->line('Aborted.'); - return 1; - } - - $result = $this->manager->rollback($id); - - if (!$result['ok']) { - $this->error($result['error'] ?? 'Rollback failed.'); - return 1; - } - - $this->info("Rolled back migration: {$result['id']}"); - return 0; - } -} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationShowCommand.php b/app/Console/Commands/BlueprintFramework/MigrationShowCommand.php deleted file mode 100644 index 449ef999..00000000 --- a/app/Console/Commands/BlueprintFramework/MigrationShowCommand.php +++ /dev/null @@ -1,43 +0,0 @@ -argument('id'); - $record = $this->repository->find($id); - - if (!$record) { - $this->error("Migration '{$id}' not found."); - return 1; - } - - $this->line("ID: {$record->migration_id}"); - $this->line("From: {$record->from_version}"); - $this->line("To: {$record->to_version}"); - $this->line("Status: {$record->status}"); - $this->line("Started at: " . optional($record->started_at)->toDateTimeString()); - $this->line("Completed at: " . optional($record->completed_at)->toDateTimeString()); - $this->line("Duration: " . ($record->duration_ms !== null ? $record->duration_ms . 'ms' : '—')); - $this->line("Rollbackable: " . ($record->rollbackable ? 'yes' : 'no')); - if ($record->error) { - $this->newLine(); - $this->line("Error: {$record->error}"); - } - - return 0; - } -} \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php b/app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php deleted file mode 100644 index 47065107..00000000 --- a/app/Console/Commands/BlueprintFramework/MigrationStatusCommand.php +++ /dev/null @@ -1,72 +0,0 @@ -placeholder->version(); - $this->line("Current version: {$current}"); - $this->newLine(); - - $pending = $this->manager->describePending($current); - if (empty($pending)) { - $this->line('Pending:'); - $this->line(' none'); - } else { - $this->line('Pending:'); - foreach ($pending as $p) { - $this->line(" {$p['id']} ({$p['description']})"); - } - } - $this->newLine(); - - $failed = $this->manager->failed(); - if (!empty($failed)) { - $this->line('Failed:'); - foreach ($failed as $f) { - $this->line(" {$f->migration_id} {$f->error}"); - } - $this->newLine(); - } - - $this->line('Extensions:'); - foreach (array_filter($this->blueprint->extensions()) as $id) { - $conf = $this->blueprint->extensionConfig($id); - $target = $conf['info']['target'] ?? ''; - if ($target === '' || $current === 'rolling' || $target === $current) { - $this->line(" ✓ {$id}"); - } else { - $this->line(" ⚠ {$id} (targets {$target})"); - } - } - $this->newLine(); - - $status = 'READY'; - if (!empty($failed)) { - $status = 'FAILED'; - } elseif (!empty($pending)) { - $status = 'PENDING'; - } - $this->line("Status: {$status}"); - - return 0; - } -} \ No newline at end of file diff --git a/app/Models/BlueprintMigration.php b/app/Models/BlueprintMigration.php deleted file mode 100644 index f05d598b..00000000 --- a/app/Models/BlueprintMigration.php +++ /dev/null @@ -1,21 +0,0 @@ - 'datetime', - 'completed_at' => 'datetime', - 'rollbackable' => 'boolean', - 'meta' => 'array', - ]; -} \ No newline at end of file diff --git a/blueprint.sh b/blueprint.sh index e0072db1..0669a71a 100644 --- a/blueprint.sh +++ b/blueprint.sh @@ -473,8 +473,6 @@ case "$cmd" in -upgrade) source ./scripts/commands/advanced/upgrade.sh ;; -unlock) source ./scripts/commands/advanced/unlock.sh ;; -doctor) source ./scripts/commands/advanced/doctor.sh ;; - -migrate) source ./scripts/commands/advanced/migrate.sh ;; - -migration) source ./scripts/commands/advanced/migration.sh ;; esac shift 2 diff --git a/database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php b/database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php deleted file mode 100644 index 6ed5d13a..00000000 --- a/database/migrations/2026_05_11_000250_create_blueprint_migrations_table.php +++ /dev/null @@ -1,33 +0,0 @@ -id(); - $table->string('migration_id')->unique(); - $table->string('from_version')->nullable(); - $table->string('to_version')->nullable(); - $table->string('status')->default('pending'); // pending|running|success|failed|rolled_back - $table->timestamp('started_at')->nullable(); - $table->timestamp('completed_at')->nullable(); - $table->unsignedInteger('duration_ms')->nullable(); - $table->boolean('rollbackable')->default(false); - $table->text('error')->nullable(); - $table->json('meta')->nullable(); - $table->timestamps(); - $table->index('status'); - $table->index('from_version'); - }); - } - - public function down(): void - { - Schema::dropIfExists('blueprint_migrations'); - } -}; \ No newline at end of file diff --git a/scripts/commands/advanced/migrate.sh b/scripts/commands/advanced/migrate.sh deleted file mode 100644 index a03c8740..00000000 --- a/scripts/commands/advanced/migrate.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -Command() { - cd "$FOLDER" || cdhalt - - args=() - for arg in "$@"; do - case "$arg" in - --force|-f) args+=("--force") ;; - --from=*) args+=("$arg") ;; - --to=*) args+=("$arg") ;; - *) ;; - esac - done - - php artisan bp:migrate "${args[@]}" - exit $? -} \ No newline at end of file diff --git a/scripts/commands/advanced/migration.sh b/scripts/commands/advanced/migration.sh deleted file mode 100644 index 2a416963..00000000 --- a/scripts/commands/advanced/migration.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -Command() { - cd "$FOLDER" || cdhalt - - action="$1"; shift || true - - case "$action" in - status) php artisan bp:migration:status ;; - history) php artisan bp:migration:history ;; - show) php artisan bp:migration:show "$@" ;; - rollback) php artisan bp:migration:rollback "$@" ;; - *) - PRINT FATAL "Unknown migration action '$action'. Use: status|history|show|rollback." - exit 2 - ;; - esac - exit $? -} \ No newline at end of file diff --git a/scripts/commands/misc/help.sh b/scripts/commands/misc/help.sh index f5826946..721fb385 100644 --- a/scripts/commands/misc/help.sh +++ b/scripts/commands/misc/help.sh @@ -36,8 +36,6 @@ ${help_dev_primary}Developer${help_dev_status}\x1b[0m${help_dev_secondary} \x1b[34;1mAdvanced\x1b[0m\x1b[34m -doctor (--fix --json) run Blueprint diagnostics - -migrate (--force) run pending Blueprint migrations - -migration status|history|rollback|show -unlock remove lockfile -upgrade (remote ) update/reset to another release -rerun-install rerun the blueprint installation script From 98224cf90e6613e6ba174757f9fc67a89f3ef050 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:03:27 +0200 Subject: [PATCH 06/11] Fix doctor command (Remove the last migration file) --- .../Doctor/Checks/MigrationCheck.php | 68 ------------------- .../Doctor/DoctorService.php | 3 - 2 files changed, 71 deletions(-) delete mode 100644 app/BlueprintFramework/Doctor/Checks/MigrationCheck.php diff --git a/app/BlueprintFramework/Doctor/Checks/MigrationCheck.php b/app/BlueprintFramework/Doctor/Checks/MigrationCheck.php deleted file mode 100644 index 6cfffd6f..00000000 --- a/app/BlueprintFramework/Doctor/Checks/MigrationCheck.php +++ /dev/null @@ -1,68 +0,0 @@ -name(), $this->title()); - - try { - $current = $this->placeholder->version(); - $pending = $this->manager->pending($current); - - if (empty($pending)) { - $r->ok('State', 'Up to date'); - } else { - $r->warn('State', count($pending) . ' pending'); - $r->issue(new Issue( - Severity::WARNING, - 'Pending Blueprint migrations', - 'Migrations: ' . implode(', ', array_map( - fn ($m) => $m->id(), - $pending - )), - 'Run: blueprint migrate', - true, - )); - } - - $failed = $this->manager->failed(); - if (!empty($failed)) { - $r->err('Failed', count($failed) . ' failed'); - $r->issue(new Issue( - Severity::ERROR, - 'Failed Blueprint migrations detected', - 'Run `blueprint migration history` for details.', - 'Resolve the failure and rerun `blueprint migrate`.', - )); - } - } catch (\Throwable $e) { - $r->warn('State', 'Unavailable'); - $r->issue(new Issue( - Severity::WARNING, - 'Migration state unavailable', - $e->getMessage(), - )); - } - - return $r; - } -} \ No newline at end of file diff --git a/app/BlueprintFramework/Doctor/DoctorService.php b/app/BlueprintFramework/Doctor/DoctorService.php index 498d02c3..0d303242 100644 --- a/app/BlueprintFramework/Doctor/DoctorService.php +++ b/app/BlueprintFramework/Doctor/DoctorService.php @@ -9,7 +9,6 @@ use Pterodactyl\BlueprintFramework\Doctor\Checks\EnvironmentCheck; use Pterodactyl\BlueprintFramework\Doctor\Checks\ExtensionCheck; use Pterodactyl\BlueprintFramework\Doctor\Checks\IntegrityCheck; -use Pterodactyl\BlueprintFramework\Doctor\Checks\MigrationCheck; use Pterodactyl\BlueprintFramework\Doctor\Checks\PterodactylCheck; use Illuminate\Support\Facades\Artisan; @@ -27,7 +26,6 @@ public function __construct( CacheCheck $cache, ExtensionCheck $extensions, IntegrityCheck $integrity, - MigrationCheck $migrations, ) { $this->checks = [ $environment, @@ -38,7 +36,6 @@ public function __construct( $cache, $extensions, $integrity, - $migrations, ]; } From ee45d34ea6f9d455cc25d66c5f626e48a6bf4075 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:10:48 +0200 Subject: [PATCH 07/11] fix(doctor): use syntax for colors --- .gitignore | 1 + app/Console/Commands/BlueprintFramework/DoctorCommand.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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/Console/Commands/BlueprintFramework/DoctorCommand.php b/app/Console/Commands/BlueprintFramework/DoctorCommand.php index f20f0273..722a8547 100644 --- a/app/Console/Commands/BlueprintFramework/DoctorCommand.php +++ b/app/Console/Commands/BlueprintFramework/DoctorCommand.php @@ -70,10 +70,10 @@ private function renderReport(DoctorReport $report): void elseif ($row['status'] === Severity::ERROR || $row['status'] === Severity::CRITICAL) { $marker = '✗'; $markerColor = 'red'; } $this->line(sprintf( - ' %-18s %-22s <%s>%s', + ' %-18s %-22s %s', $row['label'], $row['value'], - $markerColor, + $markerColor, // "green", "yellow", "red" $marker, )); } From 52a9d3f5781c7f20ae3be0d1c863e3ceeaeb99f8 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:17:40 +0200 Subject: [PATCH 08/11] feat(Doctor): More checks --- .../Doctor/Checks/LogCheck.php | 77 +++++++++++++++ .../Doctor/Checks/PermissionsCheck.php | 92 +++++++++++++++++ .../Doctor/Checks/QueueCheck.php | 73 ++++++++++++++ .../Doctor/Checks/ResourceCheck.php | 70 +++++++++++++ .../Doctor/Checks/RouteCheck.php | 99 +++++++++++++++++++ .../Doctor/Checks/SecurityCheck.php | 93 +++++++++++++++++ .../Doctor/Checks/StorageCheck.php | 86 ++++++++++++++++ .../Doctor/Checks/VersionCheck.php | 68 +++++++++++++ .../Doctor/DoctorService.php | 70 +++++++++---- .../BlueprintFramework/DoctorCommand.php | 10 +- 10 files changed, 716 insertions(+), 22 deletions(-) create mode 100644 app/BlueprintFramework/Doctor/Checks/LogCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/QueueCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/ResourceCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/RouteCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/SecurityCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/StorageCheck.php create mode 100644 app/BlueprintFramework/Doctor/Checks/VersionCheck.php 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..92689022 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php @@ -0,0 +1,92 @@ +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', + )); + } + + $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', + )); + } + } + + 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..ede21d2f --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php @@ -0,0 +1,70 @@ +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); + $usedPct = round(100 - ($free / $total) * 100, 1); + + $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.', + )); + } elseif ($freeGb < 5) { + $r->issue(new Issue( + Severity::WARNING, + 'Low disk space', + "Only {$freeGb} GB free.", + 'Consider freeing disk space.', + )); + } + } 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..8e728235 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php @@ -0,0 +1,93 @@ +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', + )); + } + } + + 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..5f8b2d98 --- /dev/null +++ b/app/BlueprintFramework/Doctor/Checks/StorageCheck.php @@ -0,0 +1,86 @@ +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', + )); + } + + $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/DoctorService.php b/app/BlueprintFramework/Doctor/DoctorService.php index 0d303242..c7d4d113 100644 --- a/app/BlueprintFramework/Doctor/DoctorService.php +++ b/app/BlueprintFramework/Doctor/DoctorService.php @@ -9,7 +9,15 @@ use Pterodactyl\BlueprintFramework\Doctor\Checks\EnvironmentCheck; use Pterodactyl\BlueprintFramework\Doctor\Checks\ExtensionCheck; use Pterodactyl\BlueprintFramework\Doctor\Checks\IntegrityCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\LogCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\PermissionsCheck; use Pterodactyl\BlueprintFramework\Doctor\Checks\PterodactylCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\QueueCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\ResourceCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\RouteCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\SecurityCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\StorageCheck; +use Pterodactyl\BlueprintFramework\Doctor\Checks\VersionCheck; use Illuminate\Support\Facades\Artisan; class DoctorService @@ -21,11 +29,19 @@ public function __construct( EnvironmentCheck $environment, BlueprintCheck $blueprint, PterodactylCheck $pterodactyl, - DatabaseCheck $database, ConfigurationCheck $configuration, + DatabaseCheck $database, CacheCheck $cache, ExtensionCheck $extensions, IntegrityCheck $integrity, + QueueCheck $queue, + PermissionsCheck $permissions, + StorageCheck $storage, + LogCheck $logs, + SecurityCheck $security, + RouteCheck $routes, + VersionCheck $version, + ResourceCheck $resources, ) { $this->checks = [ $environment, @@ -36,6 +52,14 @@ public function __construct( $cache, $extensions, $integrity, + $queue, + $permissions, + $storage, + $logs, + $security, + $routes, + $version, + $resources, ]; } @@ -60,8 +84,6 @@ public function run(): DoctorReport } /** - * Apply safe, deterministic fixes only. - * * @return array */ public function fix(DoctorReport $report): array @@ -88,7 +110,6 @@ private function applyFix(Issue $issue): ?array { $title = $issue->title; - // Stale config cache if (str_contains($title, 'Configuration cache')) { try { Artisan::call('config:clear'); @@ -99,7 +120,6 @@ private function applyFix(Issue $issue): ?array } } - // Blueprint cache if (str_contains($title, 'Blueprint cache')) { try { Artisan::call('bp:cache'); @@ -109,7 +129,6 @@ private function applyFix(Issue $issue): ?array } } - // Pending Laravel migrations if (str_contains($title, 'Pending database migrations')) { try { Artisan::call('migrate', ['--force' => true]); @@ -119,17 +138,6 @@ private function applyFix(Issue $issue): ?array } } - // Pending Blueprint migrations - if (str_contains($title, 'Pending Blueprint migrations')) { - try { - Artisan::call('bp:migrate', ['--force' => true]); - return ['label' => 'Applied pending Blueprint migrations', 'ok' => true, 'detail' => null]; - } catch (\Throwable $e) { - return ['label' => 'Apply pending Blueprint migrations', 'ok' => false, 'detail' => $e->getMessage()]; - } - } - - // Install marker if (str_contains($title, 'installation incomplete')) { $path = base_path('.blueprint/extensions/blueprint/private/db/is_installed'); if (@touch($path)) { @@ -138,7 +146,6 @@ private function applyFix(Issue $issue): ?array return ['label' => 'Recreate is_installed marker', 'ok' => false, 'detail' => 'Permission denied']; } - // Blueprint seed if (str_contains($title, 'configuration not seeded')) { try { Artisan::call('db:seed', ['--class' => 'BlueprintSeeder', '--force' => true]); @@ -148,6 +155,33 @@ private function applyFix(Issue $issue): ?array } } + 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()]; + } + } + return null; } } \ No newline at end of file diff --git a/app/Console/Commands/BlueprintFramework/DoctorCommand.php b/app/Console/Commands/BlueprintFramework/DoctorCommand.php index 722a8547..1e2c212b 100644 --- a/app/Console/Commands/BlueprintFramework/DoctorCommand.php +++ b/app/Console/Commands/BlueprintFramework/DoctorCommand.php @@ -70,10 +70,10 @@ private function renderReport(DoctorReport $report): void elseif ($row['status'] === Severity::ERROR || $row['status'] === Severity::CRITICAL) { $marker = '✗'; $markerColor = 'red'; } $this->line(sprintf( - ' %-18s %-22s %s', + ' %-18s %-30s %s', $row['label'], $row['value'], - $markerColor, // "green", "yellow", "red" + $markerColor, $marker, )); } @@ -97,8 +97,10 @@ private function renderReport(DoctorReport $report): void $this->line('────────────────────────────────'); $this->line('Result:'); - $this->line(' ' . $report->countBySeverity(Severity::WARNING) . ' warning' . ($report->countBySeverity(Severity::WARNING) === 1 ? '' : 's')); - $this->line(' ' . ($report->countBySeverity(Severity::ERROR) + $report->countBySeverity(Severity::CRITICAL)) . ' error' . (($report->countBySeverity(Severity::ERROR) + $report->countBySeverity(Severity::CRITICAL)) === 1 ? '' : 's')); + $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 From c09fb2f4cef9903383847930622903117dd37115 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:27:04 +0200 Subject: [PATCH 09/11] feat(Doctor): Added more autofix --- .../Doctor/Checks/PermissionsCheck.php | 2 + .../Doctor/Checks/ResourceCheck.php | 3 +- .../Doctor/Checks/SecurityCheck.php | 1 + .../Doctor/Checks/StorageCheck.php | 5 +- .../Doctor/DoctorService.php | 121 ++++++++++++++++++ 5 files changed, 129 insertions(+), 3 deletions(-) diff --git a/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php b/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php index 92689022..f8c139e4 100644 --- a/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php +++ b/app/BlueprintFramework/Doctor/Checks/PermissionsCheck.php @@ -45,6 +45,7 @@ public function run(): CheckResult '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, )); } @@ -83,6 +84,7 @@ public function run(): CheckResult '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, )); } } diff --git a/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php b/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php index ede21d2f..3b052496 100644 --- a/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php +++ b/app/BlueprintFramework/Doctor/Checks/ResourceCheck.php @@ -21,7 +21,6 @@ public function run(): CheckResult if ($free !== false && $total !== false && $total > 0) { $freeGb = round($free / 1024 / 1024 / 1024, 2); $totalGb = round($total / 1024 / 1024 / 1024, 2); - $usedPct = round(100 - ($free / $total) * 100, 1); $status = Severity::INFO; if ($freeGb < 1) { @@ -38,6 +37,7 @@ public function run(): CheckResult '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( @@ -45,6 +45,7 @@ public function run(): CheckResult 'Low disk space', "Only {$freeGb} GB free.", 'Consider freeing disk space.', + true, )); } } else { diff --git a/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php b/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php index 8e728235..998b11cc 100644 --- a/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php +++ b/app/BlueprintFramework/Doctor/Checks/SecurityCheck.php @@ -84,6 +84,7 @@ public function run(): CheckResult '.env file permissions are too permissive', "Current: {$perms}, recommended: 0600 or 0640", 'Run: chmod 640 .env', + true, )); } } diff --git a/app/BlueprintFramework/Doctor/Checks/StorageCheck.php b/app/BlueprintFramework/Doctor/Checks/StorageCheck.php index 5f8b2d98..a85417a0 100644 --- a/app/BlueprintFramework/Doctor/Checks/StorageCheck.php +++ b/app/BlueprintFramework/Doctor/Checks/StorageCheck.php @@ -26,7 +26,7 @@ public function run(): CheckResult 'public/storage link is missing', 'The Laravel storage link has not been created.', 'Run: php artisan storage:link', - true, + true, )); } @@ -40,6 +40,7 @@ public function run(): CheckResult 'Blueprint public link is missing', 'The Blueprint extension public directory is not linked.', 'Rerun the installer: blueprint -rerun-install', + true, )); } @@ -53,7 +54,7 @@ public function run(): CheckResult 'Schedules directory missing', 'Extension schedules cannot be registered.', 'Create the directory: mkdir -p app/BlueprintFramework/Schedules', - true, + true, )); } diff --git a/app/BlueprintFramework/Doctor/DoctorService.php b/app/BlueprintFramework/Doctor/DoctorService.php index c7d4d113..e91d41fb 100644 --- a/app/BlueprintFramework/Doctor/DoctorService.php +++ b/app/BlueprintFramework/Doctor/DoctorService.php @@ -140,6 +140,10 @@ private function applyFix(Issue $issue): ?array 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]; } @@ -182,6 +186,123 @@ private function applyFix(Issue $issue): ?array } } + 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 From 22537191e2590098fb6a6e33c5d4ee43605dd84b Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:30:33 +0200 Subject: [PATCH 10/11] Remove the last things from migrate thing... --- app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php | 3 --- blueprint.sh | 2 +- scripts/commands/advanced/doctor.sh | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php b/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php index d2b565c6..b9db2fce 100644 --- a/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php +++ b/app/BlueprintFramework/Doctor/Checks/ExtensionCheck.php @@ -75,7 +75,6 @@ public function run(): CheckResult $version = $info['version'] ?? 'unknown'; $target = $info['target'] ?? ''; - // Compatibility $compatible = true; $reason = ''; if ($target !== '' && $current !== 'rolling' && $target !== $current) { @@ -99,14 +98,12 @@ public function run(): CheckResult )); } - // Required dependencies (if declared) $requires = $conf['requires'] ?? []; foreach ((array) $requires as $dep => $constraint) { if (!is_string($dep)) { continue; } if (str_starts_with($dep, 'blueprint/')) { - // Blueprint core dependency $satisfied = $this->satisfiesBlueprintConstraint((string) $constraint, $current); if (!$satisfied) { $r->issue(new Issue( diff --git a/blueprint.sh b/blueprint.sh index 0669a71a..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 -doctor -migrate -migration" ;; + *) opts="-install -add -remove -query -init -build -export -wipe -version -help -info -debug -upgrade -unlock -rerun-install -dist -doctor" ;; esac if [[ ${cur} == * ]]; then diff --git a/scripts/commands/advanced/doctor.sh b/scripts/commands/advanced/doctor.sh index f673b7c0..5f37e07f 100644 --- a/scripts/commands/advanced/doctor.sh +++ b/scripts/commands/advanced/doctor.sh @@ -3,7 +3,6 @@ Command() { cd "$FOLDER" || cdhalt - # Forward arguments directly to the artisan command. php artisan bp:doctor "$@" exit $? } \ No newline at end of file From 070ec53546bf34542b15d9a57ee626d4d7b39105 Mon Sep 17 00:00:00 2001 From: Dominikhun250 <149384401+Dominikhun250@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:40:01 +0200 Subject: [PATCH 11/11] Revert it to default file --- scripts/commands/advanced/upgrade.sh | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/scripts/commands/advanced/upgrade.sh b/scripts/commands/advanced/upgrade.sh index 0b5dcbb2..86d7a5f7 100644 --- a/scripts/commands/advanced/upgrade.sh +++ b/scripts/commands/advanced/upgrade.sh @@ -140,8 +140,6 @@ Command() { ((PROGRESS_NOW++)) - echo "$VERSION" > .update/old_version - # Delete files PRINT INFO "Deleting files.." rm -rf .blueprint @@ -181,20 +179,6 @@ Command() { ((PROGRESS_NOW++)) - if [[ -f ".update/old_version" ]]; then - OLD_VERSION="$(cat .update/old_version)" - if [[ -n "$OLD_VERSION" ]]; then - PRINT INFO "Checking for pending Blueprint migrations from $OLD_VERSION.." - hide_progress - php artisan bp:migrate --force --from="$OLD_VERSION" - migration_exit=$? - if [[ $migration_exit -ne 0 ]]; then - update_fail="true" - PRINT FATAL "Blueprint migrations failed; please resolve before continuing." - fi - fi - fi - cleanup # Tell user that update has finished