diff --git a/packages/core/src/Console/Commands/MooxInstaller.php b/packages/core/src/Console/Commands/MooxInstaller.php index 37619ee5f..090d0ef54 100644 --- a/packages/core/src/Console/Commands/MooxInstaller.php +++ b/packages/core/src/Console/Commands/MooxInstaller.php @@ -51,7 +51,7 @@ public function handle(): void ] ); - if (! $this->checkForFilament()) { + if (! $this->checkForFilament(silent: true)) { $this->error('❌ Filament installation is required or was aborted.'); return; @@ -63,24 +63,33 @@ public function handle(): void }; } - protected function runPackageInstallFlow(): void + protected function isPanelGenerationMode(): bool { - $categories = $this->getAllKnownMooxPackages(); - $installed = $this->getInstalledMooxPackages(); + foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { + if (($frame['function'] ?? null) === 'runPanelGenerationFlow') { + return true; + } + } - $this->displayPackageStatus($categories, $installed); + return false; + } - $notInstalled = collect($categories)->flatten()->diff($installed)->toArray(); - sort($notInstalled); - if (empty($notInstalled)) { - $this->info('πŸŽ‰ All Moox Packages are already installed!'); + protected function runPackageInstallFlow(): void + { + $available = $this->getPackagesFromComposerRequire(); + if (empty($available)) { + $this->warn('⚠️ No Moox packages found in composer.json require.'); return; } + sort($available); + + $this->info('Please select the packages you want to install from composer.json:'); + $selection = multiselect( - label: 'Which of the not yet installed packages would you like to install?', - options: array_combine($notInstalled, $notInstalled), + label: 'Select Moox packages (from composer.json require) to install/configure:', + options: array_combine($available, $available), required: true ); @@ -90,12 +99,10 @@ protected function runPackageInstallFlow(): void return; } - // Warn if any selected package is already registered in panel package composer.json $this->warnIfPackagesAlreadyRegistered($selection); $selectedPanelKey = $this->determinePanelForPackage(implode(', ', $selection)); - - $this->ensurePanelForKey($selectedPanelKey); + $this->ensurePanelForKey($selectedPanelKey, $selectedPanelKey, false); $providerPath = $this->panelMap[$selectedPanelKey]['path'].'/'.ucfirst($selectedPanelKey).'PanelProvider.php'; @@ -114,44 +121,65 @@ protected function runPackageInstallFlow(): void $this->callSilent('filament:upgrade'); } - $this->info('πŸŽ‰ Selected packages have been installed successfully!'); + $this->info('πŸŽ‰ Selected packages have been installed successfully: '.implode(', ', $selection)); } - protected function displayPackageStatus(array $categories, array $installed): void + protected function getPackagesFromComposerRequire(): array { - foreach ($categories as $category => $packages) { - $this->info("πŸ“‚ {$category}:"); + $composerPath = base_path('composer.json'); + if (! file_exists($composerPath)) { + return []; + } - $installedList = array_values(array_intersect($packages, $installed)); - sort($installedList); + $json = json_decode(file_get_contents($composerPath), true); + $require = $json['require'] ?? []; - $notInstalledList = array_values(array_diff($packages, $installed)); - sort($notInstalledList); + return array_values(array_filter(array_keys($require), function ($pkg) { + return is_string($pkg) && str_starts_with($pkg, 'moox/'); + })); + } - if (! empty($installedList)) { - $this->line(' βœ… Installed:'); - foreach ($installedList as $pkg) { - $this->line(" β€’ {$pkg}"); - } - } + protected function runPanelGenerationFlow(): void + { + $this->setAutoRequireComposer(false); - if (! empty($notInstalledList)) { - $this->line(' βž• Available:'); - foreach ($notInstalledList as $pkg) { - $this->line(" β€’ {$pkg}"); - } + $existingPanels = $this->getExistingPanelsWithLogin(); + + if (! empty($existingPanels)) { + $this->info('➑️ You can still create additional panels.'); + $this->error('❌ A panel with login already exists:'); + foreach ($existingPanels as $panelClass) { + $this->line(" β€’ {$panelClass}"); } + } - $this->newLine(); + $this->selectedPanels = $this->selectPanels(); + + if (empty($this->selectedPanels)) { + $this->warn('⚠️ No panel selection made. Operation aborted.'); + + return; } + + $this->info('ℹ️ Panels created/updated. Skipped composer require in panel-generation mode.'); + + foreach ($this->selectedPanels as $panel) { + if ($panel === 'press') { + $this->checkOrCreateWpUser(); + } else { + $this->checkOrCreateFilamentUser(); + } + } + + $this->installPluginsFromGeneratedPanels(); + + $this->info('βœ… Moox Panels installed successfully. Enjoy! πŸŽ‰'); } protected function determinePanelForPackage(string $package): string { - // Parse existing provider classes from bootstrap/providers.php $providerClasses = $this->getProviderClassesFromBootstrap(); - // Build options: unique key per occurrence, human label shows key + class $panelOptions = []; foreach ($providerClasses as $index => $class) { $key = $this->mapProviderClassToPanelKey($class); @@ -184,7 +212,6 @@ protected function determinePanelForPackage(string $package): string return explode('_', $selectedUniqueKey)[0]; } else { - // Determine which predefined panels already exist in providers.php $existingKeys = array_values(array_filter(array_map(function ($class) { return $this->mapProviderClassToPanelKey($class); }, $providerClasses))); @@ -193,15 +220,6 @@ protected function determinePanelForPackage(string $package): string } } - // βœ… Korrektur: sauber alle Keys aus panelMap zurΓΌckgeben - protected function panelKeyFromPath(string $path): ?string - { - $filename = pathinfo($path, PATHINFO_FILENAME); // z.B. CmsPanelProvider - $key = strtolower(str_replace('PanelProvider', '', $filename)); - - return in_array($key, array_keys($this->panelMap)) ? $key : null; - } - protected function selectNewPanel(array $existingPanels): string { $allPanels = array_keys($this->panelMap); @@ -221,88 +239,24 @@ protected function selectNewPanel(array $existingPanels): string ); } - protected function getAllPanelsFromBootstrap(): array - { - // Deprecated internal helper (kept for BC if referenced elsewhere) - return $this->getProviderClassesFromBootstrap(); - } - - protected function getProviderClassesFromBootstrap(): array - { - $bootstrapProvidersPath = base_path('bootstrap/providers.php'); - if (! file_exists($bootstrapProvidersPath)) { - return []; - } - - $content = file_get_contents($bootstrapProvidersPath); - if ($content === false) { - return []; - } - - if (! preg_match_all('/([\\\\A-Za-z0-9_]+)::class/', $content, $matches)) { - return []; - } - - return $matches[1] ?? []; - } - - protected function runPanelGenerationFlow(): void + protected function getExistingPanelsWithLogin(): array { - // Nur eigene Panels mit login() prΓΌfen, nicht Filament Standardprovider - $existingPanels = $this->getExistingPanelsWithLogin(); - - if (! empty($existingPanels)) { - $this->info('ℹ️ Existing panels with login detected. Panel creation is skipped.'); - - return; - } - - // Wenn keine Panels existieren, Auswahl anzeigen - $this->selectedPanels = $this->selectPanels(); - if (! empty($this->selectedPanels)) { - $changed = $this->installPackages($this->selectedPanels); - } else { - $this->warn('⚠️ No panel bundle selected. Skipping package installation.'); - - return; - } + $providerClasses = $this->getProviderClassesFromBootstrap(); - $this->checkOrCreateFilamentUser(); + $panels = []; + foreach ($providerClasses as $class) { + if (! class_exists($class)) { + continue; + } - if (isset($changed) && $changed) { - $this->info('βš™οΈ Finalizing (package discovery + Filament upgrade)...'); - $this->callSilent('package:discover'); - $this->callSilent('filament:upgrade'); + if (is_subclass_of($class, \Filament\PanelProvider::class)) { + if (method_exists($class, 'login')) { + $panels[] = $class; + } + } } - $this->info('βœ… Moox Panels installed successfully. Enjoy! πŸŽ‰'); - } - - protected function getMooxPackages(): array - { - return collect($this->getAllKnownMooxPackages())->flatten()->toArray(); - } - - protected function getExistingPanelsWithLogin(): array - { - // Consider any provider registered in bootstrap/providers.php as an existing panel - // regardless of whether it already has ->login() configured. - return $this->getProviderClassesFromBootstrap(); - } - - protected function getAllKnownMooxPackages(): array - { - return [ - 'Core & System' => ['moox/core', 'moox/build', 'moox/skeleton', 'moox/packages'], - 'Development Tools' => ['moox/devops', 'moox/devtools', 'moox/devlink'], - 'Content & Media' => ['moox/content', 'moox/page', 'moox/news', 'moox/press', 'moox/press-trainings', 'moox/press-wiki', 'moox/media'], - 'User & Authentication' => ['moox/user', 'moox/user-device', 'moox/user-session', 'moox/login-link', 'moox/passkey', 'moox/security'], - 'E-Commerce & Shop' => ['moox/shop', 'moox/item', 'moox/category'], - 'Collaboration & Productivity' => ['moox/clipboard', 'moox/jobs', 'moox/trainings', 'moox/progress'], - 'Data & Utilities' => ['moox/data', 'moox/backup-server', 'moox/restore', 'moox/audit', 'moox/expiry', 'moox/draft', 'moox/slug', 'moox/tag'], - 'UI Components & Icons' => ['moox/components', 'moox/featherlight', 'moox/laravel-icons', 'moox/flag-icons-circle', 'moox/flag-icons-origin', 'moox/flag-icons-rect', 'moox/flag-icons-square'], - 'Localization & Communication' => ['moox/localization', 'moox/notifications'], - ]; + return $panels; } protected function warnIfPackagesAlreadyRegistered(array $packages): void @@ -343,4 +297,155 @@ protected function findPanelsContainingPackage(string $package): array return $panels; } + + protected function updatePanelPackageComposerJson(string $panelKey, array $packages): void + { + $panelPath = base_path($this->panelMap[$panelKey]['path'].'/../../composer.json'); + if (! file_exists($panelPath)) { + $this->warn("⚠️ Panel composer.json not found for {$panelKey}, skipping update."); + + return; + } + + $composer = json_decode(file_get_contents($panelPath), true); + foreach ($packages as $pkg) { + $composer['require'][$pkg] = '*'; + } + + file_put_contents($panelPath, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $this->info("βœ… Updated composer.json for panel '{$panelKey}' with package(s): ".implode(', ', $packages)); + } + + protected function installPluginsFromGeneratedPanels(): void + { + $this->info('πŸ”Ž Search for plugin packages in the generated panels...'); + + if (empty($this->selectedPanels)) { + $this->warn('⚠️ No panels selected. Skipping plugin installation.'); + + return; + } + + $panelCount = count($this->selectedPanels); + + foreach ($this->selectedPanels as $i => $panelKey) { + $panelInfo = $this->panelMap[$panelKey] ?? null; + + if (! $panelInfo) { + $this->warn("⚠️ Unknown panel key '{$panelKey}'. Skipping plugin scan."); + + continue; + } + + $panelClass = ($panelInfo['namespace'] ?? null) + ? $panelInfo['namespace'].'\\'.ucfirst($panelKey).'PanelProvider' + : null; + + if (! $panelClass || ! class_exists($panelClass)) { + $this->warn("⚠️ Panel provider class '{$panelClass}' does not exist. Skipping plugin scan."); + + continue; + } + + $this->newLine(); + $this->line(str_repeat('═', 60)); + $this->info('🧩 ['.($i + 1)."/{$panelCount}] Processing panel: {$panelKey}"); + $this->line(str_repeat('═', 60)); + $this->newLine(); + + $providerInstance = new $panelClass(app()); + + $panel = new \Filament\Panel; + $configuredPanel = $providerInstance->panel($panel); + + $plugins = $configuredPanel->getPlugins() ?? []; + + if (empty($plugins)) { + $this->info("ℹ️ No plugins found in panel '{$panelKey}'."); + + continue; + } + + // --- Plugin-Klassen in Composer-Pakete umwandeln --- + $packagesToInstall = []; + foreach ($plugins as $plugin) { + $class = get_class($plugin); + + foreach ($this->pluginPackageMap as $prefix => $package) { + if (str_starts_with($class, $prefix)) { + $packagesToInstall[] = $package; + break; + } + } + } + + $packagesToInstall = array_unique($packagesToInstall); + + if (empty($packagesToInstall)) { + $this->info("ℹ️ No composer packages detected for panel '{$panelKey}'."); + + continue; + } + + $this->info('πŸ“¦ Detected plugin packages:'); + foreach ($packagesToInstall as $pkg) { + $this->line(" β€’ {$pkg}"); + } + + $providerPath = $panelInfo['path'].'/'.ucfirst($panelKey).'PanelProvider.php'; + + // --- Pakete installieren --- + foreach ($packagesToInstall as $pkg) { + $this->line("\n─────────────────────────────────────────────"); + $this->info("πŸ“¦ Installing package: {$pkg}"); + + $packageData = ['name' => $pkg, 'composer' => $pkg]; + + try { + $this->installPackage($packageData, [$providerPath]); + $this->updatePanelPackageComposerJson($panelKey, [$pkg]); + $this->info(" βœ” Updated composer.json for {$panelKey} β†’ {$pkg}"); + } catch (\RuntimeException $e) { + $this->warn("⚠️ Installation failed for '{$pkg}': {$e->getMessage()}"); + } + } + + $this->newLine(); + $this->info("πŸŽ‰ All plugins declared in the '{$panelKey}' panel were installed successfully!"); + } + + $this->newLine(2); + $this->line(str_repeat('═', 60)); + $this->info('πŸŽ‰ All selected panels processed successfully!'); + $this->info('✨ Moox Panels installed successfully. Enjoy!'); + $this->line(str_repeat('═', 60)); + $this->newLine(); + } + + protected function getProviderClassesFromBootstrap(): array + { + $bootstrapProvidersPath = base_path('bootstrap/providers.php'); + if (! file_exists($bootstrapProvidersPath)) { + return []; + } + + $content = file_get_contents($bootstrapProvidersPath); + if ($content === false) { + return []; + } + + if (! preg_match_all('/([\\\\A-Za-z0-9_]+)::class/', $content, $matches)) { + return []; + } + + return $matches[1] ?? []; + } + + protected function mapProviderClassToPanelKey(string $class): ?string + { + $classParts = explode('\\', $class); + $name = end($classParts); + + return strtolower(str_replace('PanelProvider', '', $name)); + } } diff --git a/packages/core/src/Console/Traits/CheckForFilament.php b/packages/core/src/Console/Traits/CheckForFilament.php index b605b39fb..2106141b3 100644 --- a/packages/core/src/Console/Traits/CheckForFilament.php +++ b/packages/core/src/Console/Traits/CheckForFilament.php @@ -14,7 +14,7 @@ trait CheckForFilament { protected string $providerPath = 'app/Providers/Filament/AdminPanelProvider.php'; - public function checkForFilament(): bool + public function checkForFilament(bool $silent = false): bool { if (! class_exists(\Filament\PanelProvider::class, false)) { $panelProviderPath = base_path('vendor/filament/filament/src/PanelProvider.php'); @@ -28,10 +28,14 @@ public function checkForFilament(): bool return false; } - info('πŸ“¦ Running: composer require filament/filament...'); + if (! $silent) { + info('πŸ“¦ Running: composer require filament/filament...'); + } exec('composer require filament/filament:* 2>&1', $output, $returnVar); foreach ($output as $line) { - info(' '.$line); + if (! $silent) { + info(' '.$line); + } } if ($returnVar !== 0) { @@ -40,15 +44,24 @@ public function checkForFilament(): bool return false; } - info('βœ… filament/filament successfully installed.'); + if (! $silent) { + info('βœ… filament/filament successfully installed.'); + } } else { - info('βœ… Filament is already installed.'); + if (! $silent) { + info('βœ… Filament is already installed.'); + } } } else { - info('βœ… Filament is already installed.'); + if (! $silent) { + info('βœ… Filament is already installed.'); + } } - $this->analyzeFilamentEnvironment(); + // Only analyze in panel generation flow. The packages flow should stay clean. + if (! $silent && method_exists($this, 'isPanelGenerationMode') && $this->isPanelGenerationMode()) { + $this->analyzeFilamentEnvironment(); + } return true; } diff --git a/packages/core/src/Console/Traits/CheckOrCreateFilamentUser.php b/packages/core/src/Console/Traits/CheckOrCreateFilamentUser.php index 799393deb..7e8faee12 100644 --- a/packages/core/src/Console/Traits/CheckOrCreateFilamentUser.php +++ b/packages/core/src/Console/Traits/CheckOrCreateFilamentUser.php @@ -42,28 +42,10 @@ public function checkOrCreateFilamentUser(): void return; } - alert("🚨 No users found in '{$table}'. Let's create the first Filament user."); + alert("🚨 No users found. Let's create the first user"); $this->createFilamentUser($userModel); } - public function hasFilamentUsers(): bool - { - /** @var class-string $userModel */ - $userModel = Config::get('filament.auth.providers.users.model') ?? \App\Models\User::class; - - if (! class_exists($userModel)) { - return false; - } - - $table = (new $userModel)->getTable(); - - if (! Schema::hasTable($table)) { - return false; - } - - return $userModel::count() > 0; - } - protected function createFilamentUser(string $userModel): void { info("πŸ§‘ Creating new admin user for model '{$userModel}'..."); @@ -80,4 +62,54 @@ protected function createFilamentUser(string $userModel): void info("βœ… User '{$user->email}' created successfully."); } + + public function checkOrCreateWpUser(): void + { + $wpUserModel = \Moox\Press\Models\WpUser::class; + + if (! class_exists($wpUserModel)) { + warning("⚠️ WP User model '{$wpUserModel}' does not exist."); + + return; + } + + $table = (new $wpUserModel)->getTable(); + + info("πŸ” Checking WP user setup for Press Panel [Model: {$wpUserModel}]..."); + + if (! Schema::hasTable($table)) { + warning("⚠️ Table '{$table}' not found. Did you run migrations?"); + + return; + } + + if ($wpUserModel::count() > 0) { + info("βœ… Found existing WP users in '{$table}'. Skipping user creation."); + + return; + } + + alert("🚨 No WP users found. Let's create the first WP user"); + $this->createWpUser($wpUserModel); + } + + protected function createWpUser(string $wpUserModel): void + { + info("πŸ§‘ Creating new WP user for model '{$wpUserModel}'..."); + + $login = text('Enter login', default: 'wpadmin'); + $email = text('Enter email', default: 'wpadmin@example.com'); + $password = password('Enter password', required: true); + $displayName = text('Enter display name', default: $login); + + $user = $wpUserModel::create([ + 'user_login' => $login, + 'user_email' => $email, + 'user_pass' => $password, + 'display_name' => $displayName, + 'user_registered' => now(), + ]); + + info("βœ… WP user '{$user->user_login}' created successfully."); + } } diff --git a/packages/core/src/Console/Traits/InstallPackage.php b/packages/core/src/Console/Traits/InstallPackage.php index daff8e70c..ca844cd1f 100644 --- a/packages/core/src/Console/Traits/InstallPackage.php +++ b/packages/core/src/Console/Traits/InstallPackage.php @@ -33,66 +33,6 @@ protected function ensurePackageServiceIsSet(): void } } - protected function requirePackage(string $package): string - { - $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); - - if (isset($composerJson['require'][$package])) { - return 'already'; - } - - $isPathRepo = false; - if (isset($composerJson['repositories'])) { - foreach ($composerJson['repositories'] as $repo) { - if (($repo['type'] ?? null) === 'path' && - str_contains($repo['url'], str_replace('moox/', '', $package))) { - $isPathRepo = true; - break; - } - } - } - - // Path-Repo: nur Composer.json updaten, kein "composer require" - if ($isPathRepo) { - info("ℹ️ Local path repo detected for {$package}, adding to composer.json..."); - $this->addPackageToComposerJson($package); - - return 'already'; - } - - $version = '*'; - $command = "composer require {$package}:{$version} --no-scripts --quiet 2>&1"; - exec($command, $output, $returnVar); - - if ($returnVar !== 0) { - warning("❌ Error running composer require {$package}."); - throw new \RuntimeException("Composer require for {$package} failed."); - } - - // Immer Composer.json updaten - $this->addPackageToComposerJson($package); - - info("βœ… Installed package: {$package}"); - - return 'installed'; - } - - protected function addPackageToComposerJson(string $package, string $version = '*'): void - { - $composerPath = base_path('composer.json'); - $composerJson = json_decode(file_get_contents($composerPath), true); - - if (! isset($composerJson['require'][$package])) { - $composerJson['require'][$package] = $version; - - // Alphabetisch sortieren - ksort($composerJson['require']); - - file_put_contents($composerPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - info("βœ… Added {$package} to global composer.json require"); - } - } - public function installPackage(array $package, array $panelPaths = []): bool { if (empty($package) || ! isset($package['name'])) { @@ -102,47 +42,103 @@ public function installPackage(array $package, array $panelPaths = []): bool } $didChange = false; + $this->ensurePackageServiceIsSet(); + // --- Composer require --- if (isset($package['composer'])) { $status = $this->requirePackage($package['composer']); if ($status === 'installed') { - info("βœ… Installed: {$package['name']}"); + info("βœ… Installed '{$package['name']}' via composer."); $didChange = true; } } - $this->ensurePackageServiceIsSet(); + // --- Migrations publish & run --- + $migrations = $this->packageService->getMigrations($package); + if (! empty($migrations)) { + if (confirm('πŸ“₯ New migrations have been published. Would you like to run them now?', true)) { + // --- Config publish --- + if (confirm("πŸ“„ Would you like to publish configs for '{$package['name']}'?", true)) { + $didChange = $this->packageService->publishConfigs($package) || $didChange; + } - $didChange = $this->runMigrations($package) || $didChange; - $didChange = $this->publishConfig($package) || $didChange; - $didChange = $this->runSeeders($package) || $didChange; - $didChange = $this->runAutoCommands($package) || $didChange; + $didChange = $this->runMigrations($migrations) || $didChange; + } else { + info('⏩ Migrations were published but not executed.'); + } + } + // --- Seeders --- + $seeders = $this->packageService->getRequiredSeeders($package); + if (! empty($seeders)) { + if (confirm("🌱 Run seeders for '{$package['name']}'?", false)) { + $didChange = $this->runSeeders($package) || $didChange; + } else { + info('⏩ Skipped seeders by user.'); + } + } + + // --- Panels & plugins --- if (empty($panelPaths)) { $panelPaths = $this->determinePanelsForPackage($package); if (! empty($panelPaths)) { $didChange = true; } } - $didChange = $this->installPlugins($package, $panelPaths) || $didChange; + // --- Post-install commands --- + $didChange = $this->runAutoCommands($package) || $didChange; + if ($didChange) { - info('πŸŽ‰ Installation completed.'); + info("πŸŽ‰ Installation for '{$package['name']}' completed!"); + } else { + info("ℹ️ Nothing was changed for '{$package['name']}'."); } return $didChange; } + /** + * Install a composer package if not already installed. + */ + protected function requirePackage(string $packageName): string + { + $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); + if (isset($composerJson['require'][$packageName])) { + info("ℹ️ Package '{$packageName}' is already required in composer.json."); + + return 'already'; + } + + info("πŸ“¦ Installing composer package: {$packageName}"); + $process = Process::fromShellCommandline("composer require {$packageName} --no-scripts --quiet"); + $process->setTimeout(null); + $process->run(function ($type, $buffer) { + foreach (explode("\n", rtrim($buffer)) as $line) { + if ($line !== '') { + info(' '.$line); + } + } + }); + + if ($process->isSuccessful()) { + return 'installed'; + } + + warning("⚠️ Failed to install {$packageName}"); + + return 'failed'; + } + protected function determinePanelsForPackage(array $package): array { $existingPanels = $this->getExistingPanelsWithLogin(); if (empty($existingPanels)) { info('ℹ️ No existing panels found. Creating a new panel...'); - $newPanel = $this->createNewPanelProvider(); - return [$newPanel]; + return [$this->createNewPanelProvider()]; } info('πŸ”Ή Existing panels found:'); @@ -151,26 +147,23 @@ protected function determinePanelsForPackage(array $package): array } $useExisting = confirm("Do you want to install '{$package['name']}' in an existing panel?", true); - if ($useExisting) { $selectedKey = $this->selectFromList($existingPanels, "Select panel for '{$package['name']}'"); - $selectedPanel = $existingPanels[$selectedKey]; - info("βœ… Installing in existing panel: {$selectedPanel}"); - return [$selectedPanel]; + return [$existingPanels[$selectedKey]]; } info("ℹ️ Creating a new panel for '{$package['name']}'..."); - $newPanel = $this->createNewPanelProvider(); - return [$newPanel]; + return [$this->createNewPanelProvider()]; } public function installPlugins(array $package, array $panelPaths): bool { $plugins = $this->packageService->getPlugins($package); - if (empty($plugins)) { + info("ℹ️ No plugins found for '{$package['name']}'. Skipping."); + return false; } @@ -190,19 +183,13 @@ protected function createNewPanelProvider(): string { $panelName = 'Panel'.time(); info("Creating new panel provider: {$panelName} ..."); - Artisan::call('make:filament-panel', ['name' => $panelName]); return $panelName; } - protected function runMigrations(array $package): bool + protected function runMigrations(array $migrations): bool { - $migrations = $this->packageService->getMigrations($package); - if (empty($migrations)) { - return false; - } - $didRun = false; foreach ($migrations as $migration) { $absolutePath = base_path($migration); @@ -212,24 +199,14 @@ protected function runMigrations(array $package): bool continue; } - $status = $this->packageService->checkMigrationStatus($migration); - - if ($status['hasChanges']) { - if ($status['hasDataInDeletedFields'] && ! confirm("❗ Migration '{$migration}' removes columns with data. Continue anyway?", false)) { - warning("⏭️ Skipped migration '{$migration}'."); - - continue; - } - - info("πŸ“₯ Running migration {$migration}..."); - $exitCode = Artisan::call('migrate', [ - '--path' => $migration, - '--force' => true, - '--no-interaction' => true, - ]); - info("βœ… Migration completed (Exit Code: {$exitCode})"); - $didRun = true; - } + $relativePath = str_replace(base_path().'/', '', $absolutePath); + Artisan::call('migrate', [ + '--path' => $relativePath, + '--force' => true, + '--no-interaction' => true, + ]); + info("βœ… Migration completed for {$relativePath}"); + $didRun = true; } return $didRun; @@ -241,8 +218,7 @@ private function hasMigrationsAtPath(string $absolutePath): bool return str_ends_with($absolutePath, '.php'); } if (File::isDirectory($absolutePath)) { - $files = collect(File::files($absolutePath)) - ->filter(fn ($f) => str_ends_with($f->getFilename(), '.php')); + $files = collect(File::files($absolutePath))->filter(fn ($f) => str_ends_with($f->getFilename(), '.php')); return $files->isNotEmpty(); } @@ -250,51 +226,6 @@ private function hasMigrationsAtPath(string $absolutePath): bool return false; } - protected function publishConfig(array $package): bool - { - $configs = $this->packageService->getConfig($package); - $updatedAny = false; - - foreach ($configs as $path => $content) { - if (is_string($path) && str_starts_with($path, 'tag:')) { - $tag = substr($path, 4); - info("πŸ“¦ Publishing vendor tag: {$tag}"); - Artisan::call('vendor:publish', [ - '--tag' => $tag, - '--force' => true, - '--no-interaction' => true, - ]); - $updatedAny = true; - - continue; - } - - $publishPath = config_path(basename($path)); - if (! file_exists($publishPath)) { - info("πŸ“„ Publishing new config: {$path}"); - File::put($publishPath, $content); - $updatedAny = true; - - continue; - } - - $existingContent = File::get($publishPath); - if ($existingContent === $content) { - continue; - } - - if (confirm("⚠️ Config file {$path} has changes. Overwrite?", false)) { - info("πŸ”„ Updating config file: {$path}"); - File::put($publishPath, $content); - $updatedAny = true; - } else { - warning("⏭️ Config {$path} was not overwritten."); - } - } - - return $updatedAny; - } - protected function runSeeders(array $package): bool { $requiredSeeders = $this->packageService->getRequiredSeeders($package); @@ -302,7 +233,6 @@ protected function runSeeders(array $package): bool foreach ($requiredSeeders as $seeder) { $table = $this->getSeederTable($seeder); - if (! $table || ! Schema::hasTable($table)) { warning("⚠️ Table for seeder {$seeder} not found. Skipping."); @@ -311,13 +241,10 @@ protected function runSeeders(array $package): bool if (DB::table($table)->count() === 0 || confirm("πŸ“‚ Table '{$table}' already contains data. Seed again anyway?", false)) { info("🌱 Seeding data into {$table}..."); - Artisan::call('db:seed', [ - '--class' => $seeder, - '--force' => true, - ]); + Artisan::call('db:seed', ['--class' => $seeder, '--force' => true]); $didSeed = true; } else { - warning("⏭️ Seeder for {$table} skipped."); + warning("⏩ Seeder for {$table} skipped."); } } @@ -328,28 +255,17 @@ protected function runAutoCommands(array $package): bool { $rootCmds = $this->packageService->getAutoRunCommands($package); $hereCmds = $this->packageService->getAutoRunHereCommands($package); - - if (empty($rootCmds) && empty($hereCmds)) { - return false; - } - - if (! confirm('πŸš€ Run post-install commands (auto_run/auto_runhere)?', true)) { - warning('⏭️ Post-install commands skipped.'); - - return false; - } - $ranAny = false; + foreach ($rootCmds as $cmd) { info("▢️ {$cmd}"); $this->execInCwd($cmd, base_path()); $ranAny = true; } + foreach ($hereCmds as $entry) { - $cmd = $entry['cmd']; - $cwd = $entry['cwd']; - info("▢️ (in {$cwd}) {$cmd}"); - $this->execInCwd($cmd, $cwd); + info("▢️ (in {$entry['cwd']}) {$entry['cmd']}"); + $this->execInCwd($entry['cmd'], $entry['cwd']); $ranAny = true; } @@ -385,8 +301,7 @@ protected function getExistingPanelsWithLogin(): array return []; } - $files = scandir($panelPath); - foreach ($files as $file) { + foreach (scandir($panelPath) as $file) { if (str_ends_with($file, '.php')) { $panels[] = pathinfo($file, PATHINFO_FILENAME); } @@ -398,18 +313,12 @@ protected function getExistingPanelsWithLogin(): array protected function selectFromList(array $items, string $prompt): int { info($prompt); - foreach ($items as $key => $item) { info(" [{$key}] {$item}"); } $choice = (int) readline('Enter number: '); - if (! isset($items[$choice])) { - warning('Invalid selection, defaulting to first item.'); - - return 0; - } - return $choice; + return $items[$choice] ?? 0; } } diff --git a/packages/core/src/Console/Traits/SelectFilamentPanel.php b/packages/core/src/Console/Traits/SelectFilamentPanel.php index 7af56257f..1a2687678 100644 --- a/packages/core/src/Console/Traits/SelectFilamentPanel.php +++ b/packages/core/src/Console/Traits/SelectFilamentPanel.php @@ -2,14 +2,12 @@ namespace Moox\Core\Console\Traits; -use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\File; use function Laravel\Prompts\confirm; use function Laravel\Prompts\error; use function Laravel\Prompts\info; use function Laravel\Prompts\multiselect; -use function Laravel\Prompts\text; use function Laravel\Prompts\warning; trait SelectFilamentPanel @@ -36,11 +34,37 @@ trait SelectFilamentPanel 'Moox\\Press\\' => 'moox/press', ]; + protected bool $autoRequireComposer = true; + + public function setAutoRequireComposer(bool $v): void + { + $this->autoRequireComposer = $v; + } + public function selectPanels(): array { - $availablePanels = collect($this->panelMap) - ->filter(fn ($config, $panel) => ! $this->panelExists($panel)) - ->keys() + $existingPanel = $this->getPanelWithLoginFromBootstrap(); + if ($existingPanel) { + warning("⚠️ A panel with login already exists: {$existingPanel}"); + info('➑️ You can still create additional panels.'); + } + + // Build list of panels that are NOT yet registered in bootstrap/providers.php + $registeredProviderClasses = method_exists($this, 'getProviderClassesFromBootstrap') + ? $this->getProviderClassesFromBootstrap() + : []; + + $registeredPanelKeys = []; + foreach ($registeredProviderClasses as $class) { + $key = $this->mapProviderClassToPanelKey($class); + if ($key !== null) { + $registeredPanelKeys[$key] = true; + } + } + + $availablePanels = collect(array_keys($this->panelMap)) + ->reject(fn ($panel) => isset($registeredPanelKeys[$panel])) + ->values() ->toArray(); if (empty($availablePanels)) { @@ -50,7 +74,7 @@ public function selectPanels(): array } $selectedPanels = multiselect( - label: 'πŸ› οΈ Which panels do you want to install?', + label: 'πŸ› οΈ Which panels do you want to enable?', options: $availablePanels, required: false ); @@ -62,329 +86,112 @@ public function selectPanels(): array } foreach ($selectedPanels as $panel) { - if (! isset($this->panelMap[$panel])) { - error("❌ No path mapping found for panel '{$panel}'. Skipping."); - - continue; - } - - if ($this->panelExists($panel)) { - warning("⚠️ Panel '{$panel}' already exists. Skipping creation."); - - continue; - } - - $shouldPublish = confirm("πŸ“€ Do you want to publish the panel '{$panel}' into app/Providers/Filament?", default: false); - - $panelId = text("πŸ”§ Enter the panel ID for '{$panel}':", default: $panel); - - $this->call('make:filament-panel', [ - 'id' => $panelId, - ]); - - $from = base_path('app/Providers/Filament/'.ucfirst($panel).'PanelProvider.php'); - $toDir = base_path($this->panelMap[$panel]['path']); - $to = $toDir.'/'.ucfirst($panel).'PanelProvider.php'; - - if (! File::exists($from)) { - warning("⚠️ Expected file {$from} not found. Skipping."); - - continue; - } - - File::ensureDirectoryExists($toDir); - File::move($from, $to); - info("βœ… Moved panel provider to: {$to}"); - - $content = File::get($to); - $content = str_replace( - 'namespace App\\Providers\\Filament;', - 'namespace '.$this->panelMap[$panel]['namespace'].';', - $content - ); - File::put($to, $content); - info('🧭 Updated namespace to: '.$this->panelMap[$panel]['namespace']); - - $this->registerDefaultPluginsForPanel($panel, $to); - - $this->configureAuthUserModelForPanel($panel, $to); - - if ($shouldPublish) { - $publishDir = base_path('app/Providers/Filament'); - File::ensureDirectoryExists($publishDir); - $publishPath = $publishDir.'/'.ucfirst($panel).'PanelProvider.php'; - - $publishContent = File::get($to); - $publishContent = preg_replace( - '/namespace\s+[^;]+;/', - 'namespace App\\Providers\\Filament;', - $publishContent - ); - - File::put($publishPath, $publishContent); - info("πŸ“€ Panel has been published: {$publishPath}"); - } - - $providerClass = $shouldPublish - ? 'App\\Providers\\Filament\\'.ucfirst($panel).'PanelProvider' - : $this->panelMap[$panel]['namespace'].'\\'.ucfirst($panel).'PanelProvider'; - - $this->registerPanelProviderInBootstrapProviders($providerClass, $panel); - - if (! $shouldPublish) { - $this->cleanupPanelProviderInAppServiceProvider($panel); - } + $customize = confirm("βš™οΈ Do you want to change something in {$panel}? For example the path or the user model?", default: false); + $this->ensurePanelForKey($panel, $panel, $customize); } return $selectedPanels; } - protected function runFilamentUpgrade(): void + protected function getPanelWithLoginFromBootstrap(): ?string { - info('βš™οΈ Running php artisan filament:upgrade ...'); - - Artisan::call('filament:upgrade'); - $output = Artisan::output(); - - info($output); - info('βœ… Filament upgrade command finished.'); - } - - protected function registerDefaultPluginsForPanel(string $panel, string $providerPath): void - { - $pluginMap = [ - 'press' => [ - '\Moox\Press\WpCategoryPlugin::make()', - '\Moox\Press\WpCommentMetaPlugin::make()', - '\Moox\Press\WpCommentPlugin::make()', - '\Moox\Press\WpMediaPlugin::make()', - '\Moox\Press\WpOptionPlugin::make()', - '\Moox\Press\WpPagePlugin::make()', - '\Moox\Press\WpPostPlugin::make()', - '\Moox\Press\WpPostMetaPlugin::make()', - '\Moox\Press\WpTagPlugin::make()', - '\Moox\Press\WpTermMetaPlugin::make()', - '\Moox\Press\WpTermPlugin::make()', - '\Moox\Press\WpTermRelationshipPlugin::make()', - '\Moox\Press\WpTermTaxonomyPlugin::make()', - '\Moox\Press\WpUserMetaPlugin::make()', - '\Moox\Press\WpUserPlugin::make()', - ], - 'cms' => [ - '\Moox\News\Moox\Plugins\NewsPlugin::make()', - '\Moox\Media\MediaCollectionPlugin::make()', - '\Moox\Media\MediaPlugin::make()', - '\Moox\Jobs\JobsBatchesPlugin::make()', - '\Moox\Jobs\JobsFailedPlugin::make()', - '\Moox\Jobs\JobsPlugin::make()', - '\Moox\Jobs\JobsWaitingPlugin::make()', - '\Moox\User\UserPlugin::make()', - '\Moox\Page\PagePlugin::make()', - '\Moox\Tag\TagPlugin::make()', - '\Moox\Category\Moox\Entities\Categories\Plugins\CategoryPlugin::make()', - '\Moox\Security\ResetPasswordPlugin::make()', - '\Moox\UserSession\UserSessionPlugin::make()', - '\Moox\UserDevice\UserDevicePlugin::make()', - ], - 'devops' => [], - 'shop' => [], - 'empty' => [], - 'admin' => [], - ]; - - $plugins = $pluginMap[$panel] ?? []; - - if (empty($plugins)) { - info("ℹ️ No default plugins defined for panel '{$panel}'."); - - return; - } - - if (! File::exists($providerPath)) { - error("❌ Provider file not found: {$providerPath}"); - - return; - } - - $content = File::get($providerPath); - - if (str_contains($content, '->plugins([')) { - warning("⚠️ Panel '{$panel}' already has plugins registered. Skipping."); - - return; - } - - $pluginCode = implode(",\n ", $plugins); - - $insert = <<plugins([ - {$pluginCode} - ]) -PHP; - - $content = preg_replace( - '/return\s+\$panel(.*?)(;)/s', - "return \$panel\$1{$insert}\$2", - $content, - 1 - ); - - File::put($providerPath, $content); - - info("βœ… Plugins registered for panel '{$panel}'."); - - $requiredPackages = []; - - foreach ($plugins as $plugin) { - if (preg_match('/\\\\?([\w\\\\]+)::make/', $plugin, $matches)) { - $class = ltrim($matches[1], '\\'); - $package = $this->guessComposerPackageFromClass($class); - if ($package && ! in_array($package, $requiredPackages)) { - $requiredPackages[] = $package; - } - } + $bootstrapProvidersPath = base_path('bootstrap/providers.php'); + if (! File::exists($bootstrapProvidersPath)) { + return null; } - foreach ($requiredPackages as $package) { - if (! $this->isPackageInstalled($package)) { - $this->requireComposerPackage($package); - } - } + $content = File::get($bootstrapProvidersPath); - if (! empty($requiredPackages)) { - $this->updatePanelPackageComposerJson($panel, $requiredPackages); + if (! preg_match_all('/([\\\\A-Za-z0-9_]+)::class/', $content, $matches)) { + return null; } - } - protected function guessComposerPackageFromClass(string $class): ?string - { - foreach ($this->pluginPackageMap as $namespacePrefix => $packageName) { - if (str_starts_with($class, $namespacePrefix)) { - return $packageName; + foreach ($matches[1] as $class) { + $panel = $this->mapProviderClassToPanelKey($class); + if ($panel && $this->panelHasLogin($panel)) { + return $class; } } return null; } - protected function isPackageInstalled(string $package): bool + protected function panelHasLogin(string $panel): bool { - $installed = shell_exec("composer show {$package} 2>&1"); + $providerPath = base_path($this->panelMap[$panel]['path'].'/'.ucfirst($panel).'PanelProvider.php'); + if (! File::exists($providerPath)) { + return false; + } - return ! str_contains($installed, 'not found'); - } + $content = File::get($providerPath); - protected function requireComposerPackage(string $package): void - { - info("πŸ“¦ Requiring composer package: {$package} ..."); - exec("composer require {$package}", $output, $exitCode); - if ($exitCode !== 0) { - warning("⚠️ Failed to require {$package}. Please check manually."); - } else { - info("βœ… Package {$package} required successfully."); - } + return str_contains($content, '->login('); } - protected function updatePanelPackageComposerJson(string $panel, array $requiredPackages): void + protected function ensurePanelForKey(string $panel, string $panelId, bool $publish = false): void { - $panelPath = $this->panelMap[$panel]['path'] ?? null; - if (! $panelPath) { - warning("⚠️ No path found for panel '{$panel}'. Cannot update composer.json."); + if (! isset($this->panelMap[$panel])) { + error("❌ Unknown panel '{$panel}'."); return; } - $composerJsonPath = base_path($panelPath.'/../../composer.json'); + $packageProviderPath = base_path($this->panelMap[$panel]['path'].'/'.ucfirst($panel).'PanelProvider.php'); - if (! File::exists($composerJsonPath)) { - info("ℹ️ No composer.json found for panel package at: {$composerJsonPath}"); + if (! File::exists($packageProviderPath)) { + error("❌ Panel provider not found in package: {$packageProviderPath}"); return; } - $composerJson = json_decode(File::get($composerJsonPath), true); - if (! $composerJson) { - warning("⚠️ Invalid composer.json at: {$composerJsonPath}"); + if ($publish) { + $toDir = app_path('Providers/Filament'); + File::ensureDirectoryExists($toDir); + $publishedPath = $toDir.'/'.ucfirst($panel).'PanelProvider.php'; + File::copy($packageProviderPath, $publishedPath); - return; - } + $content = File::get($publishedPath); + $content = preg_replace('/^namespace\s+[^;]+;/m', 'namespace App\\Providers\\Filament;', $content, 1); - $updated = false; - if (! isset($composerJson['require'])) { - $composerJson['require'] = []; - } + File::put($publishedPath, $content); + info("βœ… Published and customized provider: {$publishedPath}"); - foreach ($requiredPackages as $package) { - if (! isset($composerJson['require'][$package])) { - $composerJson['require'][$package] = '*'; - $updated = true; - info("πŸ“ Added {$package} to panel package composer.json"); - } - } - - if ($updated) { - File::put($composerJsonPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - info("βœ… Updated composer.json for panel package: {$composerJsonPath}"); + $providerClass = 'App\\\\Providers\\\\Filament\\\\'.ucfirst($panel).'PanelProvider'; + $this->registerPanelProviderInBootstrapProviders($providerClass, $panel); + $this->cleanupPanelProviderInAppServiceProvider($panel); } else { - info('ℹ️ No new dependencies to add to panel package composer.json'); + $providerClass = $this->panelMap[$panel]['namespace'].'\\'.ucfirst($panel).'PanelProvider'; + $this->registerPanelProviderInBootstrapProviders($providerClass, $panel); } } - public function updatePanelDependencies(string $panel): void + protected function panelExists(string $panel): bool { if (! isset($this->panelMap[$panel])) { - error("❌ Panel '{$panel}' not found in panel map."); - - return; + return false; } $providerPath = base_path($this->panelMap[$panel]['path'].'/'.ucfirst($panel).'PanelProvider.php'); - if (! File::exists($providerPath)) { - error("❌ Panel provider not found: {$providerPath}"); - - return; - } - - info("πŸ” Analyzing plugins for panel '{$panel}'..."); - - $content = File::get($providerPath); - $plugins = $this->extractPluginsFromProvider($content); - - if (empty($plugins)) { - info("ℹ️ No plugins found in panel '{$panel}'."); - - return; - } + return File::exists($providerPath); + } - $requiredPackages = []; - foreach ($plugins as $plugin) { - $package = $this->guessComposerPackageFromClass($plugin); - if ($package && ! in_array($package, $requiredPackages)) { - $requiredPackages[] = $package; + protected function mapProviderClassToPanelKey(string $providerClass): ?string + { + foreach ($this->panelMap as $key => $cfg) { + $expected = $cfg['namespace'].'\\'.ucfirst($key).'PanelProvider'; + if ($providerClass === $expected) { + return $key; } } - if (! empty($requiredPackages)) { - $this->updatePanelPackageComposerJson($panel, $requiredPackages); - } - } + if (preg_match('/^App\\\\Providers\\\\Filament\\\\([A-Za-z]+)PanelProvider$/', $providerClass, $m)) { + $panel = strtolower($m[1]); - protected function extractPluginsFromProvider(string $content): array - { - $plugins = []; - - if (preg_match('/->plugins\(\[(.*?)\]\)/s', $content, $matches)) { - $pluginLines = explode(',', $matches[1]); - foreach ($pluginLines as $line) { - $line = trim($line); - if (preg_match('/\\\\?([\w\\\\]+)::make\(\)/', $line, $matches)) { - $plugins[] = ltrim($matches[1], '\\'); - } - } + return isset($this->panelMap[$panel]) ? $panel : null; } - return $plugins; + return null; } protected function configureAuthUserModelForPanel(string $panel, string $providerPath): void @@ -419,7 +226,7 @@ protected function configureAuthUserModelForPanel(string $panel, string $provide ->login( fn () => Filament::auth({$userModel}::class), ) - PHP; +PHP; $content = preg_replace( '/(->path\(.*?\))/', @@ -433,58 +240,43 @@ protected function configureAuthUserModelForPanel(string $panel, string $provide info("βœ… Auth configuration for panel '{$panel}' set to: {$userModel}"); } - protected function panelExists(string $panel): bool + protected function extractPanelPath(string $content): ?string { - if (! isset($this->panelMap[$panel])) { - return false; + if (preg_match('/->path\(\s*[\'\"]([^\'\"]+)[\'\"]/m', $content, $m)) { + return $m[1]; } - $providerPath = base_path($this->panelMap[$panel]['path'].'/'.ucfirst($panel).'PanelProvider.php'); - - return File::exists($providerPath); + return null; } - protected function registerPanelProviderInAppServiceProvider(string $providerClass, string $panel): void + protected function setPanelPath(string $content, string $newPath): string { - $appServiceProviderPath = app_path('Providers/AppServiceProvider.php'); - - if (! File::exists($appServiceProviderPath)) { - error("❌ AppServiceProvider.php not found at {$appServiceProviderPath}"); - - return; + if (preg_match('/->path\(.*?\)/m', $content)) { + return preg_replace('/->path\(.*?\)/m', "->path('".addslashes($newPath)."')", $content, 1); } - $content = File::get($appServiceProviderPath); - - if (str_contains($content, $providerClass.'::class')) { - info("βœ… Provider {$providerClass} is already registered in AppServiceProvider."); + return preg_replace('/(->id\(.*?\))/m', "$1\n ->path('".addslashes($newPath)."')", $content, 1); + } - return; + protected function setAuthUserModel(string $content, string $userModelFqn): string + { + if (! str_contains($content, 'use Filament\\Facades\\Filament;')) { + $content = preg_replace('/(namespace\s+[^;]+;)/', "$1\n\nuse Filament\\Facades\\Filament;", $content, 1); } - $pattern = '/public function register\s*\([^)]*\)\s*(?::\s*\w+)?\s*\{(.*?)\}/s'; - - if (preg_match($pattern, $content, $matches)) { - $registerBody = $matches[1]; - - $registerLine = " \$this->app->register({$providerClass}::class);"; - - if (str_contains($registerBody, $registerLine)) { - info("βœ… Provider {$providerClass} already registered inside register()."); - - return; - } - - $registerBodyNew = rtrim($registerBody)."\n".$registerLine; - - $contentNew = preg_replace($pattern, "public function register(): void\n {\n{$registerBodyNew}\n }", $content); + $authCode = <<login( + fn () => Filament::auth({$userModelFqn}::class), + ) +PHP; - File::put($appServiceProviderPath, $contentNew); + if (str_contains($content, 'Filament::auth(')) { + $content = preg_replace('/Filament::auth\(([^\)]+)\)/', 'Filament::auth('.$userModelFqn.'::class)', $content, 1); - info("βœ… Registered {$providerClass} in AppServiceProvider::register()"); - } else { - warning("⚠️ Could not find register() method in AppServiceProvider.php to register provider {$providerClass}."); + return $content; } + + return preg_replace('/(->path\(.*?\))/', "$1\n {$authCode}", $content, 1); } protected function registerPanelProviderInBootstrapProviders(string $providerClass, string $panel): void @@ -492,67 +284,37 @@ protected function registerPanelProviderInBootstrapProviders(string $providerCla $bootstrapProvidersPath = base_path('bootstrap/providers.php'); if (! File::exists($bootstrapProvidersPath)) { + warning("⚠️ bootstrap/providers.php not found. Cannot register {$providerClass}."); + return; } $content = File::get($bootstrapProvidersPath); - $appClass = 'App\\Providers\\Filament\\'.ucfirst($panel).'PanelProvider'; - $packageClass = $this->panelMap[$panel]['namespace'].'\\'.ucfirst($panel).'PanelProvider'; + $mooxProvider = $this->panelMap[$panel]['namespace'].'\\'.ucfirst($panel).'PanelProvider'; + $appProvider = 'App\\Providers\\Filament\\'.ucfirst($panel).'PanelProvider'; - $desiredClass = $providerClass; - $otherClass = ($providerClass === $appClass) ? $packageClass : $appClass; - - $patternRemove = '/^\s*'.preg_quote($otherClass, '/').'::class\s*,?\s*$/m'; + $content = preg_replace( + '/^\s*('.preg_quote($mooxProvider, '/').'|'.preg_quote($appProvider, '/').')::class,?\s*$/m', + '', + $content + ); - $contentWithoutOther = preg_replace($patternRemove, '', $content); + if (preg_match('/return\s*\[(.*?)\];/s', $content, $matches)) { + $inner = trim($matches[1]); - if (str_contains($contentWithoutOther, $desiredClass.'::class')) { - if ($contentWithoutOther !== $content) { - File::put($bootstrapProvidersPath, $contentWithoutOther); - info('🧹 Removed other provider variant from bootstrap/providers.php'); - } else { - info("βœ… Provider {$providerClass} already present in bootstrap/providers.php"); + if ($inner !== '' && ! str_ends_with(trim($inner), ',')) { + $inner .= ','; } - return; - } + $inner .= "\n {$providerClass}::class,"; - $updated = preg_replace_callback( - '/return\s*\[([\s\S]*?)\];/m', - function (array $matches) use ($desiredClass) { - $inner = rtrim($matches[1]); - if ($inner !== '' && ! str_ends_with(trim($inner), ',')) { - $inner .= ','; - } - $inner .= "\n {$desiredClass}::class,"; - - return "return [\n{$inner}\n];"; - }, - $contentWithoutOther ?? $content, - 1 - ); + $newContent = preg_replace('/return\s*\[.*?\];/s', "return [\n{$inner}\n];", $content, 1); + File::put($bootstrapProvidersPath, $newContent); - if ($updated && $updated !== $content) { - File::put($bootstrapProvidersPath, $updated); - info("βœ… Registered {$providerClass} in bootstrap/providers.php"); + info("βœ… Registered {$providerClass} in bootstrap/providers.php."); } else { - // Fallback: simple append before closing array, for non-matching formats - $pos = strrpos($content, '];'); - if ($pos !== false) { - $before = substr($content, 0, $pos); - $after = substr($content, $pos); - $line = " {$desiredClass}::class,\n"; - // Ensure trailing comma before appending if needed - if (! preg_match('/,\s*$/', trim($before))) { - $before = rtrim($before).",\n"; - } - $newContent = $before.$line.$after; - File::put($bootstrapProvidersPath, $newContent); - info("βœ… Registered {$providerClass} in bootstrap/providers.php (fallback)"); - } else { - warning("⚠️ Could not update bootstrap/providers.php to register {$providerClass}"); - } + warning("⚠️ Could not find return array in bootstrap/providers.php. Please add {$providerClass} manually."); } } @@ -564,7 +326,7 @@ protected function cleanupPanelProviderInAppServiceProvider(string $panel): void } $content = File::get($appServiceProviderPath); - $pattern = '/\$this->\\app->\\register\((App\\\\Providers\\\\Filament\\\\'.ucfirst($panel).'PanelProvider::class)\);/'; + $pattern = '/\$this->app->register\(App\\\\Providers\\\\Filament\\\\'.ucfirst($panel).'PanelProvider::class\);/'; $updated = preg_replace($pattern, '', $content); if ($updated !== null && $updated !== $content) { @@ -572,93 +334,4 @@ protected function cleanupPanelProviderInAppServiceProvider(string $panel): void info("🧹 Removed published App provider registration from AppServiceProvider for panel '{$panel}'"); } } - - protected function getPanelFromBootstrapProviders(): ?string - { - $bootstrapProvidersPath = base_path('bootstrap/providers.php'); - if (! File::exists($bootstrapProvidersPath)) { - return null; - } - - $content = File::get($bootstrapProvidersPath); - - if (! preg_match_all('/([\\\\A-Za-z0-9_]+)::class/', $content, $matches)) { - return null; - } - - foreach ($matches[1] as $class) { - $panel = $this->mapProviderClassToPanelKey($class); - if ($panel !== null) { - return $panel; - } - } - - return null; - } - - protected function mapProviderClassToPanelKey(string $providerClass): ?string - { - foreach ($this->panelMap as $key => $cfg) { - $expected = $cfg['namespace'].'\\'.ucfirst($key).'PanelProvider'; - if ($providerClass === $expected) { - return $key; - } - } - - if (preg_match('/^App\\\\Providers\\\\Filament\\\\([A-Za-z]+)PanelProvider$/', $providerClass, $m)) { - $panel = strtolower($m[1]); - - return isset($this->panelMap[$panel]) ? $panel : null; - } - - return null; - } - - protected function ensurePanelForKey(string $panel): void - { - if (! isset($this->panelMap[$panel])) { - error("❌ Unknown panel '{$panel}'."); - - return; - } - - if ($this->panelExists($panel)) { - return; - } - - $panelId = $panel; - $this->call('make:filament-panel', [ - 'id' => $panelId, - ]); - - $from = base_path('app/Providers/Filament/'.ucfirst($panel).'PanelProvider.php'); - $toDir = base_path($this->panelMap[$panel]['path']); - $to = $toDir.'/'.ucfirst($panel).'PanelProvider.php'; - - if (! File::exists($from)) { - warning("⚠️ Expected file {$from} not found. Skipping panel move."); - - return; - } - - File::ensureDirectoryExists($toDir); - File::move($from, $to); - info("βœ… Moved panel provider to: {$to}"); - - $content = File::get($to); - $content = str_replace( - 'namespace App\\Providers\\Filament;', - 'namespace '.$this->panelMap[$panel]['namespace'].';', - $content - ); - File::put($to, $content); - info('🧭 Updated namespace to: '.$this->panelMap[$panel]['namespace']); - - $this->registerDefaultPluginsForPanel($panel, $to); - $this->configureAuthUserModelForPanel($panel, $to); - - $providerClass = $this->panelMap[$panel]['namespace'].'\\'.ucfirst($panel).'PanelProvider'; - $this->registerPanelProviderInBootstrapProviders($providerClass, $panel); - $this->cleanupPanelProviderInAppServiceProvider($panel); - } } diff --git a/packages/core/src/Services/PackageService.php b/packages/core/src/Services/PackageService.php index e4c0ff3e8..71a096efa 100644 --- a/packages/core/src/Services/PackageService.php +++ b/packages/core/src/Services/PackageService.php @@ -2,30 +2,76 @@ namespace Moox\Core\Services; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\File; +use function Laravel\Prompts\info; + class PackageService { /** * Return migration paths (relative to project base) declared by the package API. - * Supports extra.moox.install.auto_migrate as string or array. - * - * @param array{name?:string} $package - * @return array */ public function getMigrations(array $package): array { $meta = $this->readPackageMeta($package['name'] ?? ''); $auto = $meta['extra']['moox']['install']['auto_migrate'] ?? null; $paths = []; + $allSkipped = true; + $anyFound = false; foreach ($this->normalizeToArray($auto) as $rel) { - $full = $this->toProjectRelativePath($meta['baseDir'], $rel); - if ($full) { - $paths[] = $full; + $full = $this->toAbsolutePath($meta['baseDir'], $rel); + if (! $full || ! File::isDirectory($full)) { + continue; + } + + foreach (File::files($full) as $file) { + $anyFound = true; + $filename = $file->getFilename(); + + if (str_ends_with($filename, '.stub')) { + $baseName = str_replace('.php.stub', '.php', $filename); + $existing = collect(File::files(database_path('migrations'))) + ->first(fn ($f) => str_ends_with($f->getFilename(), $baseName)); + + if ($existing) { + info("ℹ️ Migration '{$baseName}' already exists, skipped."); + $paths[] = 'database/migrations/'.$existing->getFilename(); + + continue; + } + + $allSkipped = false; + $newName = date('Y_m_d_His').'_'.$baseName; + $target = database_path('migrations/'.$newName); + + File::copy($file->getRealPath(), $target); + $content = File::get($target); + $content = str_replace(['{{ timestamp }}'], [date('Y_m_d_His')], $content); + File::put($target, $content); + + $paths[] = 'database/migrations/'.$newName; + info("βœ… Migration '{$baseName}' was created."); + } elseif (str_ends_with($filename, '.php')) { + $paths[] = $file->getRealPath(); + $allSkipped = false; + } } } + if (! $anyFound) { + info('ℹ️ No migrations found.'); + + return []; + } + + if ($allSkipped) { + info('ℹ️ All migrations already exist.'); + + return []; + } + return $paths; } @@ -41,12 +87,66 @@ public function checkMigrationStatus(string $migrationPath): array } /** - * Return config publish instructions. If the package exposes vendor publish tags - * via extra.moox.install.auto_publish, encode as special keys 'tag:' => true. - * - * @param array{name?:string} $package - * @return array + * Publish package configs only if they do not exist yet. */ + public function publishConfigs(array $package): bool + { + $configs = $this->getConfig($package); + $updatedAny = false; + + foreach ($configs as $path => $contentOrTrue) { + if (is_string($path) && str_starts_with($path, 'tag:')) { + $tag = substr($path, 4); + $meta = $this->readPackageMeta($package['name'] ?? ''); + $packageConfigDir = rtrim($meta['baseDir'], '/').'/config'; + $filesInPackage = File::isDirectory($packageConfigDir) ? File::files($packageConfigDir) : []; + + $allExist = true; + foreach ($filesInPackage as $file) { + $targetPath = config_path($file->getFilename()); + if (! File::exists($targetPath)) { + $allExist = false; + break; + } + } + + if ($allExist) { + info("ℹ️ Configs for package '{$package['name']}' already exist. Skipping tag '{$tag}'."); + + continue; + } + + info("πŸ“¦ Publishing vendor tag: {$tag}"); + Artisan::call('vendor:publish', [ + '--tag' => $tag, + '--force' => false, + '--no-interaction' => true, + ]); + $updatedAny = true; + + continue; + } + + $publishPath = config_path(basename($path)); + if (! File::exists($publishPath)) { + info("πŸ“„ Publishing new config: {$path}"); + File::put($publishPath, $contentOrTrue); + $updatedAny = true; + + continue; + } + + $existingContent = File::get($publishPath); + if ($existingContent !== $contentOrTrue && confirm("⚠️ Config file {$path} has changes. Overwrite?", false)) { + info("πŸ”„ Updating config file: {$path}"); + File::put($publishPath, $contentOrTrue); + $updatedAny = true; + } + } + + return $updatedAny; + } + public function getConfig(array $package): array { $meta = $this->readPackageMeta($package['name'] ?? ''); @@ -55,7 +155,6 @@ public function getConfig(array $package): array foreach ($this->normalizeToArray($auto) as $tagOrPath) { if (is_string($tagOrPath) && $tagOrPath !== '') { - // Treat as vendor publish tag $result['tag:'.$tagOrPath] = true; } } @@ -63,13 +162,6 @@ public function getConfig(array $package): array return $result; } - /** - * Return seeder classes from extra.moox.install.seed. - * Accepts class names or file paths; file paths will be parsed for FQCN. - * - * @param array{name?:string} $package - * @return array Fully-qualified class names - */ public function getRequiredSeeders(array $package): array { $meta = $this->readPackageMeta($package['name'] ?? ''); @@ -88,28 +180,53 @@ public function getRequiredSeeders(array $package): array $classes[] = $fqcn; } } else { - $classes[] = $entry; // assume already FQCN + $classes[] = $entry; } } return array_values(array_unique($classes)); } - /** - * Optional: plugins read from package API in future. Return empty for now. - */ public function getPlugins(array $package): array { - return []; + $meta = $this->readPackageMeta($package['name'] ?? ''); + $baseDir = $meta['baseDir']; + $psr4 = $meta['autoload']['psr-4'] ?? []; + + if (empty($psr4) || ! is_array($psr4)) { + return []; + } + + $baseNamespace = array_key_first($psr4); + $nsPath = rtrim($psr4[$baseNamespace] ?? 'src', '/'); + $scanDir = rtrim($baseDir, '/').'/'.$nsPath; + + if (! File::isDirectory($scanDir)) { + return []; + } + + $plugins = []; + $files = File::allFiles($scanDir); + foreach ($files as $file) { + $filename = $file->getFilename(); + if (! str_ends_with($filename, 'Plugin.php')) { + continue; + } + + $relativePath = str_replace($scanDir.'/', '', $file->getPathname()); + $relativeClass = str_replace(['/', '.php'], ['\\', ''], $relativePath); + $fqcn = rtrim($baseNamespace, '\\').'\\'.$relativeClass; + + if (str_contains($fqcn, ' ')) { + continue; + } + + $plugins[] = $fqcn; + } + + return array_values(array_unique($plugins)); } - /** - * Return shell commands to run after install (project root). - * Uses extra.moox.install.auto_run. - * - * @param array{name?:string} $package - * @return array - */ public function getAutoRunCommands(array $package): array { $meta = $this->readPackageMeta($package['name'] ?? ''); @@ -118,13 +235,6 @@ public function getAutoRunCommands(array $package): array return array_values(array_filter($this->normalizeToArray($cmds), fn ($c) => is_string($c) && $c !== '')); } - /** - * Return shell commands to run after install (package dir). - * Uses extra.moox.install.auto_runhere. - * - * @param array{name?:string} $package - * @return array - */ public function getAutoRunHereCommands(array $package): array { $meta = $this->readPackageMeta($package['name'] ?? ''); @@ -140,9 +250,6 @@ public function getAutoRunHereCommands(array $package): array return $list; } - /** - * Helpers - */ private function normalizeToArray(mixed $value): array { if ($value === null || $value === false) { @@ -152,23 +259,23 @@ private function normalizeToArray(mixed $value): array return is_array($value) ? $value : [$value]; } - /** - * @return array{baseDir:string,extra:array} - */ private function readPackageMeta(string $composerName): array { $baseDir = $this->resolvePackageBaseDir($composerName); $composerJson = $baseDir ? $baseDir.'/composer.json' : null; $extra = []; + $autoload = []; if ($composerJson && File::exists($composerJson)) { $json = json_decode(File::get($composerJson), true) ?: []; $extra = $json['extra'] ?? []; + $autoload = $json['autoload'] ?? []; } return [ 'baseDir' => $baseDir ?: base_path(), 'extra' => $extra, + 'autoload' => $autoload, ]; } @@ -178,16 +285,14 @@ private function resolvePackageBaseDir(string $composerName): ?string return null; } - // Prefer local path repo under packages/ $short = str_contains($composerName, '/') ? explode('/', $composerName)[1] : $composerName; $local = base_path('packages/'.$short); - if (\Illuminate\Support\Facades\File::isDirectory($local)) { + if (File::isDirectory($local)) { return $local; } - // Fallback to vendor path $vendor = base_path('vendor/'.$composerName); - if (\Illuminate\Support\Facades\File::isDirectory($vendor)) { + if (File::isDirectory($vendor)) { return $vendor; } @@ -210,9 +315,7 @@ private function toProjectRelativePath(?string $baseDir, string $relative): ?str private function toAbsolutePath(string $baseDir, string $relative): ?string { - $path = rtrim($baseDir, '/').'/'.ltrim($relative, '/'); - - return $path; + return rtrim($baseDir, '/').'/'.ltrim($relative, '/'); } private function extractClassFromFile(string $filePath): ?string @@ -220,6 +323,7 @@ private function extractClassFromFile(string $filePath): ?string if (! File::exists($filePath)) { return null; } + $content = File::get($filePath); $namespace = null; $class = null; diff --git a/packages/devops/src/Panels/DevopsPanelProvider.php b/packages/devops/src/Panels/DevopsPanelProvider.php index 025b9ca29..3594f10e1 100644 --- a/packages/devops/src/Panels/DevopsPanelProvider.php +++ b/packages/devops/src/Panels/DevopsPanelProvider.php @@ -2,7 +2,6 @@ namespace Moox\Devops\Panels; -use Filament\Facades\Filament; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -22,25 +21,25 @@ class DevopsPanelProvider extends PanelProvider { + protected const USER_MODEL = \Moox\User\Models\User::class; + public function panel(Panel $panel): Panel { return $panel ->default() ->id('devops') ->path('devops') - ->login( - fn () => Filament::auth(\Moox\User\Models\User::class), - ) + ->authGuard('web') ->login() ->colors([ - 'primary' => Color::Amber, + 'primary' => Color::Indigo, ]) - ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') - ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') + ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') + ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->pages([ Dashboard::class, ]) - ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets') + ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ AccountWidget::class, FilamentInfoWidget::class, diff --git a/packages/jobs/composer.json b/packages/jobs/composer.json index 65487c66a..ee9c05bb1 100644 --- a/packages/jobs/composer.json +++ b/packages/jobs/composer.json @@ -36,27 +36,18 @@ }, "extra": { "moox": { - "stability": "stable" - }, - "laravel": { - "providers": [ - "Moox\\Jobs\\JobsServiceProvider", - "Moox\\Jobs\\JobManagerProvider" - ], - "aliases": { - "JobMonitor": "Moox\\Jobs\\JobManagerProvider\\Facade" - } - }, - "moox": { + "stability": "stable", "require": { "spatie/laravel-medialibrary": { "auto_publish": "spatie-jobs-config" } }, "install": { - "auto_migrate": "database/migrations", + "auto_migrate": [ + "database/migrations" + ], "seed": "database/seeders/JobsSeeder.php", - "auto_publish": "moox-jobs-config", + "auto_publish": "jobs-config", "auto_entities": { "Some Resource": true, "Another Resource": null @@ -66,12 +57,12 @@ "Another Class": "Moox\\Jobs\\AnotherClass" }, "auto_run": { - "Run this": "php artisan run:this", - "Build the frontend": "npm run build", - "Clear the cache": "php artisan cache:clear" + "Run this": "", + "Build the frontend": "", + "Clear the cache": "" }, "auto_runhere": { - "Build the frontend": "npm run build" + "Build the frontend": "" } }, "update": { @@ -82,7 +73,17 @@ "migrate": "database/migrations", "remove": "moox-jobs-config" } + }, + "laravel": { + "providers": [ + "Moox\\Jobs\\JobsServiceProvider", + "Moox\\Jobs\\JobManagerProvider" + ], + "aliases": { + "JobMonitor": "Moox\\Jobs\\JobManagerProvider\\Facade" + } } }, + "prefer-stable": true }