pragmarx/artisan-tool
A lightweight web interface for running Laravel Artisan commands from the browser. Browse available commands, execute them, and view their output without opening a terminal—handy for quick admin tasks, demos, and remote environments.
Confirm the fit before writing a line of integration. Despite the generic description ("wraps the console layer in a clean, expressive API"), the package is a Nova tool — it registers in the Nova sidebar. If the app doesn't run Laravel Nova, stop here. If it does, the mental model is: a browser session gains the ability to execute Artisan as the web server user. Everything below flows from that.
Install and publish.
composer require pragmarx/artisan-tool
php artisan vendor:publish --provider="PragmaRX\ArtisanTool\ServiceProvider"
php artisan config:clear
Nova 4+ uses Vite for tool assets. If the sidebar item never appears after publishing, the compiled asset almost certainly didn't land — that's an asset problem, not a registration problem (see Gotchas).
Read in this order before enabling anything:
config/artisan-tool.php (or whatever the publish step dropped) — this is your allowlist/permission surface.src/ end to end. The package is small. What you need to know is whether it calls Artisan::call() in-process, spawns a subprocess, or queues; and how it captures stdout, stderr, and exit codes.NovaServiceProvider::tools() — where the gate belongs.config/nova.php and your Nova middleware stack — where you bound exposure by environment/IP.Register it behind a gate, not bare.
// app/Providers/NovaServiceProvider.php
public function tools()
{
return array_filter([
Gate::allows('run-artisan') ? new \PragmaRX\ArtisanTool\ArtisanTool() : null,
]);
}
Define run-artisan in AuthServiceProvider against a named role or explicit user allowlist. "Is a Nova user" is far too broad — Nova access is not shell access.
First use case: read-only diagnostics for a trusted operator. about, route:list, migrate:status, queue:failed, schedule:list, model:show. These are idempotent, cover most "I need a shell just to check something" requests, and carry near-zero side-effect risk. The pilot's purpose is validating guardrails — gate, allowlist, audit log, timeout behavior — not proving you can run a command.
Gate at the tool level, not inside the UI. Filtering via tools() is strictly better than relying on the package's own permission checks: a missing sidebar item is a far better failure mode than a visible-but-unauthorized console. For a second trust tier (read-only ops vs. destructive ops), add your own Gate::allows() checks around your curated commands — don't extend the package's UI assumptions.
Curate purpose-built app: commands instead of exposing the raw console. This is the highest-leverage pattern day to day. Narrow commands with baked-in defaults, no prompts, and logging baked in:
class RefreshSearchIndex extends Command
{
protected $signature = 'app:refresh-search-index {--full : Rebuild from scratch}';
public function handle()
{
Log::info('search index refresh via Nova', [
'user' => auth()->id(),
'full' => $this->option('full'),
]);
// deterministic work; no $this->ask(), no confirmations
return self::SUCCESS;
}
}
You get auditability for free, a stable command name, sane defaults, and the option to queue the real work. It also makes the package swappable: if you later replace it with a custom Nova tool, operators keep the same conceptual commands and the same runbook.
Use the wrapper API from application code where it fits. If the package exposes a programmatic entry point (the description advertises "execute any registered Artisan command from PHP" and "capture output as a string"), that's genuinely handy in tests and in your own admin controllers:
$output = app(\PragmaRX\ArtisanTool\ArtisanRunner::class)->run('app:refresh-search-index');
$this->assertStringContainsString('done', $output);
Verify the actual class/method names in src/ — the README is generic. Treat the string output as untrusted presentation data, not structured results; assert on things you control.
Start with read-only, graduate deliberately. Diagnostics first. Only after the audit trail and failure surfacing are proven should you add cache:clear / view:clear / queue:restart. Destructive commands (migrate, db:wipe, queue:flush) belong in a deploy pipeline, not in a browser button.
Document runbook order for deploy-time sequences. Under pressure, everything in a Nova console looks one click away, which makes improvised ordering more likely. Write it down: config:cache → route:cache → queue:restart → storage:link → migrate --force.
Dispatch anything slow onto a queue. More than a few seconds means it shouldn't run synchronously through a Nova web request. Either the curated command dispatches a job and returns immediately, or you queue the invocation and report progress elsewhere. Don't hold PHP-FPM workers hostage for a cache warm.
Log invocations yourself. The browser gives you no shell history. Logging inside each curated app: command is the durable option (survives a package swap). If you must allow free-form input, wrap execution in your own thin controller/middleware that records user_id, the command line, start/end time, and exit status.
Use it locally first. During development, cache:clear, config:cache, queue:restart, and diagnostics in the browser are genuinely convenient when you're pairing, on mobile, or in a flaky remote session. It's the lowest-risk way to learn the package's output formatting and failure behavior before staging ever sees it.
Keep the capability inside Nova. Don't bolt a second route or standalone UI onto the same execution path. Staying inside Nova means your existing auth, session, middleware, and role model apply without reinvention.
The execution mechanism determines everything. In-process Artisan::call() shares the web request's memory, timeout, and process — and a config:cache run that way won't affect the already-loaded config in that same request. A subprocess runs as the web server user with whatever env and permissions that implies, and output/exit-code capture becomes your responsibility to interpret. Read src/ and find out which one this package does before reasoning about failure modes.
The web server user is not you. Commands run as www-data (or your FPM pool user). migrate may lack DB grants; writes outside storage/ and bootstrap/cache/ produce permission errors that look like package bugs; anything shelling out to git misbehaves. Test every allowlisted command as that user on staging before shipping.
Interactive prompts hang or silently no-op. There is no stdin in a browser context. migrate without --force, db:wipe confirmations, and any $this->ask() will stall or bail. Add --no-interaction where available and simply don't expose commands that require input.
Timeouts and truncated output are real failure modes — and failures may look like successes. Long commands hit max_execution_time or FPM timeouts with no partial output. Large output gets clipped. Most importantly: a command exiting non-zero may still render as success if exit codes aren't surfaced. Test this deliberately on staging — write a throwaway app:test-failure command that returns 1 and confirm the UI reflects the failure. If it doesn't, and there's no hook to fix it, that alone justifies building a thin custom runner.
config:cache and route:cache through the browser are subtly dangerous. Caching config during a request that already loaded the old config, or in an environment whose .env diverges from what CI expects, produces a boot loop that is miserable to diagnose remotely. Consider blocking both from the UI entirely and keeping them in the deploy pipeline.
Environment gating isn't optional. Either gate registration on app()->environment() or keep the tool out of production. A staging instance is a testing convenience; a production instance is a privileged control plane and needs matching incident procedures.
Nova asset publishing, especially post-upgrade. If the tool compiles to nothing or the sidebar item is invisible, check that vendor:publish placed the tool's assets and that they match your Nova version's pipeline (Nova 4+ is Vite). A stale published asset after a package upgrade looks exactly like a broken tool but isn't.
Pin the version and plan for a fork. Low stars, zero dependents, sparse docs. Pin the Composer constraint, note the commit you validated, and budget a compatibility review at every Nova/Laravel major bump. There is no community pressure to fix things upstream.
Have an exit strategy. The replacement — a small custom Nova tool wrapping an explicit allowlist of your own app: commands with your own auth, logging, and queue dispatch — is a weekend of work and gives clearer authorization, better progress reporting, and full auditability. Design your curated commands and runbook as if that replacement is already coming; then swapping out is a non-event for operators.
Never let it become the only path. Keep a non-Nova route for every operation it covers — CI job, deploy script, controlled SSH. The moment routine maintenance only works through a browser tool, a Nova upgrade or a package regression becomes an operational outage.
How can I help you explore Laravel packages today?