diff --git a/composer.json b/composer.json index 9bc42d31c3..ca6e324fb6 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,9 @@ "composer/composer": "^2.2.22", "guzzlehttp/guzzle": "^6.3 || ^7.0", "james-heinrich/getid3": "^1.9.21", - "laravel/framework": "^10.0 || ^11.0", + "laravel/framework": "^10.25.0 || ^11.0", "laravel/helpers": "^1.1", + "laravel/prompts": "^0.1.16", "league/commonmark": "^2.2", "league/csv": "^9.0", "league/glide": "^2.0", diff --git a/src/Console/Commands/AddonsDiscover.php b/src/Console/Commands/AddonsDiscover.php index 7741714cf8..27bed61166 100644 --- a/src/Console/Commands/AddonsDiscover.php +++ b/src/Console/Commands/AddonsDiscover.php @@ -31,12 +31,13 @@ class AddonsDiscover extends Command */ public function handle(Manifest $manifest) { + $this->newLine(); $manifest->build(); foreach (array_keys($manifest->manifest) as $package) { - $this->line("Discovered Addon: {$package}"); + $this->components->task("Discovered Addon: {$package}"); } - $this->info('Addon manifest generated successfully.'); + $this->components->info('Addon manifest generated successfully.'); } } diff --git a/src/Console/Commands/AssetsGeneratePresets.php b/src/Console/Commands/AssetsGeneratePresets.php index edbc112a0a..aba0a593f9 100644 --- a/src/Console/Commands/AssetsGeneratePresets.php +++ b/src/Console/Commands/AssetsGeneratePresets.php @@ -10,6 +10,11 @@ use Statamic\Jobs\GeneratePresetImageManipulation; use Statamic\Support\Arr; +use function Laravel\Prompts\error; +use function Laravel\Prompts\info; +use function Laravel\Prompts\note; +use function Laravel\Prompts\progress; + class AssetsGeneratePresets extends Command { use RunsInPlease; @@ -46,12 +51,12 @@ public function handle() $this->shouldQueue = $this->option('queue'); if ($this->shouldQueue && config('queue.default') === 'sync') { - $this->error('The queue connection is set to "sync". Queueing will be disabled.'); + error('The queue connection is set to "sync". Queueing will be disabled.'); $this->shouldQueue = false; } AssetContainer::all()->sortBy('title')->each(function ($container) { - $this->line('Generating presets for '.$container->title().'...'); + note('Generating presets for '.$container->title().'...'); $this->generatePresets($container); $this->newLine(); }); @@ -73,37 +78,40 @@ private function generatePresets($container) $cpPresets = config('statamic.cp.enabled') ? 1 : 0; $steps = (count($container->warmPresets()) + $cpPresets) * count($assets); - $bar = $this->output->createProgressBar($steps); - - foreach ($assets as $asset) { - $verb = $this->shouldQueue ? 'Queueing' : 'Generating'; - $bar->setFormat("[%current%/%max%] $verb %filename% %preset%... "); - - foreach ($asset->warmPresets() as $preset) { - $counts[$preset] = ($counts[$preset] ?? 0) + 1; - $bar->setMessage($preset, 'preset'); - $bar->setMessage($asset->basename(), 'filename'); - $bar->display(); - - $dispatchMethod = $this->shouldQueue - ? 'dispatch' - : (method_exists(Dispatcher::class, 'dispatchSync') ? 'dispatchSync' : 'dispatchNow'); - - try { - GeneratePresetImageManipulation::$dispatchMethod($asset, $preset); - } catch (\Exception $e) { - Log::debug($e); - $counts['errors'] = ($counts['errors'] ?? 0) + 1; - } - $bar->advance(); + if ($steps > 0) { + $progress = progress( + label: $this->shouldQueue ? 'Queueing...' : 'Generating...', + steps: $steps + ); + + $progress->start(); + + foreach ($assets as $asset) { + foreach ($asset->warmPresets() as $preset) { + $counts[$preset] = ($counts[$preset] ?? 0) + 1; + $progress->label("Generating $preset for {$asset->basename()}..."); + + $dispatchMethod = $this->shouldQueue + ? 'dispatch' + : (method_exists(Dispatcher::class, 'dispatchSync') ? 'dispatchSync' : 'dispatchNow'); + + try { + GeneratePresetImageManipulation::$dispatchMethod($asset, $preset); + } catch (\Exception $e) { + Log::debug($e); + $counts['errors'] = ($counts['errors'] ?? 0) + 1; + } + + $progress->advance(); + } } + + $progress->finish(); } $verb = $this->shouldQueue ? 'queued' : 'generated'; - $bar->setFormat(sprintf("[✔] %s images $verb for %s assets.", $steps, count($assets))); - $bar->finish(); - $this->newLine(2); + info(sprintf("[✔] %s images $verb for %s assets.", $steps, count($assets))); if (property_exists($this, 'components')) { $errors = Arr::pull($counts, 'errors'); diff --git a/src/Console/Commands/AssetsMeta.php b/src/Console/Commands/AssetsMeta.php index 69d6acbd16..8e990ed5c6 100644 --- a/src/Console/Commands/AssetsMeta.php +++ b/src/Console/Commands/AssetsMeta.php @@ -7,6 +7,8 @@ use Statamic\Facades\Asset; use Statamic\Facades\AssetContainer; +use function Laravel\Prompts\progress; + class AssetsMeta extends Command { use RunsInPlease; @@ -19,18 +21,22 @@ public function handle() { $assets = $this->getAssets(); - $bar = $this->output->createProgressBar($assets->count()); - - $assets->each(function ($asset) use ($bar) { - $asset->hydrate(); - $asset->save(); - $bar->advance(); - }); - - $bar->finish(); + if ($assets->isEmpty()) { + return $this->components->warn("There's no metadata to generate. You don't have any assets."); + } - $this->line(''); - $this->info('Asset metadata generated'); + progress( + label: 'Generating asset metadata...', + steps: $assets, + callback: function ($asset, $progress) { + $asset->hydrate(); + $asset->save(); + $progress->advance(); + }, + hint: 'This may take a while if you have a lot of assets.' + ); + + $this->components->info("Generated metadata for {$assets->count()} ".str_plural('asset', $assets->count()).'.'); } /** diff --git a/src/Console/Commands/AuthMigration.php b/src/Console/Commands/AuthMigration.php index 75981821ea..aaf148fd85 100644 --- a/src/Console/Commands/AuthMigration.php +++ b/src/Console/Commands/AuthMigration.php @@ -43,7 +43,7 @@ public function handle() File::put($to, $contents); - $this->line("Created Auth Migration: {$file}"); + $this->components->info("Migration [$file] created successfully."); $this->createGroupsTable(); $this->createRolesTable(); @@ -67,7 +67,7 @@ private function createGroupsTable() File::put($to, $contents); - $this->line("Created Groups Migration: {$file}"); + $this->components->info("Migration [$file] created successfully."); } private function createRolesTable() @@ -86,6 +86,6 @@ private function createRolesTable() File::put($to, $contents); - $this->line("Created Roles Migration: {$file}"); + $this->components->info("Migration [$file] created successfully."); } } diff --git a/src/Console/Commands/GeneratorCommand.php b/src/Console/Commands/GeneratorCommand.php index 9de87901c9..c497d6ed8f 100644 --- a/src/Console/Commands/GeneratorCommand.php +++ b/src/Console/Commands/GeneratorCommand.php @@ -28,15 +28,7 @@ public function handle() : $addon; } - if (parent::handle() === false) { - return false; - } - - $relativePath = $this->getRelativePath($this->getPath($this->qualifyClass($this->getNameInput()))); - - if (! $addon) { - $this->line("Your {$this->typeLower} class awaits: {$relativePath}"); - } + return parent::handle(); } /** diff --git a/src/Console/Commands/ImportGroups.php b/src/Console/Commands/ImportGroups.php index 0d8a522f84..a03243a890 100644 --- a/src/Console/Commands/ImportGroups.php +++ b/src/Console/Commands/ImportGroups.php @@ -13,6 +13,9 @@ use Statamic\Contracts\Auth\UserGroupRepository as GroupRepositoryContract; use Statamic\Facades\UserGroup; +use function Laravel\Prompts\error; +use function Laravel\Prompts\progress; + class ImportGroups extends Command { use RunsInPlease; @@ -39,7 +42,7 @@ class ImportGroups extends Command public function handle() { if (! config('statamic.users.tables.groups', false)) { - $this->error('You do not have eloquent driven groups enabled'); + error('You do not have eloquent driven groups enabled'); return; } @@ -65,17 +68,20 @@ private function importGroups() Facade::clearResolvedInstance(GroupContract::class); Facade::clearResolvedInstance(GroupRepositoryContract::class); - $this->withProgressBar($groups, function ($group) { - $eloquentGroup = UserGroup::make() - ->handle($group->handle()) - ->title($group->title()) - ->roles($group->roles()) - ->data($group->data()->except(['title', 'roles'])); + progress( + label: 'Importing groups...', + steps: $groups, + callback: function ($group, $progress) { + $eloquentGroup = UserGroup::make() + ->handle($group->handle()) + ->title($group->title()) + ->roles($group->roles()) + ->data($group->data()->except(['title', 'roles'])); - $eloquentGroup->save(); - }); + $eloquentGroup->save(); + } + ); - $this->newLine(); $this->info('Groups imported'); } } diff --git a/src/Console/Commands/ImportRoles.php b/src/Console/Commands/ImportRoles.php index 49abf026fc..5e726b98d6 100644 --- a/src/Console/Commands/ImportRoles.php +++ b/src/Console/Commands/ImportRoles.php @@ -13,6 +13,9 @@ use Statamic\Contracts\Auth\RoleRepository as RoleRepositoryContract; use Statamic\Facades\Role; +use function Laravel\Prompts\error; +use function Laravel\Prompts\progress; + class ImportRoles extends Command { use RunsInPlease; @@ -39,7 +42,7 @@ class ImportRoles extends Command public function handle() { if (! config('statamic.users.tables.roles', false)) { - $this->error('You do not have eloquent driven roles enabled'); + error('You do not have eloquent driven roles enabled'); return; } @@ -65,16 +68,19 @@ private function importRoles() Facade::clearResolvedInstance(RoleContract::class); Facade::clearResolvedInstance(RoleRepositoryContract::class); - $this->withProgressBar($roles, function ($role) { - $eloquentRole = Role::make($role->handle()) - ->title($role->title()) - ->permissions($role->permissions()) - ->preferences($role->preferences()); + progress( + label: 'Importing roles...', + steps: $roles, + callback: function ($role) { + $eloquentRole = Role::make($role->handle()) + ->title($role->title()) + ->permissions($role->permissions()) + ->preferences($role->preferences()); - $eloquentRole->save(); - }); + $eloquentRole->save(); + } + ); - $this->newLine(); $this->info('Roles imported'); } } diff --git a/src/Console/Commands/ImportUsers.php b/src/Console/Commands/ImportUsers.php index de0acacea7..854ab786f4 100644 --- a/src/Console/Commands/ImportUsers.php +++ b/src/Console/Commands/ImportUsers.php @@ -15,6 +15,10 @@ use Statamic\Stache\Repositories\UserRepository as FileRepository; use Statamic\Stache\Stores\UsersStore; +use function Laravel\Prompts\error; +use function Laravel\Prompts\info; +use function Laravel\Prompts\progress; + class ImportUsers extends Command { use RunsInPlease; @@ -41,7 +45,7 @@ class ImportUsers extends Command public function handle() { if (config('statamic.users.repository') !== 'eloquent') { - $this->error('Your site is not using the eloquent user repository.'); + error('Your site is not using the eloquent user repository.'); return 0; } @@ -58,7 +62,7 @@ private function importUsers() $model = config("auth.providers.$provider.model"); if (! in_array(HasUuids::class, class_uses_recursive($model))) { - $this->error('Your user model must use the HasUuids trait for this migration to run'); + error('Please add the HasUuids trait to your '.$model.' model in order to run this importer.'); return; } @@ -75,34 +79,37 @@ private function importUsers() $eloquentRepository = app(UserRepositoryManager::class)->createEloquentDriver([]); - $this->withProgressBar($users, function ($user) use ($eloquentRepository) { - $data = $user->data(); + progress( + label: 'Importing users...', + steps: $users, + callback: function ($user, $progress) use ($eloquentRepository) { + $data = $user->data(); - $eloquentUser = $eloquentRepository->make() - ->email($user->email()) - ->preferences($user->preferences()) - ->data($data->except(['groups', 'roles'])->merge(['name' => $user->name()])) - ->id($user->id()); + $eloquentUser = $eloquentRepository->make() + ->email($user->email()) + ->preferences($user->preferences()) + ->data($data->except(['groups', 'roles'])->merge(['name' => $user->name()])) + ->id($user->id()); - if ($user->isSuper()) { - $eloquentUser->makeSuper(); - } + if ($user->isSuper()) { + $eloquentUser->makeSuper(); + } - if (count($data->get('groups', [])) > 0) { - $eloquentUser->groups($data->get('groups')); - } + if (count($data->get('groups', [])) > 0) { + $eloquentUser->groups($data->get('groups')); + } - if (count($data->get('roles', [])) > 0) { - $eloquentUser->roles($data->get('roles')); - } + if (count($data->get('roles', [])) > 0) { + $eloquentUser->roles($data->get('roles')); + } - $eloquentUser->saveToDatabase(); + $eloquentUser->saveToDatabase(); - $eloquentUser->model()->forceFill(['password' => $user->password()]); - $eloquentUser->model()->saveQuietly(); - }); + $eloquentUser->model()->forceFill(['password' => $user->password()]); + $eloquentUser->model()->saveQuietly(); + } + ); - $this->newLine(); - $this->info('Users imported'); + info('Users imported'); } } diff --git a/src/Console/Commands/InstallSsg.php b/src/Console/Commands/InstallSsg.php index 57478851f4..0f8455dafa 100644 --- a/src/Console/Commands/InstallSsg.php +++ b/src/Console/Commands/InstallSsg.php @@ -9,6 +9,10 @@ use Statamic\Console\RunsInPlease; use Symfony\Component\Process\PhpExecutableFinder; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\error; +use function Laravel\Prompts\spin; + class InstallSsg extends Command { use EnhancesCommands, RunsInPlease; @@ -35,30 +39,42 @@ class InstallSsg extends Command public function handle() { if (Composer::isInstalled('statamic/ssg')) { - return $this->error('The Static Site Generator package is already installed.'); + return error('The Static Site Generator package is already installed.'); } - $this->info('Installing the statamic/ssg package...'); - Composer::withoutQueue()->throwOnFailure()->require('statamic/ssg'); + spin( + fn () => Composer::withoutQueue()->throwOnFailure()->require('statamic/ssg'), + 'Installing the statamic/ssg package...' + ); + $this->checkLine('Installed statamic/ssg package'); - if ($this->confirm('Would you like to publish the config file?')) { - Process::run([ - (new PhpExecutableFinder())->find(false) ?: 'php', - defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', - 'vendor:publish', - '--provider', - 'Statamic\\StaticSite\\ServiceProvider', - ]); + if (confirm('Would you like to publish the config file?')) { + spin( + function () { + Process::run([ + (new PhpExecutableFinder())->find(false) ?: 'php', + defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', + 'vendor:publish', + '--provider', + 'Statamic\\StaticSite\\ServiceProvider', + ]); + }, + message: 'Publishing the config file...' + ); $this->checkLine('Config file published. You can find it at config/statamic/ssg.php'); } if ( ! Composer::isInstalled('spatie/fork') - && $this->confirm('Would you like to install spatie/fork? It allows for running multiple workers at once.') + && confirm('Would you like to install spatie/fork? It allows for running multiple workers at once.') ) { - Composer::withoutQueue()->throwOnFailure()->require('spatie/fork'); + spin( + fn () => Composer::withoutQueue()->throwOnFailure()->require('spatie/fork'), + 'Installing the spatie/fork package...' + ); + $this->checkLine('Installed spatie/fork package'); } } diff --git a/src/Console/Commands/MakeAddon.php b/src/Console/Commands/MakeAddon.php index bada10e715..71ca01628a 100644 --- a/src/Console/Commands/MakeAddon.php +++ b/src/Console/Commands/MakeAddon.php @@ -11,6 +11,8 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; +use function Laravel\Prompts\spin; + class MakeAddon extends GeneratorCommand { use EnhancesCommands, RunsInPlease, ValidatesInput; @@ -83,16 +85,22 @@ public function handle() ->generateOptional() ->installComposerDependencies(); } catch (\Exception $e) { - $this->error($e->getMessage()); + $this->components->error($e->getMessage()); return 1; } $relativePath = $this->getRelativePath($this->addonPath()); - $this->output->newLine(); - $this->info("🎉 Your addon package is ready: {$relativePath}"); - $this->line('Learn how to build addons in our docs: https://statamic.dev/extending/addons'); + $this->components->info('Your addon is ready! 🎉'); + + $this->components->bulletList([ + "You find your addon in {$relativePath}", + 'Learn how to build addons in our docs: https://statamic.dev/extending/addons', + "When you're ready, setup as a seller to publish your addon on the Marketplace: https://statamic.com/sell", + ]); + + $this->newLine(); } /** @@ -132,30 +140,31 @@ protected function generateComposerJson() */ protected function generateAddonFiles() { - $this->line('Creating addon...'); - - $this->generateComposerJson(); - - $files = [ - 'addon/provider.php.stub' => 'src/ServiceProvider.php', - 'addon/TestCase.php.stub' => 'tests/TestCase.php', - 'addon/ExampleTest.php.stub' => 'tests/ExampleTest.php', - 'addon/.gitignore.stub' => '.gitignore', - 'addon/README.md.stub' => 'README.md', - 'addon/phpunit.xml.stub' => 'phpunit.xml', - ]; - - $data = [ - 'name' => $this->addonTitle(), - 'package' => $this->package, - 'namespace' => $this->addonNamespace(), - ]; - - foreach ($files as $stub => $file) { - $this->createFromStub($stub, $this->addonPath($file), $data); - } - - $this->checkInfo('Addon boilerplate created successfully.'); + spin( + function () { + $this->generateComposerJson(); + + $files = [ + 'addon/provider.php.stub' => 'src/ServiceProvider.php', + 'addon/TestCase.php.stub' => 'tests/TestCase.php', + 'addon/ExampleTest.php.stub' => 'tests/ExampleTest.php', + 'addon/.gitignore.stub' => '.gitignore', + 'addon/README.md.stub' => 'README.md', + 'addon/phpunit.xml.stub' => 'phpunit.xml', + ]; + + $data = [ + 'name' => $this->addonTitle(), + 'package' => $this->package, + 'namespace' => $this->addonNamespace(), + ]; + + foreach ($files as $stub => $file) { + $this->createFromStub($stub, $this->addonPath($file), $data); + } + }, + 'Creating addon...' + ); return $this; } @@ -195,19 +204,17 @@ protected function generateOptional() */ protected function installComposerDependencies() { - $this->output->newLine(); - - $this->line("Installing your addon's Composer dependencies. This may take a moment..."); - - try { - Composer::withoutQueue()->throwOnFailure()->install($this->addonPath()); - } catch (ProcessException $exception) { - $this->line($exception->getMessage()); - $this->output->newLine(); - throw new \Exception("An error was encountered while installing your addon's Composer dependencies!"); - } - - $this->checkInfo('Composer dependencies installed successfully.'); + spin( + function () { + try { + Composer::withoutQueue()->throwOnFailure()->install($this->addonPath()); + } catch (ProcessException $exception) { + $this->line($exception->getMessage()); + throw new \Exception("An error was encountered while installing your addon's Composer dependencies."); + } + }, + "Installing your addon's Composer dependencies..." + ); return $this; } @@ -242,19 +249,20 @@ protected function addRepositoryPath() */ protected function installAddon() { - $this->output->newLine(); - $this->line('Installing your addon with Composer. This may take a moment...'); - $this->addRepositoryPath(); - - try { - Composer::withoutQueue()->throwOnFailure()->require($this->package); - } catch (ProcessException $exception) { - $this->line($exception->getMessage()); - $this->output->newLine(); - throw new \Exception('An error was encountered while installing your addon!'); - } - - $this->checkInfo('Addon installed successfully.'); + spin( + function () { + $this->addRepositoryPath(); + + try { + Composer::withoutQueue()->throwOnFailure()->require($this->package); + } catch (ProcessException $exception) { + $this->newLine(); + $this->line($exception->getMessage()); + throw new \Exception('An error was encountered while installing your addon.'); + } + }, + 'Installing your addon with Composer...' + ); return $this; } diff --git a/src/Console/Commands/MakeFieldtype.php b/src/Console/Commands/MakeFieldtype.php index 64dd08c02e..c3af3944cc 100644 --- a/src/Console/Commands/MakeFieldtype.php +++ b/src/Console/Commands/MakeFieldtype.php @@ -75,8 +75,14 @@ protected function generateVueComponent() if ($addon = $this->argument('addon')) { $this->wireUpAddonJs($addon); } else { - $this->line("Your {$this->typeLower} Vue component awaits: {$relativePath}"); - $this->comment("Don't forget to import and register your Fieldtype component in resources/js/addon.js"); + $this->components->info("Fieldtype Vue component [{$relativePath}] created successfully."); + + $this->components->bulletList([ + "Don't forget to import and register your fieldtype's Vue component in resources/js/addon.js", + 'For more information, see the documentation: https://statamic.dev/fieldtypes#vue-components', + ]); + + $this->newLine(); } } diff --git a/src/Console/Commands/MakeUser.php b/src/Console/Commands/MakeUser.php index 59d84f20ac..87adde9466 100644 --- a/src/Console/Commands/MakeUser.php +++ b/src/Console/Commands/MakeUser.php @@ -12,6 +12,11 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\error; +use function Laravel\Prompts\password; +use function Laravel\Prompts\text; + class MakeUser extends Command { use RunsInPlease, ValidatesInput; @@ -59,7 +64,7 @@ class MakeUser extends Command public function handle() { if (! Statamic::pro() && User::query()->count() > 0) { - return $this->error(__('Statamic Pro is required.')); + return error(__('Statamic Pro is required.')); } // If email argument exists, non-interactively create user. @@ -83,7 +88,7 @@ public function handle() */ protected function promptEmail() { - $this->email = $this->ask('Email'); + $this->email = text(label: 'Email', required: true); if ($this->emailValidationFails()) { return $this->promptEmail(); @@ -103,7 +108,7 @@ protected function promptName() return $this->promptSeparateNameFields(); } - $this->data['name'] = $this->ask('Name', false); + $this->data['name'] = text(label: 'Name'); return $this; } @@ -115,8 +120,8 @@ protected function promptName() */ protected function promptSeparateNameFields() { - $this->data['first_name'] = $this->ask('First Name', false); - $this->data['last_name'] = $this->ask('Last Name', false); + $this->data['first_name'] = text(label: 'First Name'); + $this->data['last_name'] = text(label: 'Last Name'); return $this; } @@ -128,7 +133,7 @@ protected function promptSeparateNameFields() */ protected function promptPassword() { - $this->data['password'] = $this->secret('Password (Your input will be hidden)'); + $this->data['password'] = password(label: 'Password', required: true); if ($this->passwordValidationFails()) { return $this->promptPassword(); @@ -148,7 +153,7 @@ protected function promptSuper() return $this; } - if ($this->confirm('Super user', false)) { + if (confirm('Super user?', false)) { $this->super = true; } @@ -175,7 +180,7 @@ protected function createUser() $user->save(); - $this->info('User created successfully.'); + $this->components->info('User created successfully.'); } /** diff --git a/src/Console/Commands/Multisite.php b/src/Console/Commands/Multisite.php index 69b1414c42..a54e351bec 100644 --- a/src/Console/Commands/Multisite.php +++ b/src/Console/Commands/Multisite.php @@ -17,6 +17,10 @@ use Statamic\Statamic; use Symfony\Component\VarExporter\VarExporter; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\info; +use function Laravel\Prompts\text; + class Multisite extends Command { use EnhancesCommands, RunsInPlease; @@ -39,23 +43,24 @@ public function handle() $this->validateRunningOfCommand(); - $confirmed = $this->confirm("The current site handle is [{$this->siteOne()}], content will be moved into folders with this name. Is this okay?"); + $confirmed = confirm("The current site handle is [{$this->siteOne()}], content will be moved into folders with this name. Is this okay?"); if (! $confirmed) { - $this->crossLine('Change the site handle in config/statamic/sites.php then try this command again.'); + $this->components->error('Change the site handle in config/statamic/sites.php then try this command again.'); return; } - $this->info("Please enter the handles of the additional sites. Just press enter when you're done."); + info("Please enter the handles of the additional sites. Just press enter when you're done."); + do { - if ($site = $this->ask('Handle of site #'.($this->sites->count() + 1))) { + if ($site = text('Handle of site #'.($this->sites->count() + 1))) { $this->sites->add($site); } - } while ($site !== null); + } while ($site !== ''); if ($this->sites->count() < 2) { - return $this->crossLine('Multisite has not been enabled.'); + return $this->components->error('Multisite has not been enabled.'); } $this->clearStache(); diff --git a/src/Console/Commands/SiteClear.php b/src/Console/Commands/SiteClear.php index 6ef57827d4..a4156378f4 100644 --- a/src/Console/Commands/SiteClear.php +++ b/src/Console/Commands/SiteClear.php @@ -7,6 +7,8 @@ use Statamic\Console\RunsInPlease; use Statamic\Facades\YAML; +use function Laravel\Prompts\confirm; + class SiteClear extends Command { use RunsInPlease; @@ -74,7 +76,7 @@ protected function shouldAbort() return false; } - return ! $this->confirm('Are you sure you want to remove all the site content and resources?', false); + return ! confirm(label: 'Are you sure you want to remove all the site content and resources?', default: false); } /** diff --git a/src/Console/Commands/StacheClear.php b/src/Console/Commands/StacheClear.php index 25e7d4505e..038ba23bee 100644 --- a/src/Console/Commands/StacheClear.php +++ b/src/Console/Commands/StacheClear.php @@ -33,6 +33,6 @@ public function handle() { Stache::clear(); - $this->info('You have trimmed the Stache. It looks dashing.'); + $this->components->info('You have trimmed the Stache. It looks dashing.'); } } diff --git a/src/Console/Commands/StacheRefresh.php b/src/Console/Commands/StacheRefresh.php index e103b8c2e8..aaa98bebd7 100644 --- a/src/Console/Commands/StacheRefresh.php +++ b/src/Console/Commands/StacheRefresh.php @@ -5,7 +5,8 @@ use Illuminate\Console\Command; use Statamic\Console\RunsInPlease; use Statamic\Facades\Stache; -use Wilderborn\Partyline\Facade as Partyline; + +use function Laravel\Prompts\spin; class StacheRefresh extends Command { @@ -16,12 +17,9 @@ class StacheRefresh extends Command public function handle() { - Partyline::bind($this); - - $this->line('Please wait. This may take a while if you have a lot of content.'); - - Stache::refresh(); + spin(callback: fn () => Stache::clear(), message: 'Clearing the Stache...'); + spin(callback: fn () => Stache::warm(), message: 'Warming the Stache...'); - $this->info('You have trimmed and polished the Stache. It is handsome, warm, and ready.'); + $this->components->info('You have trimmed and polished the Stache. It is handsome, warm, and ready.'); } } diff --git a/src/Console/Commands/StacheWarm.php b/src/Console/Commands/StacheWarm.php index 7df4777b2d..0a6965c744 100644 --- a/src/Console/Commands/StacheWarm.php +++ b/src/Console/Commands/StacheWarm.php @@ -5,7 +5,8 @@ use Illuminate\Console\Command; use Statamic\Console\RunsInPlease; use Statamic\Facades\Stache; -use Wilderborn\Partyline\Facade as Partyline; + +use function Laravel\Prompts\spin; class StacheWarm extends Command { @@ -16,12 +17,8 @@ class StacheWarm extends Command public function handle() { - Partyline::bind($this); - - $this->line('Please wait. This may take a while if you have a lot of content.'); - - Stache::warm(); + spin(callback: fn () => Stache::warm(), message: 'Warming the Stache...'); - $this->info('You have poured oil over the Stache and polished it until it shines. It is warm and ready'); + $this->components->info('You have poured oil over the Stache and polished it until it shines. It is warm and ready'); } } diff --git a/src/Console/Commands/StarterKitExport.php b/src/Console/Commands/StarterKitExport.php index a31321f93e..3760878701 100644 --- a/src/Console/Commands/StarterKitExport.php +++ b/src/Console/Commands/StarterKitExport.php @@ -9,6 +9,8 @@ use Statamic\Facades\Path; use Statamic\StarterKits\Exceptions\StarterKitException; +use function Laravel\Prompts\confirm; + class StarterKitExport extends Command { use RunsInPlease; @@ -43,12 +45,12 @@ public function handle() try { StarterKitExporter::export($path); } catch (StarterKitException $exception) { - $this->error($exception->getMessage()); + $this->components->error($exception->getMessage()); return 1; } - $this->info("Starter kit was successfully exported to [$path]."); + $this->components->info("Starter kit was successfully exported to [$path]."); } /** @@ -60,7 +62,7 @@ protected function askToStubStarterKitConfig() $newPath = base_path($config = 'starter-kit.yaml'); if ($this->input->isInteractive()) { - if (! $this->confirm("Config [{$config}] does not exist. Would you like to create it now?", true)) { + if (! confirm("Config [{$config}] does not exist. Would you like to create it now?", true)) { return; } } @@ -93,13 +95,13 @@ protected function getAbsolutePath() protected function askToCreateExportPath($path) { if ($this->input->isInteractive()) { - if (! $this->confirm("Path [{$path}] does not exist. Would you like to create it now?", true)) { + if (! confirm("Path [{$path}] does not exist. Would you like to create it now?", true)) { return; } } File::makeDirectory($path, 0755, true); - $this->comment("A new directory has been created at [{$path}]."); + $this->components->info("A new directory has been created at [{$path}]."); } } diff --git a/src/Console/Commands/StarterKitInstall.php b/src/Console/Commands/StarterKitInstall.php index 119235b852..75d492e6c0 100644 --- a/src/Console/Commands/StarterKitInstall.php +++ b/src/Console/Commands/StarterKitInstall.php @@ -10,6 +10,9 @@ use Statamic\StarterKits\Installer as StarterKitInstaller; use Statamic\StarterKits\LicenseManager as StarterKitLicenseManager; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\text; + class StarterKitInstall extends Command { use RunsInPlease, ValidatesInput; @@ -59,7 +62,8 @@ public function handle() ->fromLocalRepo($this->option('local')) ->withConfig($this->option('with-config')) ->withoutDependencies($this->option('without-dependencies')) - ->withUser($cleared && $this->input->isInteractive() && ! $this->option('cli-install')) + ->isInteractive($isInteractive = $this->input->isInteractive()) + ->withUser($cleared && $isInteractive && ! $this->option('cli-install')) ->usingSubProcess($this->option('cli-install')) ->force($this->option('force')); @@ -81,7 +85,7 @@ public function handle() $this->comment('composer global update statamic/cli'.PHP_EOL); } - $this->info("Starter kit [$package] was successfully installed."); + $this->components->info("Starter kit [$package] was successfully installed."); } /** @@ -91,7 +95,7 @@ public function handle() */ protected function getPackage() { - return $this->argument('package') ?: $this->ask('Package'); + return $this->argument('package') ?: text('Package'); } /** @@ -104,7 +108,7 @@ protected function shouldClear() if ($this->option('clear-site')) { return true; } elseif ($this->input->isInteractive()) { - return $this->confirm('Clear site first?', false); + return confirm('Clear site first?', false); } return false; diff --git a/src/Console/Commands/StarterKitRunPostInstall.php b/src/Console/Commands/StarterKitRunPostInstall.php index 3eee3dea8d..0db823cb58 100644 --- a/src/Console/Commands/StarterKitRunPostInstall.php +++ b/src/Console/Commands/StarterKitRunPostInstall.php @@ -37,7 +37,7 @@ public function handle() } if (! app('files')->exists(base_path("vendor/{$package}"))) { - $this->error("Cannot find starter kit [{$package}] in vendor."); + $this->components->error("Cannot find starter kit [{$package}] in vendor."); return 1; } @@ -47,11 +47,11 @@ public function handle() try { $installer->runPostInstallHook(true)->removeStarterKit(); } catch (StarterKitException $exception) { - $this->error($exception->getMessage()); + $this->components->error($exception->getMessage()); return 1; } - $this->info("Starter kit [$package] was successfully installed."); + $this->components->info("Starter kit [$package] was successfully installed."); } } diff --git a/src/Console/Commands/StaticClear.php b/src/Console/Commands/StaticClear.php index a6bc6bf7fb..98732f7812 100644 --- a/src/Console/Commands/StaticClear.php +++ b/src/Console/Commands/StaticClear.php @@ -6,6 +6,8 @@ use Statamic\Console\RunsInPlease; use Statamic\Facades\StaticCache; +use function Laravel\Prompts\spin; + class StaticClear extends Command { use RunsInPlease; @@ -31,8 +33,8 @@ class StaticClear extends Command */ public function handle() { - StaticCache::flush(); + spin(callback: fn () => StaticCache::flush(), message: 'Clearing the static page cache...'); - $this->info('Your static page cache is now so very, very empty.'); + $this->components->info('Your static page cache is now so very, very empty.'); } } diff --git a/src/Console/Commands/StaticWarm.php b/src/Console/Commands/StaticWarm.php index ad9a8b1975..c1010af209 100644 --- a/src/Console/Commands/StaticWarm.php +++ b/src/Console/Commands/StaticWarm.php @@ -44,7 +44,7 @@ class StaticWarm extends Command public function handle() { if (! config('statamic.static_caching.strategy')) { - $this->error('Static caching is not enabled.'); + $this->components->error('Static caching is not enabled.'); return 1; } @@ -52,7 +52,7 @@ public function handle() $this->shouldQueue = $this->option('queue'); if ($this->shouldQueue && config('queue.default') === 'sync') { - $this->error('The queue connection is set to "sync". Queueing will be disabled.'); + $this->components->error('The queue connection is set to "sync". Queueing will be disabled.'); $this->shouldQueue = false; } @@ -60,10 +60,10 @@ public function handle() $this->warm(); - $this->output->newLine(); - $this->info($this->shouldQueue - ? 'All requests to warm the static cache have been added to the queue.' - : 'The static cache has been warmed.' + $this->components->info( + $this->shouldQueue + ? 'All requests to warm the static cache have been added to the queue.' + : 'The static cache has been warmed.' ); return 0; @@ -116,7 +116,7 @@ private function concurrency(): int public function outputSuccessLine(Response $response, $index): void { - $this->checkLine($this->getRelativeUri($index)); + $this->components->twoColumnDetail($this->getRelativeUri($index), '✓ Cached'); } public function outputFailureLine($exception, $index): void @@ -135,7 +135,7 @@ public function outputFailureLine($exception, $index): void $message = $exception->getMessage(); } - $this->crossLine("$uri → $message"); + $this->components->twoColumnDetail($uri, "$message"); } private function getRelativeUri(int $index): string diff --git a/src/Console/Commands/SupportZipBlueprint.php b/src/Console/Commands/SupportZipBlueprint.php index bf402ae381..9d0608a74c 100644 --- a/src/Console/Commands/SupportZipBlueprint.php +++ b/src/Console/Commands/SupportZipBlueprint.php @@ -26,8 +26,7 @@ public function handle() return 1; } - $this->info('Zip created successfully.'); - $this->comment("Your zip file awaits: {$filename}"); + $this->components->info("Zip [{$filename}] created successfully."); } protected function createZip($blueprint) @@ -37,7 +36,7 @@ protected function createZip($blueprint) $zip = new ZipArchive(); if ($zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - $this->error("Unable to create zip file \"$filename\""); + $this->components->error("Unable to create zip file: [$filename]"); return false; } @@ -58,7 +57,7 @@ protected function getBlueprint() $handle = $this->argument('blueprint'); if (! $blueprint = Blueprint::find($handle)) { - $this->error("Blueprint \"$handle\" not found"); + $this->components->error("Blueprint [$handle] not found."); return null; } diff --git a/src/Console/Commands/UpdatesRun.php b/src/Console/Commands/UpdatesRun.php index ba29f611ea..20d39742f5 100644 --- a/src/Console/Commands/UpdatesRun.php +++ b/src/Console/Commands/UpdatesRun.php @@ -39,7 +39,7 @@ public function handle() $success = UpdateScriptManager::runUpdatesForSpecificPackageVersion($package, $this->argument('version'), $this); $success - ? $this->info('Update scripts were run successfully!') - : $this->comment('There were no update scripts for this version.'); + ? $this->components->info('Update scripts were run successfully!') + : $this->components->warn('There were no update scripts for this version.'); } } diff --git a/src/Console/ValidatesInput.php b/src/Console/ValidatesInput.php index f219643f4a..92b250f285 100644 --- a/src/Console/ValidatesInput.php +++ b/src/Console/ValidatesInput.php @@ -21,7 +21,7 @@ private function validationFails($input, $rules) return false; } - $this->error($validator->errors()->first()); + $this->components->error($validator->errors()->first()); return true; } diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 1bfab301e9..29e164c817 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -10,7 +10,6 @@ use Statamic\Support\Str; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; -use Wilderborn\Partyline\Facade as Partyline; class Stache { @@ -82,8 +81,6 @@ public function generateId() public function clear() { - Partyline::comment('Clearing Stache...'); - $this->stores()->reverse()->each->clear(); $this->duplicates()->clear(); @@ -100,8 +97,6 @@ public function refresh() public function warm() { - Partyline::comment('Warming Stache...'); - $lock = tap($this->lock('stache-warming'))->acquire(true); $this->startTimer(); diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index eaa856f7b0..17996a83e5 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -5,8 +5,11 @@ use Facades\Statamic\Console\Processes\Composer; use Facades\Statamic\Console\Processes\TtyDetector; use Facades\Statamic\StarterKits\Hook; +use Illuminate\Console\Command; +use Illuminate\Console\View\Components\Line; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Http; +use Laravel\Prompts\Prompt; use Statamic\Console\NullConsole; use Statamic\Console\Please\Application as PleaseApplication; use Statamic\Console\Processes\Exceptions\ProcessException; @@ -16,6 +19,9 @@ use Statamic\StarterKits\Exceptions\StarterKitException; use Statamic\Support\Str; +use function Laravel\Prompts\confirm; +use function Laravel\Prompts\spin; + final class Installer { protected $package; @@ -51,7 +57,7 @@ public function __construct(string $package, $console = null, ?LicenseManager $l * @param mixed $console * @return static */ - public static function package(string $package, $console = null, ?LicenseManager $licenseManager = null) + public static function package(string $package, ?Command $console = null, ?LicenseManager $licenseManager = null) { return new self($package, $console, $licenseManager); } @@ -95,6 +101,13 @@ public function withoutDependencies($withoutDependencies = false) return $this; } + public function isInteractive($isInteractive = false) + { + Prompt::interactive($isInteractive); + + return $this; + } + /** * Install with super user. * @@ -248,13 +261,16 @@ protected function prepareRepository() */ protected function requireStarterKit() { - $this->console->info("Preparing starter kit [{$this->package}]..."); - - try { - Composer::withoutQueue()->throwOnFailure()->requireDev($this->package); - } catch (ProcessException $exception) { - $this->rollbackWithError("Error installing starter kit [{$this->package}].", $exception->getMessage()); - } + spin( + function () { + try { + Composer::withoutQueue()->throwOnFailure()->requireDev($this->package); + } catch (ProcessException $exception) { + $this->rollbackWithError("Error installing starter kit [{$this->package}].", $exception->getMessage()); + } + }, + "Preparing starter kit [{$this->package}]..." + ); return $this; } @@ -481,7 +497,7 @@ public function makeSuperUser() return $this; } - if ($this->console->confirm('Create a super user?', false)) { + if (confirm('Create a super user?', false)) { $this->console->call('make:user', ['--super' => true]); } @@ -569,11 +585,14 @@ protected function registerInstalledCommand($commandClass) */ protected function reticulateSplines() { - $this->console->info('Reticulating splines...'); - - if (config('app.env') !== 'testing') { - usleep(500000); - } + spin( + function () { + if (config('app.env') !== 'testing') { + usleep(500000); + } + }, + 'Reticulating splines...' + ); return $this; } @@ -589,11 +608,14 @@ public function removeStarterKit() return $this; } - $this->console->info('Cleaning up temporary files...'); - - if (Composer::isInstalled($this->package)) { - Composer::withoutQueue()->throwOnFailure(false)->removeDev($this->package); - } + spin( + function () { + if (Composer::isInstalled($this->package)) { + Composer::withoutQueue()->throwOnFailure(false)->removeDev($this->package); + } + }, + 'Cleaning up temporary files...' + ); return $this; } diff --git a/tests/Console/Commands/AssetsMetaTest.php b/tests/Console/Commands/AssetsMetaTest.php index da5b693000..0e894104e1 100644 --- a/tests/Console/Commands/AssetsMetaTest.php +++ b/tests/Console/Commands/AssetsMetaTest.php @@ -72,7 +72,7 @@ public function it_generates_one_asset_meta_file_for_asset_with_no_meta_file() Storage::disk('test')->assertMissing('foo/.meta/bar.txt.yaml'); $this->artisan('statamic:assets:meta test_container') - ->expectsOutput('Asset metadata generated'); + ->expectsOutputToContain('Generated metadata for 1 asset.'); Storage::disk('test')->assertExists('foo/bar.txt'); Storage::disk('test')->assertExists('foo/.meta/bar.txt.yaml'); @@ -90,7 +90,7 @@ public function it_preserves_data_property_in_meta_data_file() ); $this->artisan('statamic:assets:meta test_container') - ->expectsOutput('Asset metadata generated'); + ->expectsOutputToContain('Generated metadata for 1 asset.'); $this->assertEquals( Arr::get(YAML::parse(Storage::disk('test')->get('foo/.meta/bar.txt.yaml')), 'data.foo'), diff --git a/tests/Console/Commands/MakeAddonTest.php b/tests/Console/Commands/MakeAddonTest.php index 6b02194c9f..0e89bfc64b 100644 --- a/tests/Console/Commands/MakeAddonTest.php +++ b/tests/Console/Commands/MakeAddonTest.php @@ -56,10 +56,10 @@ public function it_can_generate_an_addon() public function it_cannot_make_addon_with_invalid_composer_package_name() { $this->artisan('statamic:make:addon', ['addon' => 'deaths-tar-vulnerability']) - ->expectsOutput('Please enter a valid composer package name (eg. hasselhoff/kung-fury).'); + ->expectsOutputToContain('Please enter a valid composer package name (eg. hasselhoff/kung-fury).'); $this->artisan('statamic:make:addon', ['addon' => 'some/path/deaths-tar-vulnerability']) - ->expectsOutput('Please enter a valid composer package name (eg. hasselhoff/kung-fury).'); + ->expectsOutputToContain('Please enter a valid composer package name (eg. hasselhoff/kung-fury).'); $this->assertFileDoesNotExist(base_path('addons/erso/deaths-tar-vulnerability')); } diff --git a/tests/Console/Commands/MakeUserTest.php b/tests/Console/Commands/MakeUserTest.php index 29c1c59303..3c01dfa0dc 100644 --- a/tests/Console/Commands/MakeUserTest.php +++ b/tests/Console/Commands/MakeUserTest.php @@ -44,8 +44,8 @@ public function it_can_make_a_super_user_interactively() $this->artisan('statamic:make:user') ->expectsQuestion('Email', 'jason@ifyoucantescapeit.org') ->expectsQuestion('Name', 'Jason') - ->expectsQuestion('Password (Your input will be hidden)', 'midnight') - ->expectsQuestion('Super user', true) + ->expectsQuestion('Password', 'midnight') + ->expectsQuestion('Super user?', true) ->assertExitCode(0); $user = User::all()->first(); @@ -65,8 +65,8 @@ public function it_can_make_a_non_super_user_interactively() $this->artisan('statamic:make:user') ->expectsQuestion('Email', 'jesses.girl@springfield.com') ->expectsQuestion('Name', 'Gertrude') - ->expectsQuestion('Password (Your input will be hidden)', 'iloverickie') - ->expectsQuestion('Super user', false) + ->expectsQuestion('Password', 'iloverickie') + ->expectsQuestion('Super user?', false) ->assertExitCode(0); $user = User::all()->first(); @@ -82,13 +82,13 @@ public function it_validates_email() $this->assertEmpty(User::all()); $this->artisan('statamic:make:user', ['email' => 'jason']) - ->expectsOutput(trans('validation.email', ['attribute' => 'input'])); + ->expectsOutputToContain(trans('validation.email', ['attribute' => 'input'])); $this->artisan('statamic:make:user', ['email' => 'jason@keeponrunnin.com']) - ->expectsOutput('User created successfully.'); + ->expectsOutputToContain('User created successfully.'); $this->artisan('statamic:make:user', ['email' => 'jason@keeponrunnin.com']) - ->expectsOutput('A user with this email already exists.'); + ->expectsOutputToContain('A user with this email already exists.'); } /** @test */ diff --git a/tests/StarterKits/RunPostInstallTest.php b/tests/StarterKits/RunPostInstallTest.php index 3efcff0dae..83efc82b3b 100644 --- a/tests/StarterKits/RunPostInstallTest.php +++ b/tests/StarterKits/RunPostInstallTest.php @@ -79,7 +79,7 @@ public function it_errors_gracefully_if_post_install_hook_cannot_be_found() ->artisan('statamic:starter-kit:run-post-install', [ 'package' => 'statamic/cool-runnings', ]) - ->expectsOutput('Cannot find post-install hook for [statamic/cool-runnings].') + ->expectsOutputToContain('Cannot find post-install hook for [statamic/cool-runnings].') ->assertExitCode(1); $this->assertFalse(Blink::has('post-install-hook-run')); @@ -97,7 +97,7 @@ public function it_errors_gracefully_if_starter_kit_package_doesnt_exist_in_vend ->artisan('statamic:starter-kit:run-post-install', [ 'package' => 'statamic/non-existent', ]) - ->expectsOutput('Cannot find starter kit [statamic/non-existent] in vendor.') + ->expectsOutputToContain('Cannot find starter kit [statamic/non-existent] in vendor.') ->assertExitCode(1); $this->assertFalse(Blink::has('post-install-hook-run'));