lorisleiva/artisan-ui
Artisan UI provides a clean web interface for browsing and running Laravel Artisan commands without leaving the browser. Inspect available commands, execute them, and review output from a convenient dashboard.
Install the package and get a working dashboard in three commands:
composer require lorisleiva/artisan-ui
php artisan vendor:publish --tag=artisan-ui # publishes config + assets
php artisan serve
The dashboard is served at /artisan-ui by default. On first load you will see the buttons you have registered as actions — nothing is exposed automatically. The config file (config/artisan-ui.php) is where you control the route path, middleware, and which action classes are loaded.
Where to look first, in order:
config/artisan-ui.php — set the path and, importantly, the middleware. Add 'auth' (or an admin gate) before you do anything else.The fastest first use case is wrapping a safe, idempotent, developer-facing command you already run by hand — cache:clear, queue:restart, queue:retry all, or route:clear. That gives you a working button in a few lines and validates that your middleware and route prefix are correct before you start wiring up anything destructive.
// config/artisan-ui.php (shape varies slightly by version)
return [
'path' => 'admin/artisan-ui',
'middleware' => ['web', 'auth', 'can:manage-system'],
'actions' => [
\App\ArtisanUi\Actions\ClearCache::class,
],
];
Action class per capability. Do not dump raw command strings into config. Create a small action class per button (ClearCache, RestartQueue, BackupDatabase) so authorization, labelling, and input handling live next to the command they run and are unit-testable. This also keeps the config file stable while your operational surface grows.
Authorization on the action, not just the route. The route middleware gates the dashboard as a whole; per-action authorize() (or a can check inside handle()) gates the destructive buttons. This lets support staff see read-only actions while only SREs see migrate:fresh.
Treat actions as parameterised forms. The real value over a terminal is that you can expose a narrow, validated input surface: a list of queues, a date range, a tenant ID. Validate inputs, whitelist permitted values, and pass them as arguments/options to Artisan::call() rather than interpolating into a shell string.
public function handle()
{
$this->validate(['queue' => 'required|in:default,emails,exports']);
Artisan::call('queue:restart'); // or a parameterised command
return 'Queue restarted.';
}
Group by operational domain, not by artisan namespace. Buttons grouped as Caches, Queues, Data, Deploy map onto how on-call engineers actually think. The package renders them flatly unless you organise the registry, so enforce grouping in your naming/config ordering.
Pair with logging and notifications. Have each action dispatch a log entry (Log::info('artisan-ui: restart queue by ' . auth()->id())) and, for anything long-running, a Slack notification on completion. The UI is an audit surface; treat every button press as an operational event.
Integration tip — deploy hook. The safest pattern is to register actions that mirror your deployment runbook (config:cache, route:cache, horizon:terminate) so post-deploy recovery is one click, but keep migrate and anything destructive behind a confirmation input or a separate gated action.
Use it as an internal ops console, not an admin panel. It complements Tinker, Horizon, and your APM tool; it does not replace them.
/artisan-ui is remote command execution as a service. Add auth plus a gate, and consider an IP allowlist middleware for production.php artisan config:cache in a deploy can produce an empty or stale action list if registration depends on config values. Test with config cached before shipping.Bus::dispatch(new RunCommand)) and let the action return "started", rather than blocking the UI.Artisan::call('cmd', ['--option' => $validated]) over exec('php artisan cmd ' . $input). If the package's action wrapper shells out, audit every interpolation point.migrate:fresh) using app()->isProduction() inside authorize() rather than trusting operators to click carefully.authorize() (hidden vs. 403), config cache (empty registry), then the command itself (check storage/logs and Artisan::output()). Return output from handle() so failures are visible in the UI instead of silently swallowed.path/middleware (multi-tenant or environment-scoped mounting). Avoid forking the package internals; the shallow extension surface is a feature given its age.How can I help you explore Laravel packages today?