moox / prompts
Prompts is a package that provides CLI-compatible prompts for Laravel Artisan Commands with identical API to Laravel Prompts. Supports all prompt types
Installs: 13
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/moox/prompts
Requires
- moox/core: 5.0.12
Requires (Dev)
- moox/devtools: ^4.0
- pestphp/pest-plugin-browser: ^4.0
- pestphp/pest-plugin-livewire: ^4.0
README
CLI- and Web-compatible prompts for Laravel Artisan commands – with a flow that can continue step-by-step in the browser.
What does a Flow Command look like?
To make a command work as a flow in both CLI and Web, you only need to follow these rules:
-
Extend
FlowCommanduse Moox\Prompts\Support\FlowCommand; use function Moox\Prompts\text; use function Moox\Prompts\select;
class ProjectSetupCommand extends FlowCommand { protected $signature = 'prompts:project-setup'; protected $description = 'Project setup wizard (CLI & Web)';
-
Store state as public properties
(they are automatically persisted between steps in the web flow)public ?string $environment = null; public ?string $projectName = null;
-
Define steps via
promptFlowSteps()– the array order is the flow orderpublic function promptFlowSteps(): array { return [ 'stepIntro', 'stepEnvironment', 'stepProjectName', 'stepSummary', ]; }
-
Each step is a
public function stepXyz(): void– ideally one prompt per steppublic function stepIntro(): void { $this->info('=== Project Setup ==='); } public function stepEnvironment(): void { $this->environment = select( label: 'Which environment do you want to configure?', options: [ 'local' => 'Local', 'staging' => 'Staging', 'production' => 'Production', ], default: 'local', ); } public function stepProjectName(): void { $this->projectName = text( label: 'What is your project name?', placeholder: 'e.g. MyCoolApp', validate: 'required|min:3', required: true, ); } public function stepSummary(): void { $this->info('--- Summary ---'); $this->line('Project: '.$this->projectName); $this->line('Environment: '.$this->environment); } }
-
Optional steps can simply be skipped with a guard at the beginning:
public array $features = []; public function stepLoggingLevel(): void { if (! in_array('logging', $this->features, true)) { return; // skip step } // Prompt … }
-
Calling other Artisan commands – in a flow, always use
$this->call()instead ofArtisan::call(), so the output is also visible in the web UI:public function stepPublishConfig(): void { $shouldPublish = confirm( label: 'Publish the config now?', default: true, ); if (! $shouldPublish) { return; } $this->call('vendor:publish', [ '--tag' => 'moox-prompts-config', ]); }
That’s all you need in the command – no special flow methods, no custom persistence.
Everything else (CLI/Web differences, state, web UI) is handled by the package.
Running flows in the browser (Filament)
Once you’ve created a flow command, you can run it in both CLI and browser.
CLI
php artisan prompts:project-setup
The command behaves like a normal Laravel Artisan command – all prompts are shown in the terminal.
Web
- Open the Filament page “Run Command” (automatically added to navigation)
- Select your flow command from the list
- Click “Start command”
- The flow runs step by step in the browser:
- Every step shows a prompt (text, select, multiselect, confirm, etc.)
- After each step you see the step’s output
- You can cancel any time with “Back to command selection”
- After a successful run the button switches to “Start new command”
Note: All commands executed via the web UI are automatically logged in the database (see Command Execution Logging).
How and why reflection is used
If you’re just writing commands, you don’t need to care about reflection.
To understand what happens under the hood, here’s a short overview.
-
Problem 1: Setting arguments & options in the web flow
Laravel stores arguments/options internally on a protected$inputproperty of your command.
In CLI mode the Artisan kernel takes care of this.
In the web flow, we create fresh command instances – and need to set$inputourselves.
That’s whatPromptFlowRunner::setCommandInput()does via reflection:- finds the
inputproperty on your command object, - temporarily makes it accessible,
- assigns the current
ArrayInputinstance.
Result: In flow commands you can keep usingargument()andoption()normally – both in CLI and in the browser.
- finds the
-
Problem 2: Remembering command state between web requests
In the web flow, your command runs across multiple HTTP requests. Without extra logic, properties like$environment,$features,$projectNamewould be lost between steps.
PromptFlowRunnerhandles this with two internal methods:captureCommandContext($command, $state)- uses reflection to read all non-static properties of your concrete command class
- stores simple values (scalars, arrays,
null) intoPromptFlowState::$context
restoreCommandContext($command, $state)- restores all stored values back onto the new command instance on the next request
Result: For your code it feels like the same command instance keeps running – you don’t need your own persistence layer (cache, DB, session, …).
- restores all stored values back onto the new command instance on the next request
-
Problem 3: Initializing package tools in the web context
Many packages usingSpatie\LaravelPackageToolsonly register publishable resources (config, views, migrations, assets, …) in CLI context.
WebCommandRunneruses reflection to access the internalpackageobject and replaypublishes(...)registrations for the web context.
Result: Commands likevendor:publishwork just as well in the browser as in CLI, even though Laravel is not running in console mode there.
Important:
Reflection is only used inside the package internals, not in your flow commands.
Your commands remain normal Laravel commands – you only need to:
- extend
FlowCommand, - define properties for your state,
- list steps in
promptFlowSteps(), - implement
step*methods (ideally one prompt per step).
The package takes care of the rest (reflection, state, web flow).
Are there alternatives without reflection?
Yes – technically we could avoid reflection, but it would degrade the DX:
- For arguments & options we’d need a custom API instead of
argument()/option(), or force you to manage everything via properties/arrays. That’s less “Laravel-ish” and harder to learn. - For state between steps we could ask you to manually list all properties to persist (e.g.
flowContextKeys()), or manage cache/DB/session yourself. That’s more boilerplate and error-prone. - For Spatie Package Tools in the web we’d either need changes in the Spatie package or manual configuration of all publishable paths – both would make setup more complex.
That’s why we intentionally keep reflection encapsulated in the package and keep your command API as simple as possible.
Command Execution Logging
All commands executed via the web interface are automatically logged in the database.
You can inspect them via the Filament resource “Command Executions”.
Status
Each execution has one of the following statuses:
running: The command is currently runningcompleted: The command finished successfullyfailed: The command failed with an errorcancelled: The command was cancelled by the user or aborted mid-flow
Stored information
For each execution we store:
- Basic information: command name, description, status, timestamps
- Steps: ordered list of all defined steps
- Step outputs: output of each step (JSON)
- Context: all command properties (e.g.
$environment,$projectName,$features, …) - Failure details: for
failedstatus – the error message and the step where it occurred (failed_at_step) - Cancellation details: for
cancelledstatus – the step where cancellation happened (cancelled_at_step) - User: polymorphic relation to the user who started the command (
created_by)
Important: step_outputs vs context
It’s important to understand the difference between these two fields:
-
step_outputs-
Contains the console output of each step.
-
This is everything you print via
$this->info(),$this->line(),$this->warn(), etc. -
Example:
public function stepEnvironment(): void { $this->environment = select(...); $this->info("✅ Environment: {$this->environment}"); }
will result in e.g.:
"stepEnvironment": "✅ Environment: production\n"
-
-
context-
Contains the raw state of your command – all public, non-static properties from your concrete command class.
-
This includes values returned from
text(),select(),multiselect(),confirm(), etc. -
Example:
public ?bool $publishConfig = null; public function stepPublishConfigConfirm(): void { $this->publishConfig = confirm( label: 'Publish the config now?', default: true, ); }
will store in
contextsomething like:{ "publishConfig": true }but
step_outputs["stepPublishConfigConfirm"]will be an empty string, becauseconfirm()itself doesn’t print anything.
-
If you want to see the user’s choice in the step output as well, you can explicitly print it:
public function stepPublishConfigConfirm(): void { $this->publishConfig = confirm( label: 'Publish the config now?', default: true, ); $this->info('✅ Publish config: ' . ($this->publishConfig ? 'yes' : 'no')); }
This way you have:
- the decision in
context.publishConfig, and - a readable line in
step_outputs.stepPublishConfigConfirmfor the history/inspector UI.
Running the migration
To enable logging, run:
php artisan migrate
This creates the command_executions table with all necessary fields.
Filament resource
The Filament resource “Command Executions” is automatically available in the Filament navigation (if enabled). There you can:
- inspect all past command executions,
- filter by status,
- see details per execution (steps, outputs, context, errors),
- analyze failed or cancelled commands.
The resource also shows which step a command failed on (failed_at_step) or where it was cancelled (cancelled_at_step).
License
See LICENSE.md