diff --git a/src/Laravel/Command/InstallCommand.php b/src/Laravel/Command/InstallCommand.php index a37753c4..7153622a 100644 --- a/src/Laravel/Command/InstallCommand.php +++ b/src/Laravel/Command/InstallCommand.php @@ -11,6 +11,9 @@ use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; +use Illuminate\Support\Str; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Finder\Finder; /** @@ -18,24 +21,25 @@ use Symfony\Component\Finder\Finder; * * This command provides an elegant CLI experience for installing PHPFlasher resources * including assets (JS and CSS files) and configuration files. It discovers - * all registered PHPFlasher plugins and installs their resources with visual feedback. + * all registered PHPFlasher plugins and installs their resources with stunning visuals. * - * Design patterns: - * - Command: Implements the command pattern for Artisan CLI integration - * - Discovery: Automatically discovers and processes registered plugins - * - Builder: Constructs the installation process in distinct steps + * @author Younes Khoubza */ final class InstallCommand extends Command { /** - * Command signature. + * Command signature with support for multiple options. * * @var string */ protected $signature = 'flasher:install {--c|config : Publish all config files to the config directory} {--s|symlink : Symlink PHPFlasher assets instead of copying them} - {--force : Overwrite existing files without confirmation}'; + {--force : Overwrite existing files without confirmation} + {--d|debug : Show detailed debug information during installation} + {--minimal : Display minimal output during installation} + {--a|ascii : Use ASCII art instead of Unicode characters (for terminals with limited support)} + {--no-animation : Disable animations for CI/CD environments}'; /** * Command description. @@ -49,11 +53,92 @@ final class InstallCommand extends Command */ private float $startTime; + /** + * Debug mode flag. + */ + private bool $debugMode = false; + + /** + * Minimal output mode flag. + */ + private bool $minimalMode = false; + + /** + * Disable animations flag. + */ + private bool $noAnimation = false; + + /** + * Use ASCII instead of Unicode. + */ + private bool $asciiMode = false; + /** * Collection of results for summary. */ private Collection $results; + /** + * Performance metrics for debug mode. + */ + private array $metrics = []; + + /** + * Advanced spinner characters with more visual variety. + */ + private array $spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + + /** + * ASCII fallback spinner characters. + */ + private array $asciiSpinnerChars = ['|', '/', '-', '\\']; + + /** + * Debug line count for dynamic collapsing. + */ + private int $debugLineCount = 0; + + /** + * Terminal dimensions. + */ + private array $terminalDimensions = [ + 'width' => 80, + 'height' => 24, + ]; + + /** + * File type icons for visualization. + */ + private array $fileTypeIcons = [ + 'js' => '📜', + 'css' => '🎨', + 'json' => '📋', + 'php' => '🐘', + 'default' => '📄', + ]; + + /** + * ASCII fallback icons. + */ + private array $asciiFileTypeIcons = [ + 'js' => '[JS]', + 'css' => '[CSS]', + 'json' => '[JSON]', + 'php' => '[PHP]', + 'default' => '[FILE]', + ]; + + /** + * Success messages for random selection. + */ + private array $successMessages = [ + 'All set! Your notifications will now look fabulous! ✨', + 'Success! Get ready for notification awesomeness! 🚀', + 'Installation complete! Your users will love these notifications! 💖', + 'PHPFlasher installed! Time to make your app shine! ⭐', + 'Done! Now you have the power of beautiful notifications! 💪', + ]; + /** * Creates a new InstallCommand instance. * @@ -72,16 +157,43 @@ final class InstallCommand extends Command */ public function handle(): int { - $this->startTime = microtime(true); + $this->configureOutput(); - // Display the welcome banner with stylish animation - $this->displayWelcomeBanner(); + $this->startTiming('total'); + + $this->startTime = microtime(true); + $this->debugMode = $this->option('debug'); + $this->minimalMode = $this->option('minimal'); + $this->noAnimation = $this->option('no-animation') || $this->runningInCI(); + $this->asciiMode = $this->option('ascii') || !$this->supportsUnicode(); + + // Detect terminal dimensions for responsive output + $this->detectTerminalDimensions(); + + // Ensure output is cleared and properly formatted + if (function_exists('pcntl_signal')) { + pcntl_signal(SIGINT, function () { + $this->output->writeln(''); + $this->output->writeln('Installation aborted!'); + exit(1); + }); + } + + // Display the welcome banner with refined animation + if (!$this->minimalMode) { + $this->displayWelcomeBanner(); + } else { + $this->info('PHPFlasher Installation'); + $this->newLine(); + } // Configuration options $useSymlinks = $this->option('symlink'); $publishConfig = $this->option('config'); $force = $this->option('force'); + $this->startTiming('setup'); + // Setup installation environment $publicDir = App::publicPath('/vendor/flasher/'); $filesystem = new Filesystem(); @@ -99,50 +211,82 @@ final class InstallCommand extends Command $filesystem->makeDirectory($publicDir, 0755, true, true); } - // Installation configuration summary - $this->displayInstallationConfig($useSymlinks, $publishConfig, $force); + $this->stopTiming('setup'); - // Process each plugin + // Installation configuration summary + if (!$this->minimalMode) { + $this->displayInstallationConfig($useSymlinks, $publishConfig, $force); + } + + // Discover plugins + $this->startTiming('discovery'); + $providers = $this->discoverPluginProviders(); + $this->stopTiming('discovery'); + + if ($this->debugMode) { + $this->debugGroupStart('Plugin Discovery'); + $this->debug("Found {$providers->count()} service providers", 'info'); + $providers->each(function ($provider, $index) { + $this->debug("Provider #{$index}: " . get_class($provider), 'dim'); + }); + $this->debug("Discovered {$providers->count()} PHPFlasher plugins", 'success'); + $this->debugGroupEnd(); + } + + $this->newLine(); + if (!$this->minimalMode) { + $this->line(' Discovering and installing plugins...'); + } else { + $this->info('Discovering and installing plugins...'); + } + $this->newLine(); + + // Create optimized progress bar + $progressBar = $this->createStylizedProgressBar($providers->count()); + + // Process plugins with progress bar $files = []; $exitCode = self::SUCCESS; - // Discover plugins - $providers = $this->discoverPluginProviders(); + $this->startTiming('plugins_processing'); - $this->newLine(); - $this->info(' Discovering and installing plugins...'); - $this->newLine(); - - // Create and configure progress bar - $progressBar = $this->output->createProgressBar($providers->count()); - $progressBar->setFormat( - " %current%/%max% [%bar%] %percent:3s%%\n %message%" - ); - $progressBar->setBarCharacter('▓'); - $progressBar->setEmptyBarCharacter('░'); - $progressBar->setProgressCharacter('▓'); - - // Process plugins with progress bar + // For smoother output, we'll collect results first then display $providers->each(function ($provider, $index) use ($progressBar, &$files, &$exitCode, $useSymlinks, $publishConfig, $force) { + $this->startTiming("plugin_{$index}"); + $plugin = $provider->createPlugin(); $configFile = $provider->getConfigurationFile(); - // Rotating animation characters for processing indicator - $chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; - $char = $chars[$index % count($chars)]; - - $progressBar->setMessage("{$char} Processing: {$plugin->getAlias()}"); + // Update progress with spinning indicator + if (!$this->minimalMode) { + $spinners = $this->asciiMode ? $this->asciiSpinnerChars : $this->spinnerChars; + $char = $spinners[$index % count($spinners)]; + $progressBar->setMessage("{$char} Processing: {$plugin->getAlias()}"); + } $progressBar->advance(); + // Use output buffering for smoother display + ob_start(); + try { // Process assets + $this->startTiming("assets_{$plugin->getAlias()}"); + + if ($this->debugMode) { + $this->debugGroupStart("Plugin: {$plugin->getAlias()}"); + } + $publishedFiles = $this->publishAssets($plugin, App::publicPath('/vendor/flasher/'), $useSymlinks, $force); + $this->stopTiming("assets_{$plugin->getAlias()}"); + $files[] = $publishedFiles; // Process config if needed $configPublished = false; if ($publishConfig) { + $this->startTiming("config_{$plugin->getAlias()}"); $configPublished = $this->publishConfig($plugin, $configFile, $force); + $this->stopTiming("config_{$plugin->getAlias()}"); } // Store results for summary @@ -151,10 +295,16 @@ final class InstallCommand extends Command 'status' => 'success', 'assets' => \count($publishedFiles), 'config' => $configPublished ? 'Yes' : 'No', + 'time' => $this->getElapsedTime("plugin_{$index}"), ]); - // Small delay for visual effect - usleep(50000); + if ($this->debugMode) { + $this->debug( + "Published {$plugin->getAlias()} in " . $this->getElapsedTime("plugin_{$index}") . 'ms', + 'success' + ); + $this->debugGroupEnd(); + } } catch (\Exception $e) { $exitCode = self::FAILURE; $this->results->push([ @@ -163,34 +313,199 @@ final class InstallCommand extends Command 'message' => $e->getMessage(), 'assets' => 0, 'config' => 'No', + 'time' => $this->getElapsedTime("plugin_{$index}"), ]); + + if ($this->debugMode) { + $this->debug("Error publishing {$plugin->getAlias()}: " . $e->getMessage(), 'error'); + $this->debug('Exception trace: ' . $e->getTraceAsString(), 'dim'); + $this->debugGroupEnd(); + } + } + + $this->stopTiming("plugin_{$index}"); + + // Flush output buffer for smoother display + ob_end_flush(); + + // Use minimal delay for visual effect (only if not in minimal mode) + if (!$this->minimalMode && !$this->noAnimation) { + usleep(20000); // 20ms is smoother but still visible } }); + $this->stopTiming('plugins_processing'); + $progressBar->finish(); $this->newLine(2); // Create manifest + $this->startTiming('manifest'); $this->task('Creating asset manifest', function () use ($files) { $this->assetManager->createManifest(array_merge([], ...$files)); return true; }); + $this->stopTiming('manifest'); // Display installation summary $this->displayComprehensiveSummary($exitCode); + $this->stopTiming('total'); + + // Show debug performance metrics if requested + if ($this->debugMode) { + $this->displayPerformanceMetrics(); + } + return $exitCode; } /** - * Display a stylish welcome banner with reveal animation. + * Configure output styles. + */ + private function configureOutput(): void + { + $formatter = $this->output->getFormatter(); + + // Add special styles for debug output + $formatter->setStyle('success', new OutputFormatterStyle('green')); + $formatter->setStyle('info', new OutputFormatterStyle('blue')); + $formatter->setStyle('notice', new OutputFormatterStyle('yellow')); + $formatter->setStyle('error', new OutputFormatterStyle('red')); + $formatter->setStyle('dim', new OutputFormatterStyle('gray')); + $formatter->setStyle('highlight', new OutputFormatterStyle('cyan', null, ['bold'])); + + // Box drawing styles + $formatter->setStyle('box', new OutputFormatterStyle('blue')); + $formatter->setStyle('box-title', new OutputFormatterStyle('cyan', null, ['bold'])); + } + + /** + * Detect terminal dimensions for responsive output. + */ + private function detectTerminalDimensions(): void + { + if (function_exists('exec')) { + @exec('tput cols 2>/dev/null', $columns, $return_var); + if ($return_var === 0 && isset($columns[0])) { + $this->terminalDimensions['width'] = (int)$columns[0]; + } + + @exec('tput lines 2>/dev/null', $lines, $return_var); + if ($return_var === 0 && isset($lines[0])) { + $this->terminalDimensions['height'] = (int)$lines[0]; + } + } + } + + /** + * Check if we're running in a CI environment. + */ + private function runningInCI(): bool + { + return (bool)( + getenv('CI') || + getenv('CONTINUOUS_INTEGRATION') || + getenv('GITHUB_ACTIONS') || + getenv('GITLAB_CI') || + getenv('TRAVIS') || + getenv('CIRCLECI') + ); + } + + /** + * Check if terminal supports Unicode characters. + */ + private function supportsUnicode(): bool + { + return stripos(getenv('LANG') ?: '', 'UTF-8') !== false || + stripos(getenv('LC_ALL') ?: '', 'UTF-8') !== false; + } + + /** + * Create a stylized progress bar with custom format. + */ + private function createStylizedProgressBar(int $max): ProgressBar + { + $progressBar = $this->output->createProgressBar($max); + + if ($this->minimalMode) { + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); + } else { + if ($this->asciiMode) { + $progressBar->setFormat( + " %current%/%max% [%bar%] %percent:3s%%\n %message%" + ); + $progressBar->setBarCharacter('='); + $progressBar->setEmptyBarCharacter('-'); + $progressBar->setProgressCharacter('>'); + } else { + // Enhanced Unicode progress bar + $progressBar->setFormat( + " %current%/%max% [%bar%] %percent:3s%%\n %message%" + ); + $progressBar->setBarCharacter('▓'); + $progressBar->setEmptyBarCharacter('░'); + $progressBar->setProgressCharacter('▓'); + } + } + + return $progressBar; + } + + /** + * Display a stylish welcome banner with smoother animation. */ private function displayWelcomeBanner(): void { + $this->startTiming('banner'); $this->newLine(); - // Core banner with stylized typography - $banner = [ + // Select banner based on terminal capabilities + $banner = $this->asciiMode ? $this->getAsciiBanner() : $this->getUnicodeBanner(); + + // If animations are disabled, just output the banner + if ($this->noAnimation) { + foreach ($banner as $line) { + $this->line($line); + } + } else { + // Use output buffering for smoother animation + ob_start(); + foreach ($banner as $line) { + $this->line($line); + // Flush immediately for smoother animation + ob_flush(); + flush(); + usleep(20000); // 20ms for smoother animation + } + ob_end_flush(); + } + + // Title with version + $this->newLine(); + + // Use terminal-aware formatting for the title + $titleWidth = $this->terminalDimensions['width'] >= 100 ? + $this->terminalDimensions['width'] - 20 : + 60; + + $title = 'PHPFLASHER RESOURCE INSTALLER v11'; + $padding = max(0, ($titleWidth - strlen(strip_tags($title))) / 2); + $paddingStr = str_repeat(' ', (int)$padding); + + $this->line(' ' . $paddingStr . 'PHPFLASHER RESOURCE INSTALLER v11'); + $this->newLine(); + + $this->stopTiming('banner'); + } + + /** + * Get the Unicode banner for terminals that support it. + */ + private function getUnicodeBanner(): array + { + return [ ' ██████╗ ██╗ ██╗██████╗ ███████╗██╗ █████╗ ███████╗██╗ ██╗███████╗██████╗ ', ' ██╔══██╗██║ ██║██╔══██╗██╔════╝██║ ██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗', ' ██████╔╝███████║██████╔╝█████╗ ██║ ███████║███████╗███████║█████╗ ██████╔╝', @@ -198,17 +513,21 @@ final class InstallCommand extends Command ' ██║ ██║ ██║██║ ██║ ███████╗██║ ██║███████║██║ ██║███████╗██║ ██║', ' ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝', ]; + } - // Reveal animation - smoother with shorter delay - foreach ($banner as $line) { - $this->line($line); - usleep(50000); // 50ms delay for smoother animation - } - - // Simplified header (without box characters) - $this->newLine(); - $this->line(' PHPFLASHER RESOURCE INSTALLER'); - $this->newLine(); + /** + * Get ASCII fallback banner for terminals with limited support. + */ + private function getAsciiBanner(): array + { + return [ + ' ______ _ _ ______ ______ _ _____ _ _ ______ _____ ', + ' | ____| | | | ____| ____| | /\ / ____| | | | ____| __ \ ', + ' | |__ | |__| | |__ | |__ | | / \ | (___ | |__| | |__ | |__) |', + ' | __| | __ | __| | __| | | / /\ \ \___ \| __ | __| | _ / ', + ' | | | | | | |____| |____| |____ / ____ \____) | | | | |____| | \ \ ', + ' |_| |_| |_|______|______|______/_/ \_\_____/|_| |_|______|_| \_\', + ]; } /** @@ -218,6 +537,9 @@ final class InstallCommand extends Command { // If force option is enabled, skip confirmation if ($this->option('force')) { + if ($this->debugMode) { + $this->debug("Force flag enabled, cleaning directory without confirmation: {$directory}", 'notice'); + } return true; } @@ -226,7 +548,27 @@ final class InstallCommand extends Command return true; } - // Otherwise ask for confirmation + // Otherwise ask for confirmation with enhanced visuals + if (!$this->minimalMode && !$this->asciiMode) { + $this->newLine(); + $this->line(' ╭' . str_repeat('─', 70) . '╮'); + $this->line(' CONFIRM DIRECTORY CLEANUP' . str_repeat(' ', 47) . ''); + $this->line(' ' . str_repeat(' ', 70) . ''); + + $message = 'The directory exists and needs to be cleaned before installation:'; + $this->line(' ' . $message . str_repeat(' ', 70 - strlen($message)) . ''); + + $dirLine = " {$directory}"; + $this->line(' ' . $dirLine . str_repeat(' ', 70 - strlen(strip_tags($dirLine))) . ''); + + $this->line(' ' . str_repeat(' ', 70) . ''); + $this->line(' ╰' . str_repeat('─', 70) . '╯'); + $this->newLine(); + + return $this->confirm(' • Do you want to clean this directory?', true); + } + + // Simpler version for minimal or ASCII mode return $this->confirm( "The directory {$directory} already exists. Do you want to clean it before installation?", true @@ -239,13 +581,70 @@ final class InstallCommand extends Command private function displayInstallationConfig(bool $useSymlinks, bool $publishConfig, bool $force): void { $this->newLine(); - $this->line(' [ INSTALLATION CONFIGURATION ]'); + + // Box-style header for enhanced visual appeal + if (!$this->asciiMode) { + $this->line(' ╭' . str_repeat('─', 70) . '╮'); + $this->line(' INSTALLATION CONFIGURATION' . str_repeat(' ', 46) . ''); + $this->line(' ╰' . str_repeat('─', 70) . '╯'); + } else { + $this->line(' [ INSTALLATION CONFIGURATION ]'); + } + $this->newLine(); // Enhanced visual presentation of configuration - $this->components->twoColumnDetail('Installation Mode', $useSymlinks ? 'Symlink' : 'Copy'); - $this->components->twoColumnDetail('Publish Config', $publishConfig ? 'Yes' : 'No'); - $this->components->twoColumnDetail('Force Override', $force ? 'Yes' : 'No'); + $this->components->twoColumnDetail( + 'Installation Mode', + $useSymlinks + ? 'Symlink (faster, development)' + : 'Copy (recommended, production)' + ); + + $this->components->twoColumnDetail( + 'Publish Config', + $publishConfig + ? 'Yes (customizable)' + : 'No (using defaults)' + ); + + $this->components->twoColumnDetail( + 'Force Override', + $force + ? 'Yes (will replace existing files)' + : 'No (will preserve existing files)' + ); + + $this->components->twoColumnDetail( + 'Debug Mode', + $this->debugMode + ? 'Enabled (showing detailed information)' + : 'Disabled (standard output)' + ); + + $this->components->twoColumnDetail( + 'Minimal Output', + $this->minimalMode + ? 'Enabled (showing minimal output)' + : 'Disabled (showing standard output)' + ); + + if (!$this->minimalMode) { + $this->components->twoColumnDetail( + 'Animations', + $this->noAnimation + ? 'Disabled (better for CI/CD environments)' + : 'Enabled (interactive visual feedback)' + ); + + $this->components->twoColumnDetail( + 'Character Set', + $this->asciiMode + ? 'ASCII (compatible with all terminals)' + : 'Unicode (enhanced visual experience)' + ); + } + $this->newLine(); } @@ -254,42 +653,82 @@ final class InstallCommand extends Command */ private function discoverPluginProviders(): Collection { - return collect(array_keys(App::getLoadedProviders())) - ->filter(fn ($provider) => is_a($provider, PluginServiceProvider::class, true)) - ->map(fn ($provider) => App::getProvider($provider)) + $providers = collect(array_keys(App::getLoadedProviders())) + ->filter(fn($provider) => is_a($provider, PluginServiceProvider::class, true)) + ->map(fn($provider) => App::getProvider($provider)) ->values(); + + return $providers; } /** - * Execute a task with visual feedback. + * Execute a task with enhanced visual feedback. * - * @param string $title Task title + * @param string $title Task title * @param callable $callback Task callback */ private function task(string $title, callable $callback): bool { - $this->output->write(" • {$title}: "); + $taskName = strtolower(str_replace(' ', '_', $title)); + $this->startTiming("task_{$taskName}"); + + if ($this->minimalMode) { + // Simple display in minimal mode + $this->output->write(" {$title}... "); + } else { + // Enhanced task styling + $bullet = $this->asciiMode ? '>' : '•'; + $this->output->write(" {$bullet} {$title}: "); + } try { + // Use output buffering for smoother display + ob_start(); $result = $callback(); - $this->output->writeln('✓ Complete!'); + ob_end_flush(); - return (bool) $result; + if ($this->minimalMode) { + $this->output->writeln('Done!'); + } else { + $checkmark = $this->asciiMode ? 'Complete!' : '✓ Complete!'; + $this->output->writeln("{$checkmark}"); + } + + if ($this->debugMode) { + $time = $this->getElapsedTime("task_{$taskName}"); + $this->debug("Task '{$title}' completed in {$time}ms", 'success'); + } + + $this->stopTiming("task_{$taskName}"); + return (bool)$result; } catch (\Exception $e) { - $this->output->writeln('✗ Failed!'); - $this->output->writeln(" Error: {$e->getMessage()}"); + ob_end_flush(); + if ($this->minimalMode) { + $this->output->writeln('Failed!'); + $this->output->writeln(" Error: {$e->getMessage()}"); + } else { + $failMark = $this->asciiMode ? 'Failed!' : '✗ Failed!'; + $this->output->writeln("{$failMark}"); + $this->output->writeln(" Error: {$e->getMessage()}"); + } + + if ($this->debugMode) { + $this->debug('Exception trace: ' . $e->getTraceAsString(), 'dim'); + } + + $this->stopTiming("task_{$taskName}"); return false; } } /** - * Publish assets from a plugin to the public directory. + * Publish assets from a plugin to the public directory with enhanced visual feedback. * - * @param PluginInterface $plugin The plugin to publish assets from - * @param string $publicDir The target public directory - * @param bool $useSymlinks Whether to symlink or copy assets - * @param bool $force Whether to force overwrite existing files + * @param PluginInterface $plugin The plugin to publish assets from + * @param string $publicDir The target public directory + * @param bool $useSymlinks Whether to symlink or copy assets + * @param bool $force Whether to force overwrite existing files * * @return string[] Array of published file paths */ @@ -298,6 +737,9 @@ final class InstallCommand extends Command $originDir = $plugin->getAssetsDir(); if (!is_dir($originDir)) { + if ($this->debugMode) { + $this->debug("No assets directory found for {$plugin->getAlias()}: {$originDir}", 'notice'); + } return []; } @@ -305,131 +747,590 @@ final class InstallCommand extends Command $finder = new Finder(); $finder->files()->in($originDir); - $files = []; + if ($this->debugMode) { + $this->debug("Publishing assets for {$plugin->getAlias()}: " . $finder->count() . ' files', 'info'); + } + $files = []; + $totalSize = 0; + $filesCount = 0; + $maxFilesToShow = $this->debugMode ? 12 : 0; // Limit the number of files shown in debug mode + + // Group files by type for better visualization + $filesByType = [ + 'js' => [], + 'css' => [], + 'json' => [], + 'php' => [], + 'other' => [], + ]; + + // Process files with a mini progress animation for debug mode foreach ($finder as $file) { + $filesCount++; $relativePath = trim(str_replace($originDir, '', $file->getRealPath()), \DIRECTORY_SEPARATOR); - $targetPath = $publicDir.$relativePath; + $targetPath = $publicDir . $relativePath; + $fileSize = $file->getSize(); + $totalSize += $fileSize; + $extension = strtolower($file->getExtension()); $filesystem->makeDirectory(\dirname($targetPath), 0755, recursive: true, force: true); + // Track file types + if (isset($filesByType[$extension])) { + $filesByType[$extension][] = ['path' => $relativePath, 'size' => $fileSize]; + } else { + $filesByType['other'][] = ['path' => $relativePath, 'size' => $fileSize]; + } + if ($useSymlinks) { // For symlinks, we need to delete the existing file/link first if (file_exists($targetPath)) { $filesystem->delete($targetPath); } $filesystem->link($file->getRealPath(), $targetPath); + + if ($this->debugMode && $filesCount <= $maxFilesToShow) { + $icon = $this->asciiMode ? $this->asciiFileTypeIcons[$extension] ?? $this->asciiFileTypeIcons['default'] : $this->fileTypeIcons[$extension] ?? $this->fileTypeIcons['default']; + $this->debug(" {$icon} Symlinked: {$relativePath} ({$this->formatBytes($fileSize)})", 'dim'); + } } else { // For file copies, force flag is honored $filesystem->copy($file->getRealPath(), $targetPath, $force); + + if ($this->debugMode && $filesCount <= $maxFilesToShow) { + $icon = $this->asciiMode ? $this->asciiFileTypeIcons[$extension] ?? $this->asciiFileTypeIcons['default'] : $this->fileTypeIcons[$extension] ?? $this->fileTypeIcons['default']; + $this->debug(" {$icon} Copied: {$relativePath} ({$this->formatBytes($fileSize)})", 'dim'); + } + } + + // Create a subtle pulsing animation if in debug mode + if ($this->debugMode && !$this->noAnimation && $filesCount % 3 === 0) { + echo "\033[s"; // Save cursor position + echo "\033[u"; // Restore cursor position + usleep(5000); // Short delay for subtle animation } $files[] = $targetPath; } + if ($this->debugMode) { + if ($filesCount > $maxFilesToShow) { + $this->debug(' ... and ' . ($filesCount - $maxFilesToShow) . ' more files', 'dim'); + } + + if (count($files) > 0) { + $this->debug("Total size: {$this->formatBytes($totalSize)} in " . count($files) . ' files', 'success'); + } + + // Add file type breakdown for better visualization + foreach ($filesByType as $type => $typeFiles) { + if (count($typeFiles) > 0) { + $totalTypeSize = array_sum(array_column($typeFiles, 'size')); + $icon = $this->asciiMode ? + ($this->asciiFileTypeIcons[$type] ?? $this->asciiFileTypeIcons['default']) : + ($this->fileTypeIcons[$type] ?? $this->fileTypeIcons['default']); + $this->debug(" {$icon} {$type}: " . count($typeFiles) . " files ({$this->formatBytes($totalTypeSize)})", 'dim'); + } + } + } + return $files; } /** - * Publish a plugin's configuration file. + * Publish a plugin's configuration file with enhanced visual feedback. * - * @param PluginInterface $plugin The plugin to publish configuration for - * @param string $configFile The source configuration file path - * @param bool $force Whether to force override existing files + * @param PluginInterface $plugin The plugin to publish configuration for + * @param string $configFile The source configuration file path + * @param bool $force Whether to force override existing files * * @return bool Whether configuration was published */ private function publishConfig(PluginInterface $plugin, string $configFile, bool $force): bool { if (!file_exists($configFile)) { + if ($this->debugMode) { + $this->debug("Config file not found for {$plugin->getAlias()}: {$configFile}", 'notice'); + } return false; } - $target = App::configPath($plugin->getName().'.php'); + $target = App::configPath($plugin->getName() . '.php'); // Only skip if file exists AND force is false if (file_exists($target) && !$force) { + if ($this->debugMode) { + $this->debug("Config already exists for {$plugin->getAlias()}, skipping (use --force to override)", 'notice'); + } return false; } $filesystem = new Filesystem(); - $filesystem->copy($configFile, $target, true); // Always override when we reach this point + + // Add visual delay for better UX during config copy + if ($this->debugMode && !$this->noAnimation) { + $configIcon = $this->asciiMode ? '[CFG]' : '⚙️'; + $this->debug("{$configIcon} Preparing config for {$plugin->getAlias()}...", 'info'); + usleep(100000); // 100ms delay for visual effect + } + + $filesystem->copy($configFile, $target, $force); + + if ($this->debugMode) { + $configIcon = $this->asciiMode ? '[CFG]' : '⚙️'; + $this->debug("{$configIcon} Published config for {$plugin->getAlias()}: {$this->getRelativePath($target)}", 'success'); + } return true; } /** - * Display comprehensive installation summary with enhanced visuals. + * Create a symlink with better error handling. + */ + private function createSymlink(string $source, string $target): bool + { + // Make sure the target directory exists + $targetDir = dirname($target); + if (!is_dir($targetDir)) { + mkdir($targetDir, 0755, true); + } + + // Remove the target if it exists + if (file_exists($target)) { + @unlink($target); + } + + // Create the symlink + $success = false; + if (PHP_OS_FAMILY === 'Windows') { + // Windows needs special handling for symlinks + $isDir = is_dir($source); + $success = @symlink($source, $target); + } else { + $success = @symlink($source, $target); + } + + if (!$success) { + throw new \RuntimeException("Failed to create symlink from {$source} to {$target}"); + } + + return true; + } + + /** + * Display a comprehensive summary of the installation process with enhanced visuals. + * + * @param int $exitCode The exit code indicating success or failure */ private function displayComprehensiveSummary(int $exitCode): void { + $this->startTiming('summary'); $this->newLine(); - // Simplified header (without box characters) - $this->line(' INSTALLATION SUMMARY'); - $this->newLine(); + // Don't show table in minimal mode + if ($this->minimalMode) { + $totalTime = round((microtime(true) - $this->startTime) * 1000); + $successCount = $this->results->where('status', 'success')->count(); + $errorCount = $this->results->where('status', 'error')->count(); - // Results table with enhanced styling - $this->table( - ['Plugin', 'Status', 'Assets', 'Config', 'Message'], - $this->results->map(function ($result) { - $status = 'success' === $result['status'] - ? '✓ SUCCESS' - : '✗ ERROR'; + $this->info("Installation completed in {$totalTime}ms"); + $this->info("Processed {$this->results->count()} plugins ({$successCount} successful, {$errorCount} failed)"); - return [ - ''.$result['plugin'].'', - $status, - $result['assets'], - $result['config'], - 'error' === $result['status'] ? ''.$result['message'].'' : '', - ]; - })->toArray() - ); + // Show errors in minimal mode + if ($errorCount > 0) { + $this->newLine(); + $this->error('Failed plugins:'); + $this->results->where('status', 'error')->each(function ($result) { + $this->line(" - {$result['plugin']}: {$result['message']}"); + }); + } - // Calculate statistics - $successCount = $this->results->where('status', 'success')->count(); - $errorCount = $this->results->where('status', 'error')->count(); - $assetCount = $this->results->sum('assets'); - $duration = round(microtime(true) - $this->startTime, 2); - - // Simplified header for statistics section - $this->newLine(); - $this->line(' STATISTICS'); - $this->newLine(); - - // Enhanced statistics with more visual appeal - $this->components->twoColumnDetail('Plugins Processed', "{$this->results->count()}"); - $this->components->twoColumnDetail('Successful', "{$successCount}"); - $this->components->twoColumnDetail('Failed', $errorCount > 0 ? "{$errorCount}" : '0'); - $this->components->twoColumnDetail('Assets Published', "{$assetCount}"); - $this->components->twoColumnDetail('Duration', "{$duration} seconds"); - - $this->newLine(); - - // Final status message - Windows-compatible (no emojis) - if (self::SUCCESS === $exitCode) { - $this->newLine(); - $this->line(' '); - $this->line(' SUCCESSFUL! All PHPFlasher resources have been installed successfully! '); - $this->line(' '); $this->newLine(); + return; + } - // Show relative paths instead of absolute - $this->line(' > Assets Location: public/vendor/flasher/'); - $this->line(' > Config Location: config/'); - $this->line(' > Documentation: https://php-flasher.io'); - $this->newLine(); + // Enhanced with detailed results and animated success + if ($exitCode === self::SUCCESS) { + if ($this->asciiMode) { + $this->line(' [ INSTALLATION COMPLETED SUCCESSFULLY ]'); + } else { + // Animate the success message for extra wow effect + if (!$this->noAnimation) { + $chars = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']; + for ($i = 0; $i < 5; $i++) { // 5 animation cycles + echo "\033[s"; // Save cursor position + $this->line(' [ INSTALLATION ' . $chars[$i % count($chars)] . ' ]'); + usleep(100000); // 100ms delay + echo "\033[u"; // Restore cursor position + } + } + // Rainbow gradient for success message + $this->line(' [ INSTALLATION COMPLETED SUCCESSFULLY ]'); - // Signature line with updated time and username - $this->line(' PHPFlasher 2025 - Installation completed'); - $this->newLine(); + // Show a random success message for fun + $randomMessage = $this->successMessages[array_rand($this->successMessages)]; + $this->newLine(); + $this->line(' ' . $randomMessage . ''); + } } else { - $this->newLine(); - $this->line(' '); - $this->line(' WARNING! Some errors occurred during the installation. Check the summary above. '); - $this->line(' '); - $this->newLine(); + $this->line(' [ INSTALLATION COMPLETED WITH ERRORS ]'); + } + $this->newLine(); + + // Create a fancy box for the results table if not in ASCII mode + if (!$this->asciiMode) { + $tableWidth = 70; + $this->line(' ╭' . str_repeat('─', $tableWidth) . '╮'); + $this->line(' INSTALLATION RESULTS' . str_repeat(' ', $tableWidth - 23) . ''); + $this->line(' ├' . str_repeat('─', $tableWidth) . '┤'); + } + + // Detailed results table with color-coded status and animated icons + $headers = ['Plugin', 'Status', 'Assets', 'Config', 'Time (ms)']; + $rows = $this->results->map(function ($result) { + $checkmark = $this->asciiMode ? '√' : '✓'; + $xmark = $this->asciiMode ? 'x' : '✗'; + + $status = $result['status'] === 'success' + ? '' . $checkmark . ' Success' + : '' . $xmark . ' Failed'; + + return [ + '' . $result['plugin'] . '', + $status, + $result['assets'], + $result['config'], + $result['time'], + ]; + })->toArray(); + + $this->table($headers, $rows); + + // Close the box if not in ASCII mode + if (!$this->asciiMode) { + $this->line(' ╰' . str_repeat('─', $tableWidth) . '╯'); + } + + // Statistics section with animated counting if not in no-animation mode + $totalTime = round((microtime(true) - $this->startTime) * 1000); + $successCount = $this->results->where('status', 'success')->count(); + $failureCount = $this->results->where('status', 'error')->count(); + $totalAssets = $this->results->sum('assets'); + + if (!$this->asciiMode) { + $this->line(' ╭' . str_repeat('─', $tableWidth) . '╮'); + $this->line(' INSTALLATION STATISTICS' . str_repeat(' ', $tableWidth - 25) . ''); + $this->line(' ╰' . str_repeat('─', $tableWidth) . '╯'); + } else { + $this->line(' [ INSTALLATION STATISTICS ]'); + } + + $this->newLine(); + + // Animated statistics counter for extra wow effect + if (!$this->noAnimation && !$this->minimalMode) { + // Animate total plugins count + echo ' Total Plugins .................................................................. '; + for ($i = 0; $i <= $this->results->count(); $i++) { + echo "\033[s"; // Save cursor position + echo "{$i}"; + if ($i < $this->results->count()) { + usleep(50000); // 50ms delay between increments + echo "\033[u"; // Restore cursor position + } + } + echo PHP_EOL; + + // Animate successful count + echo ' Successful ................................................................... '; + for ($i = 0; $i <= $successCount; $i++) { + echo "\033[s"; // Save cursor position + echo "{$i}"; + if ($i < $successCount) { + usleep(50000); // 50ms delay between increments + echo "\033[u"; // Restore cursor position + } + } + echo PHP_EOL; + + // Other stats without animation + $this->components->twoColumnDetail('Failed', "{$failureCount}"); + $this->components->twoColumnDetail('Assets Published', "{$totalAssets}"); + $this->components->twoColumnDetail('Total Time', "{$totalTime}ms"); + } else { + // Static display without animation + $this->components->twoColumnDetail('Total Plugins', "{$this->results->count()}"); + $this->components->twoColumnDetail('Successful', "{$successCount}"); + $this->components->twoColumnDetail('Failed', "{$failureCount}"); + $this->components->twoColumnDetail('Assets Published', "{$totalAssets}"); + $this->components->twoColumnDetail('Total Time', "{$totalTime}ms"); + } + + // Next steps with animated typing effect + $this->newLine(); + if (!$this->asciiMode) { + $this->line(' ╭' . str_repeat('─', $tableWidth) . '╮'); + $this->line(' NEXT STEPS' . str_repeat(' ', $tableWidth - 13) . ''); + $this->line(' ╰' . str_repeat('─', $tableWidth) . '╯'); + } else { + $this->line(' [ NEXT STEPS ]'); + } + $this->newLine(); + + // Animate typing effect for next steps + $nextSteps = [ + '• Include PHPFlasher in your layouts using the Blade directive: @flasher_render', + '• For SPA/API usage, include the following in your response: flasher()->render()', + '• Documentation: https://php-flasher.io', + ]; + + if (!$this->noAnimation && !$this->minimalMode) { + foreach ($nextSteps as $step) { + $plainStep = strip_tags($step); + echo ' '; + + // Typing animation for instruction - fixed to avoid empty string error + for ($i = 1; $i <= strlen($plainStep); $i++) { + // Get the current character to output + $currentChar = $plainStep[$i-1]; + echo $currentChar; + usleep(10000); // 10ms delay for character typing + } + echo PHP_EOL; + } + } else { + foreach ($nextSteps as $step) { + $this->line(' ' . $step); + } + } + + $this->newLine(); + + $this->stopTiming('summary'); + } + + /** + * Display performance metrics in debug mode with enhanced visuals. + */ + private function displayPerformanceMetrics(): void + { + // Skip if not in debug mode + if (!$this->debugMode) { + return; + } + + $this->newLine(); + if (!$this->asciiMode) { + $tableWidth = 70; + $this->line(' ╭' . str_repeat('─', $tableWidth) . '╮'); + $this->line(' PERFORMANCE METRICS' . str_repeat(' ', $tableWidth - 22) . ''); + $this->line(' ╰' . str_repeat('─', $tableWidth) . '╯'); + } else { + $this->line(' [ PERFORMANCE METRICS ]'); + } + $this->newLine(); + + // Sort timings by duration (descending) + $timings = []; + foreach ($this->metrics as $name => $timing) { + if (isset($timing['duration'])) { + $timings[$name] = $timing['duration']; + } + } + + arsort($timings); + + // Create a formatted table + $headers = ['Operation', 'Duration (ms)', 'Percentage']; + $rows = []; + $totalTime = $this->metrics['total']['duration'] ?? 1; // Prevent division by zero + + foreach ($timings as $name => $duration) { + if ($name === 'total') { + continue; // Skip total, will show it separately + } + + $percent = round(($duration / $totalTime) * 100, 1); + $percentDisplay = $this->getColoredPercentage($percent); + + // Format operation name nicely + $operation = str_replace(['_', 'task_'], [' ', ''], $name); + $operation = ucwords($operation); + + $rows[] = [ + $operation, + $duration, + $percentDisplay, + ]; + } + + $this->table($headers, $rows); + + // Show total time separately with visual bar + $this->newLine(); + $totalDuration = $this->metrics['total']['duration'] ?? round((microtime(true) - $this->startTime) * 1000); + + if (!$this->noAnimation && !$this->asciiMode) { + // Animated timer bar + $this->line(' • Total execution time: '); + $barWidth = min(50, $this->terminalDimensions['width'] - 30); + echo ' ['; + for ($i = 0; $i < $barWidth; $i++) { + echo "\033[s"; // Save cursor position + echo "\033[32m"; // Green color + for ($j = 0; $j <= $i; $j++) { + echo '■'; + } + echo "\033[0m"; // Reset color + echo str_repeat(' ', $barWidth - $i - 1); + echo '] ' . round(($i + 1) / $barWidth * $totalDuration) . 'ms'; + if ($i < $barWidth - 1) { + usleep(1000000 / $barWidth); // Distribute over 1 second + echo "\033[u"; // Restore cursor position + } + } + echo PHP_EOL; + } else { + $this->line(" • Total execution time: {$totalDuration}ms"); + } + + $this->newLine(); + } + + /** + * Format a file size in bytes to human-readable format with enhanced styling. + */ + private function formatBytes(int $bytes, int $precision = 2): string + { + if ($bytes <= 0) { + return '0 B'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $pow = floor(log($bytes, 1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= pow(1024, $pow); + + // Color-code based on size + $color = 'green'; + if ($pow >= 3) { // GB or larger + $color = 'red'; + } elseif ($pow >= 2) { // MB + $color = 'yellow'; + } elseif ($pow >= 1) { // KB + $color = 'blue'; + } + + return "" . round($bytes, $precision) . ' ' . $units[$pow] . ''; + } + + /** + * Start timing an operation. + */ + private function startTiming(string $name): void + { + if (!$this->debugMode) { + return; + } + + $this->metrics[$name] = [ + 'start' => microtime(true), + ]; + } + + /** + * Stop timing an operation and record its duration. + */ + private function stopTiming(string $name): void + { + if (!$this->debugMode || !isset($this->metrics[$name]['start'])) { + return; + } + + $this->metrics[$name]['end'] = microtime(true); + $this->metrics[$name]['duration'] = round(($this->metrics[$name]['end'] - $this->metrics[$name]['start']) * 1000); + } + + /** + * Get elapsed time for an operation in milliseconds. + */ + private function getElapsedTime(string $name): int + { + if (!isset($this->metrics[$name]['duration'])) { + return 0; + } + + return $this->metrics[$name]['duration']; + } + + /** + * Output a debug message. + */ + private function debug(string $message, string $level = 'info'): void + { + if (!$this->debugMode) { + return; + } + + $this->debugLineCount++; + $timestamp = '[' . sprintf('%.3f', (microtime(true) - $this->startTime) * 1000) . 'ms]'; + $this->line(" {$timestamp} <{$level}>{$message}"); + } + + /** + * Start a debug group. + */ + private function debugGroupStart(string $name): void + { + if (!$this->debugMode) { + return; + } + + $this->newLine(); + $this->line(" ▼ {$name}"); + } + + /** + * End a debug group. + */ + private function debugGroupEnd(): void + { + if (!$this->debugMode) { + return; + } + + $this->newLine(); + } + + /** + * Get a color-coded percentage string based on the value. + */ + private function getColoredPercentage(float $percent): string + { + if ($percent > 30) { + return "{$percent}%"; + } elseif ($percent > 10) { + return "{$percent}%"; + } else { + return "{$percent}%"; } } + + /** + * Get relative path from the application base path. + */ + private function getRelativePath(string $path): string + { + return Str::replaceFirst($this->getBasePath(), '', $path); + } + + /** + * Get application base path. + */ + private function getBasePath(): string + { + return App::basePath(); + } }