Skip to content

Commit 1ae2512

Browse files
committed
Add command execution logging
1 parent 02024e6 commit 1ae2512

File tree

17 files changed

+913
-70
lines changed

17 files changed

+913
-70
lines changed

packages/prompts/README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,128 @@ Damit ein Command sowohl in der CLI als auch im Web korrekt als Flow funktionier
112112
Mehr ist im Command nicht nötig – keine speziellen Flow-Methoden, keine eigene Persistenz.
113113
Der Rest (CLI/Web-Unterschied, State, Web-Oberfläche) wird komplett vom Package übernommen.
114114

115+
## Ausführung im Browser (Filament)
116+
117+
Nachdem du einen Flow-Command erstellt hast, kannst du ihn sowohl in der CLI als auch im Browser ausführen:
118+
119+
### CLI-Ausführung
120+
121+
```bash
122+
php artisan prompts:project-setup
123+
```
124+
125+
Der Command läuft wie ein normaler Laravel Artisan Command – alle Prompts werden direkt im Terminal angezeigt.
126+
127+
### Web-Ausführung
128+
129+
1. Öffne die Filament-Seite "Run Command" (wird automatisch im Navigation-Menü angezeigt)
130+
2. Wähle einen Flow-Command aus der Liste
131+
3. Klicke auf "Command starten"
132+
4. Der Flow läuft Schritt für Schritt im Browser ab:
133+
- Jeder Step zeigt einen Prompt (Text-Input, Select, Multiselect, Confirm, etc.)
134+
- Nach jedem Step siehst du den Output des Steps
135+
- Du kannst jederzeit mit "Zurück zur Command-Auswahl" abbrechen
136+
- Nach erfolgreichem Abschluss wird der Button zu "Start new command" geändert
137+
138+
**Wichtig:** Alle Commands, die im Web ausgeführt werden, werden automatisch in der Datenbank geloggt (siehe [Command Execution Logging](#command-execution-logging)).
139+
140+
## Wie und warum wird Reflection verwendet?
141+
142+
Wenn du nur Commands schreibst, musst du dich nicht um Reflection kümmern.
143+
Damit du aber verstehst, was im Hintergrund passiert, hier eine kurze Erklärung.
144+
145+
- **Problem 1: Argumente & Optionen im Web setzen**
146+
Laravel speichert Argumente/Optionen intern in einem geschützten Property `$input` deines Commands.
147+
In der CLI kümmert sich der Artisan-Kernel darum, dieses Property zu setzen.
148+
Im Web-Flow erzeugen wir aber selbst neue Command-Instanzen – und müssen `$input` daher manuell setzen.
149+
Genau das macht `PromptFlowRunner::setCommandInput()` mit Reflection:
150+
- es findet das `input`-Property auf deinem Command-Objekt,
151+
- macht es kurz zugänglich,
152+
- und schreibt das aktuelle Input-Objekt hinein.
153+
**Ergebnis:** In Flow-Commands kannst du überall ganz normal `argument()` und `option()` verwenden – egal ob der Command per CLI oder im Browser läuft.
154+
155+
- **Problem 2: Command-State zwischen Web-Requests merken**
156+
Im Web besteht dein Flow aus mehreren HTTP-Requests. Ohne zusätzliche Logik wären Properties wie `$environment`, `$features`, `$projectName` im nächsten Step einfach weg.
157+
`PromptFlowRunner` löst das mit zwei internen Methoden:
158+
- `captureCommandContext($command, $state)`
159+
- liest per Reflection alle nicht-statischen Properties deiner konkreten Command-Klasse aus
160+
- speichert einfache Werte (Scalars, Arrays, `null`) im `PromptFlowState::$context`
161+
- `restoreCommandContext($command, $state)`
162+
- setzt beim nächsten Request alle gespeicherten Werte wieder zurück auf das neue Command-Objekt
163+
**Ergebnis:** Für deinen Code fühlt es sich so an, als würde derselbe Command einfach weiterlaufen – du musst keine eigene Persistenz (Cache, Datenbank, Session, …) schreiben.
164+
165+
- **Problem 3: Package-Tools im Web initialisieren**
166+
Viele Packages, die `Spatie\LaravelPackageTools` verwenden, registrieren ihre publishable Ressourcen (Config, Views, Migrations, Assets, …) nur im CLI-Kontext.
167+
`WebCommandRunner` verwendet Reflection, um intern an das `package`-Objekt eines solchen Service Providers zu kommen und die `publishes(...)`-Registrierung auch im Web nachzuholen.
168+
**Ergebnis:** Befehle wie `vendor:publish` funktionieren im Browser genauso zuverlässig wie in der CLI, obwohl Laravel dort eigentlich nicht im Console-Modus läuft.
169+
170+
**Wichtig:**
171+
Reflection wird nur in diesen internen Klassen des Packages verwendet, nicht in deinen Commands.
172+
Deine Commands bleiben normale Laravel-Commands – du musst nur:
173+
174+
- von `FlowCommand` erben,
175+
- Properties für den State definieren,
176+
- Steps in `promptFlowSteps()` auflisten,
177+
- `step*`-Methoden schreiben (am besten ein Prompt pro Step).
178+
179+
Den Rest (Reflection, State, Web-Flow) übernimmt das Package für dich.
180+
181+
### Gibt es Alternativen ohne Reflection?
182+
183+
Ja – theoretisch könnten wir auf Reflection verzichten, aber das hätte Nachteile für dich als Nutzer:
184+
185+
- Für **Argumente & Optionen** könnten wir eine eigene API einführen (statt `argument()/option()`), oder erzwingen, dass du alles manuell über Properties/Arrays verwaltest. Das wäre weniger “Laravel-typisch” und schwerer zu verstehen.
186+
- Für den **Command-State zwischen Steps** könnten wir dich z.B. eine Methode wie `flowContextKeys()` implementieren lassen, in der du alle zu speichernden Properties auflistest, oder dich zwingen, selbst Cache/DB/Session zu benutzen. Das wäre mehr Boilerplate und eine zusätzliche Fehlerquelle.
187+
- Für **Spatie Package Tools im Web** bräuchten wir entweder Änderungen im Spatie-Package selbst oder eine eigene, manuelle Konfiguration aller publishbaren Pfade – beides würde die Einrichtung deutlich komplizierter machen.
188+
189+
Aus diesen Gründen kapseln wir die Reflection-Nutzung bewusst im Package und halten die API für deine Commands so einfach wie möglich.
190+
191+
## Command Execution Logging
192+
193+
Alle Commands, die über das Web-Interface ausgeführt werden, werden automatisch in der Datenbank geloggt. Du kannst die Ausführungen in der Filament-Resource "Command Executions" einsehen.
194+
195+
### Status
196+
197+
Jede Command-Ausführung hat einen der folgenden Status:
198+
199+
- **`running`**: Der Command läuft gerade
200+
- **`completed`**: Der Command wurde erfolgreich abgeschlossen
201+
- **`failed`**: Der Command ist mit einem Fehler fehlgeschlagen
202+
- **`cancelled`**: Der Command wurde vom Benutzer abgebrochen
203+
204+
### Gespeicherte Informationen
205+
206+
Für jede Ausführung werden folgende Daten gespeichert:
207+
208+
- **Basis-Informationen**: Command-Name, Beschreibung, Status, Zeitstempel
209+
- **Steps**: Liste aller Steps, die ausgeführt wurden
210+
- **Step-Outputs**: Output jedes einzelnen Steps (als JSON)
211+
- **Context**: Alle Command-Properties (z.B. `$environment`, `$projectName`, etc.)
212+
- **Fehler-Informationen**: Bei `failed` Status: Fehlermeldung und der Step, bei dem der Fehler aufgetreten ist (`failed_at_step`)
213+
- **Abbruch-Informationen**: Bei `cancelled` Status: Der Step, bei dem abgebrochen wurde (`cancelled_at_step`)
214+
- **Benutzer**: Polymorphe Beziehung zu dem Benutzer, der den Command gestartet hat (`created_by`)
215+
216+
### Migration ausführen
217+
218+
Um das Logging zu aktivieren, führe die Migration aus:
219+
220+
```bash
221+
php artisan migrate
222+
```
223+
224+
Die Migration erstellt die Tabelle `command_executions` mit allen notwendigen Feldern.
225+
226+
### Filament Resource
227+
228+
Die Filament-Resource "Command Executions" ist automatisch im Filament-Navigation-Menü verfügbar (falls aktiviert). Dort kannst du:
229+
230+
- Alle vergangenen Command-Ausführungen einsehen
231+
- Nach Status filtern
232+
- Details zu jeder Ausführung ansehen (Steps, Outputs, Context, etc.)
233+
- Fehlgeschlagene oder abgebrochene Commands analysieren
234+
235+
Die Resource zeigt auch an, bei welchem Step ein Command fehlgeschlagen (`failed_at_step`) oder abgebrochen (`cancelled_at_step`) wurde.
236+
115237
## License
116238

117239
Siehe [LICENSE.md](LICENSE.md)

packages/prompts/config/prompts.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,24 @@
3131

3232
'allowed_commands' => [
3333
'prompts:project-setup',
34+
'prompts:test-failed',
3435
// Add more commands here as needed
3536
],
3637

38+
/*
39+
|--------------------------------------------------------------------------
40+
| Navigation Group
41+
|--------------------------------------------------------------------------
42+
|
43+
| The navigation group where the Command Runner and Command Executions
44+
| will appear in the Filament navigation. Common options:
45+
| - 'System' (default)
46+
| - 'Jobs'
47+
| - 'Tools'
48+
| - null (no group)
49+
|
50+
*/
51+
52+
'navigation_group' => 'System',
53+
3754
];
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::create('command_executions', function (Blueprint $table) {
15+
$table->id();
16+
$table->string('flow_id')->unique();
17+
$table->string('command_name');
18+
$table->string('command_description')->nullable();
19+
$table->enum('status', ['cancelled', 'completed', 'failed'])->default('running');
20+
$table->timestamp('cancelled_at')->nullable();
21+
$table->string('cancelled_at_step')->nullable();
22+
$table->timestamp('started_at');
23+
$table->timestamp('completed_at')->nullable();
24+
$table->timestamp('failed_at')->nullable();
25+
$table->string('failed_at_step')->nullable();
26+
$table->text('error_message')->nullable();
27+
$table->json('steps')->nullable();
28+
$table->json('step_outputs')->nullable();
29+
$table->json('context')->nullable();
30+
$table->nullableMorphs('created_by');
31+
$table->timestamps();
32+
});
33+
}
34+
35+
/**
36+
* Reverse the migrations.
37+
*/
38+
public function down(): void
39+
{
40+
Schema::dropIfExists('command_executions');
41+
}
42+
};

packages/prompts/resources/lang/de/prompts.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,38 @@
1818
'select_command_placeholder' => 'Bitte Command auswählen …',
1919
'commands_config_hint' => 'Nur Commands aus der Konfiguration sind hier sichtbar.',
2020
'start_command_button' => 'Command starten',
21-
'back_to_selection' => 'Zurück zur Command-Auswahl',
21+
'back_to_selection' => 'Abbrechen und zur Command-Auswahl zurückkehren',
22+
'start_new_command' => 'Neuen Command starten',
2223
'unknown_error' => 'Unbekannter Fehler',
2324
'navigation_label' => 'Command Runner',
2425
'navigation_group' => 'System',
26+
'executions_navigation_label' => 'Command Ausführungen',
27+
'command_name' => 'Command Name',
28+
'command_description' => 'Beschreibung',
29+
'status' => 'Status',
30+
'status_running' => 'Läuft',
31+
'status_completed' => 'Abgeschlossen',
32+
'status_failed' => 'Fehlgeschlagen',
33+
'status_cancelled' => 'Abgebrochen',
34+
'started_at' => 'Gestartet am',
35+
'completed_at' => 'Abgeschlossen am',
36+
'failed_at' => 'Fehlgeschlagen am',
37+
'failed_at_step' => 'Fehlgeschlagen bei Step',
38+
'cancelled_at' => 'Abgebrochen am',
39+
'cancelled_at_step' => 'Abgebrochen bei Step',
40+
'error_message' => 'Fehlermeldung',
41+
'user' => 'Benutzer',
42+
'basic_information' => 'Grundinformationen',
43+
'details' => 'Details',
44+
'context' => 'Kontext',
45+
'steps' => 'Schritte',
2546
],
2647

2748
'errors' => [
2849
'command_not_found' => 'Command nicht gefunden: :command',
2950
'step_not_found' => 'Step :step nicht gefunden auf Command :class',
51+
'command_not_allowed' => 'Command ":command" darf nicht über die Web-Oberfläche ausgeführt werden.',
52+
'flow_access_denied' => 'Sie haben keinen Zugriff auf diese Command-Ausführung.',
3053
],
3154

3255
'validation' => [

packages/prompts/resources/lang/en/prompts.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,38 @@
1818
'select_command_placeholder' => 'Please select a command …',
1919
'commands_config_hint' => 'Only commands from the configuration are visible here.',
2020
'start_command_button' => 'Start command',
21-
'back_to_selection' => 'Back to command selection',
21+
'back_to_selection' => 'Cancel and back to command selection',
22+
'start_new_command' => 'Start new command',
2223
'unknown_error' => 'Unknown error',
2324
'navigation_label' => 'Command Runner',
2425
'navigation_group' => 'System',
26+
'executions_navigation_label' => 'Command Executions',
27+
'command_name' => 'Command Name',
28+
'command_description' => 'Description',
29+
'status' => 'Status',
30+
'status_running' => 'Running',
31+
'status_completed' => 'Completed',
32+
'status_failed' => 'Failed',
33+
'status_cancelled' => 'Cancelled',
34+
'started_at' => 'Started At',
35+
'completed_at' => 'Completed At',
36+
'failed_at' => 'Failed At',
37+
'failed_at_step' => 'Failed At Step',
38+
'cancelled_at' => 'Cancelled At',
39+
'cancelled_at_step' => 'Cancelled At Step',
40+
'error_message' => 'Error Message',
41+
'user' => 'User',
42+
'basic_information' => 'Basic Information',
43+
'details' => 'Details',
44+
'context' => 'Context',
45+
'steps' => 'Steps',
2546
],
2647

2748
'errors' => [
2849
'command_not_found' => 'Command not found: :command',
2950
'step_not_found' => 'Step :step not found on command :class',
51+
'command_not_allowed' => 'Command ":command" is not allowed to be executed through the web interface.',
52+
'flow_access_denied' => 'You do not have access to this command execution.',
3053
],
3154

3255
'validation' => [

packages/prompts/resources/views/filament/components/run-command.blade.php

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
@endif
1515
</x-filament::section>
1616
@elseif($isComplete)
17-
<x-filament::section>
17+
<x-filament::section x-data x-init="$dispatch('command-completed')">
1818
<x-slot name="heading">
1919
{{ __('moox-prompts::prompts.ui.success_heading') }}
2020
</x-slot>
@@ -24,45 +24,48 @@
2424
@endif
2525
</x-filament::section>
2626
@elseif($currentPrompt)
27-
{{ $this->form }}
27+
<div x-data="{ handleEnter(event) { if (event.target.tagName !== 'TEXTAREA') { $wire.submitPrompt(); } } }"
28+
@keydown.enter.prevent="handleEnter($event)">
29+
{{ $this->form }}
2830

29-
@if(!empty($validationErrors))
30-
<div
31-
style="margin-top: 1rem; padding: 0.75rem 1rem; background-color: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 0.375rem;">
32-
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
33-
<svg style="width: 1.25rem; height: 1.25rem; color: #f59e0b; flex-shrink: 0;" fill="none"
34-
stroke="currentColor" viewBox="0 0 24 24">
35-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
36-
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
37-
</path>
38-
</svg>
39-
<strong style="color: #92400e; font-size: 0.875rem; font-weight: 600;">
40-
{{ __('moox-prompts::prompts.ui.validation_title') }}
41-
</strong>
31+
@if(!empty($validationErrors))
32+
<div
33+
style="margin-top: 1rem; padding: 0.75rem 1rem; background-color: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 0.375rem;">
34+
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
35+
<svg style="width: 1.25rem; height: 1.25rem; color: #f59e0b; flex-shrink: 0;" fill="none"
36+
stroke="currentColor" viewBox="0 0 24 24">
37+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
38+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
39+
</path>
40+
</svg>
41+
<strong style="color: #92400e; font-size: 0.875rem; font-weight: 600;">
42+
{{ __('moox-prompts::prompts.ui.validation_title') }}
43+
</strong>
44+
</div>
45+
<ul style="color: #92400e; margin: 0; padding-left: 1.25rem; list-style: disc; font-size: 0.875rem;">
46+
@foreach($validationErrors as $msg)
47+
<li>{{ $msg }}</li>
48+
@endforeach
49+
</ul>
4250
</div>
43-
<ul style="color: #92400e; margin: 0; padding-left: 1.25rem; list-style: disc; font-size: 0.875rem;">
44-
@foreach($validationErrors as $msg)
45-
<li>{{ $msg }}</li>
46-
@endforeach
47-
</ul>
51+
@endif
52+
53+
<div style="margin-top: 1rem; display: flex; justify-content: flex-end;">
54+
<x-filament::button wire:click="submitPrompt" type="button" color="primary">
55+
{{ __('moox-prompts::prompts.ui.next_button') }}
56+
</x-filament::button>
4857
</div>
49-
@endif
5058

51-
<div style="margin-top: 1rem; display: flex; justify-content: flex-end;">
52-
<x-filament::button wire:click="submitPrompt" type="button" color="primary">
53-
{{ __('moox-prompts::prompts.ui.next_button') }}
54-
</x-filament::button>
59+
@if($currentStepOutput)
60+
<x-filament::section style="margin-top: 1rem;">
61+
<x-slot name="heading">
62+
{{ __('moox-prompts::prompts.ui.output_heading') }}
63+
</x-slot>
64+
<pre
65+
style="background-color: #111827; color: #4ade80; padding: 1rem; border-radius: 0.25rem; overflow: auto; font-size: 0.875rem; max-height: 400px;">{{ $currentStepOutput }}</pre>
66+
</x-filament::section>
67+
@endif
5568
</div>
56-
57-
@if($currentStepOutput)
58-
<x-filament::section style="margin-top: 1rem;">
59-
<x-slot name="heading">
60-
{{ __('moox-prompts::prompts.ui.output_heading') }}
61-
</x-slot>
62-
<pre
63-
style="background-color: #111827; color: #4ade80; padding: 1rem; border-radius: 0.25rem; overflow: auto; font-size: 0.875rem; max-height: 400px;">{{ $currentStepOutput }}</pre>
64-
</x-filament::section>
65-
@endif
6669
@else
6770
<x-filament::section>
6871
<x-slot name="heading">

0 commit comments

Comments
 (0)