stepanenko3/nova-command-runner
Run Artisan commands directly from Laravel Nova. This Nova tool adds a command runner panel where you can execute and manage artisan commands without leaving your admin dashboard, with output display and a simple interface for developers and admins.
1. Install and register the tool.
composer require stepanenko3/nova-command-runner
Register it in app/Providers/NovaServiceProvider.php:
use Stepanenko3\NovaCommandRunner\NovaCommandRunner;
public function tools(): array
{
return [
...parent::tools(),
new NovaCommandRunner(),
];
}
2. Publish and read the config stub — this is the first place to look. The package is config-driven: the tool renders a UI from a list of command definitions you declare in PHP. Publish the config (php artisan vendor:publish --provider="Stepanenko3\NovaCommandRunner\ToolServiceProvider", or check the provider for the exact --tag) and treat the published stub as the source of truth for key names, since they have shifted between releases:
// config/nova-command-runner.php
return [
'commands' => [
[
'name' => 'Clear Application Cache',
'command' => 'cache:clear',
],
[
'name' => 'Run Migrations',
'command' => 'migrate --force',
],
[
'name' => 'Tail Laravel Log',
'command' => 'tail -n 100 storage/logs/laravel.log',
'type' => 'bash',
],
],
];
3. First use case: cache and queue hygiene. Clear config/route/view caches, restart queue workers, storage:link, queue:restart — the repetitive ops buttons you currently SSH in for. These are idempotent, low-risk, and prove the tool works before you add anything destructive. Run one from /nova, confirm the output pane shows the Artisan banner and exit code, then stop and think about access control (see below) before adding migrate.
Model the command list like a menu, not a shell. Group entries by intent (Deploys, Caches, Queues, Debug) and give each a human name and a fixed command string. The tool's value is that operators never type free-form shell — every action is a vetted, named recipe. If you find yourself wanting a text box for arguments, add three explicit variants (migrate, migrate:fresh --seed, migrate:rollback --step=1) instead.
Prefer Artisan over bash wherever a command exists. php artisan queue:restart is portable and reports a proper exit code; pkill -f queue:work is neither. Reserve type => bash for things with no Artisan equivalent: tailing logs, supervisorctl status, df -h, checking storage/logs sizes.
Wrap risky commands in your own Artisan commands and expose the wrappers. This is the single highest-leverage pattern. Define php artisan ops:backup-db in your app that backs up to S3, logs auth()->user()->email, and returns a non-zero exit on failure; register that in the config. You get audit trails, guardrails, and testability the package itself won't give you.
Gate the whole tool, not just individual buttons. Register it conditionally so non-admins never see the menu:
// AuthServiceProvider
Gate::define('run-commands', fn ($user) => $user->hasRole('super-admin'));
// NovaServiceProvider::tools()
return array_filter([
...parent::tools(),
Gate::allows('run-commands') ? new NovaCommandRunner() : null,
]);
If your version exposes a visibility hook on the tool class, use that instead — but conditional registration always works.
Scope definitions per environment. In AppServiceProvider::boot() or the config file, build the list conditionally: production gets read-only diagnostics plus cache:clear/queue:restart; staging gets migrate --force and db:seed; local gets everything. Keep a config('app.env') check adjacent to the array so the intent is obvious in review.
Pair with a deploy tool, don't replace it. This is a break-glass console for "the cron is stuck at 2am," not a CI/CD step. Anything that must happen in order, atomically, or with rollback belongs in Envoyer/Forge/Deployer.
www-data; composer install, storage:link, and migrate will write files owned by www-data, which then breaks your next git pull or deploy. Either sudo -u deploy in the command, or restrict the tool to commands that only touch storage/ and the DB.~/.composer/vendor/bin, node, and often even a usable php symlink. Use absolute paths: /usr/bin/php /var/www/app/artisan migrate --force. Test each command in the browser; a command that works in SSH is not evidence.migrate over a large table, composer install) return nothing until they finish. Nginx's default 60s fastcgi_read_timeout will 504 the request while the process keeps running server-side — you'll see an error but the migration did happen. Never run tail -f, a queue worker, or npm run dev through this; they never return.--force in production, and that flag is exactly what makes them dangerous here. migrate, db:wipe, migrate:fresh, and cache:clear in prod are one misclick apart. Consider removing migrate:fresh from the config entirely and requiring a second person for anything with fresh or wipe in the string.config:cache hides your edits. In production the command list is baked into the cached config — after changing it, run php artisan config:cache (and config:clear locally). Symptom: buttons you added don't appear, or deleted destructive buttons still show up.proc_open/exec may be disabled. Shared hosts and hardened PHP images disable these in disable_functions; the tool will fail silently or throw. Check php -i | grep disable_functions before blaming the package.cache:clear, fatal for long migrations. Enforce with a Cache::lock() inside your own wrapper command.@pawell67, PR #211); everything else is dependabot bumps of tailwindcss and vite. Practical consequence: on 4.5.0 or earlier you may see a broken tool UI with no console command involved — bump to 4.5.1 before debugging. The release carries no breaking changes and no config/API changes, so definitions written against 4.5.0 keep working as-is.composer.json ("stepanenko3/nova-command-runner": "4.5.1", not ^4.0) so a surprise release can't widen its own blast radius.storage/logs/laravel.log. If the pane is empty, check the web server's error_log; a 500 there almost always means a missing binary or a disabled PHP function, not a package bug.--token=...) end up in a PHP file that's often copied between environments and readable by anyone who can view the Nova menu. Reference env() and keep the value out of the definition.How can I help you explore Laravel packages today?