pascualmg / symfony-command-ui
Web UI + API to execute Symfony console commands from the browser or any AI agent. Auto-discovers commands, dynamic forms, NDJSON streaming terminal.
Package info
github.com/pascualmg/symfony-command-ui
Language:JavaScript
Type:symfony-bundle
pkg:composer/pascualmg/symfony-command-ui
Requires
- php: >=7.4
- symfony/framework-bundle: ^4.4 || ^5.4 || ^6.0 || ^7.0
- symfony/http-foundation: ^4.4 || ^5.4 || ^6.0 || ^7.0
- symfony/process: ^4.4 || ^5.4 || ^6.0 || ^7.0
- symfony/routing: ^4.4 || ^5.4 || ^6.0 || ^7.0
README
Web UI + API to execute Symfony console commands from the browser — or from any AI agent.
Drop this bundle into any Symfony project (5.4, 6.x, 7.x) and get:
- A web dashboard with independent cards for each command (form + terminal)
- Real-time streaming output via NDJSON protocol
- Auto-discovery of commands from your
InputDefinition— zero manual config - An AI-ready API that any LLM, agent, or MCP server can use to operate your app
composer require pascualmg/symfony-command-ui
Architecture
┌─────────────────────────────────┐
│ YOUR SYMFONY APP │
│ │
│ src/Command/ │
│ app:users:sync │
│ app:payments:process │
│ app:reports:generate │
│ ... │
└──────────┬──────────────────────┘
│
bin/console list --format=json
│
┌────────────────────────────▼────────────────────────────┐
│ symfony-command-ui bundle │
│ │
│ GET /commands ─── auto-discovery ─── whitelist filter │
│ │ │
│ ▼ │
│ JSON config ──► <symfony-command> Web Component │
│ ┌──────────────────────────┐ │
│ │ ┌──────────────────────┐ │ │
│ │ │ app:users:sync │ │ │
│ │ │ [--limit ▼] [--dry ☐]│ │ │
│ │ │ [Run] [Copy] [Clear] │ │ │
│ │ │ ░░░ terminal ░░░░░░░ │ │ │
│ │ └──────────────────────┘ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ app:payments:process │ │ │
│ │ │ [--gateway ▼] ... │ │ │
│ │ │ [Run] [Copy] [Clear] │ │ │
│ │ │ ░░░ terminal ░░░░░░░ │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────┘ │
│ │
│ POST /execute ─── Process ─── NDJSON stream ──► browser│
└─────────────────────────────────────────────────────────┘
│ │
AI agents Humans
(HTTP + JSON) (browser UI)
Why this matters
For humans
You maintain a Symfony app with 20+ console commands. Some you run daily, some monthly, some only when debugging. Today you SSH into the server, remember the exact syntax, type it out.
With this bundle: open a URL, click Run. Each command is an independent card with its own form and terminal. Outputs persist — run Stats while Generate JWT keeps its result.
For AI agents
Your Symfony commands encapsulate business logic: process payments, sync users, generate reports, manage subscriptions. This bundle turns them into an HTTP API that any agent can use:
┌──────────────┐ ┌───────────────────┐ ┌──────────────┐
│ AI Agent │── GET ─►│ /commands │── JSON ─►│ "I can run │
│ (Claude, │ │ Auto-discovery │ │ these 5 │
│ GPT, etc.) │ └───────────────────┘ │ commands" │
│ │ └──────────────┘
│ │ ┌───────────────────┐ ┌──────────────┐
│ │── POST ►│ /execute │─ NDJSON ►│ Streaming │
│ │ │ {command, opts} │ │ output │
└──────────────┘ └───────────────────┘ └──────────────┘
This is essentially an MCP-compatible endpoint for your Symfony application. Any AI agent that can make HTTP calls can now operate your app's business logic through your existing console commands.
Quick start
1. Install
composer require pascualmg/symfony-command-ui
2. Register the bundle
// config/bundles.php return [ // ... Pascualmg\SymfonyCommandUI\SymfonyCommandUIBundle::class => ['all' => true], ];
3. Import routes
# config/routes/symfony_command_ui.yaml symfony_command_ui: resource: '@SymfonyCommandUIBundle/Resources/config/routes.php' prefix: /symfony-console
4. Configure your commands
# config/packages/symfony_command_ui.yaml symfony_command_ui: route_prefix: /symfony-console allowed_commands: - app:users:list - app:payments:process - app:reports:generate overrides: app:payments:process: --gateway: [stripe, paypal, braintree] --limit: [10, 50, 100, 500]
5. Open your browser
https://your-app.com/symfony-console
That's it. Your commands are auto-discovered and ready to use.
How it works
Request flow
Browser opens /symfony-console
│
▼
GET /commands ─────────────────────────────────────────────┐
│ │
│ Backend runs: php bin/console list --format=json│
│ Filters by allowed_commands whitelist │
│ Merges config overrides (dropdowns) │
│ Returns JSON array of commands │
│ │
▼ │
<symfony-command> Web Component │
│ │
│ Renders one card per command │
│ Each card: name + description + form + terminal │
│ │
│ User clicks [Run] on a card │
▼ │
POST /execute ──────────────────────────────────────────────┤
│ │
│ Backend validates command ∈ whitelist │
│ Runs: php bin/console {command} {options} │
│ Streams stdout as NDJSON (line by line) │
│ │
▼ │
Terminal shows output in real-time │
│ │
│ {"type":"line","text":"Processing..."} │
│ {"type":"line","text":"Done: 95 items"} │
│ {"type":"complete","exitCode":0,"duration":"2s"} │
│ │
▼ │
[Copy] button copies clean output (no timestamps, no chrome)│
────────────────────────────────────────────────────────────┘
Auto-discovery
The bundle runs php bin/console list --format=json via Symfony\Component\Process and filters by your whitelist. Each command's InputDefinition is translated automatically:
| Symfony InputDefinition | JSON value | UI element |
|---|---|---|
InputOption::VALUE_NONE (flag) |
false |
Checkbox (unchecked) |
InputOption::VALUE_REQUIRED with default |
"default value" |
Text input (pre-filled) |
InputOption::VALUE_REQUIRED without default |
"" |
Text input (empty) |
InputArgument required |
"" |
Text input (empty) |
| Override with array values | ["a", "b", "c"] |
Dropdown select |
Adding a new command = adding one line to allowed_commands. The UI generates itself.
Want a dropdown for a specific option? Add it to overrides. Everything else is auto-discovered.
NDJSON streaming protocol
When you execute a command, the backend streams each line of stdout as a JSON object (Newline-Delimited JSON):
Content-Type: application/x-ndjson
X-Accel-Buffering: no
{"type":"line","text":"Processing batch 1..."}
{"type":"line","text":"[OK] 95 items processed"}
{"type":"batch","batch":1,"processed":95,"errors":2}
{"type":"complete","exitCode":0,"duration":"2.3s"}
The terminal renders each type with a different color:
| Type | Color | Meaning |
|---|---|---|
line |
Gray | Standard output |
batch |
Blue | Batch progress |
complete (exit 0) |
Green | Success |
complete (exit != 0) |
Red | Failure |
No WebSocket. No Server-Sent Events. Just fetch() + ReadableStream + TextDecoder. Works everywhere.
Card layout
Each command renders as an independent card:
┌─────────────────────────────────────────────────────────┐
│ app:payments:process │
│ Process pending payments │
│ │
│ gateway: [stripe ▼] limit: [100 ▼] ☐ dry-run │
│ [Run] [Copy] [Clear] │
│ ┌────────────────────────────────────────────────────┐ │
│ │ $ bin/console app:payments:process --gateway=stripe│ │
│ │ Processing batch 1... 50 payments │ │
│ │ Processing batch 2... 48 payments │ │
│ │ [OK] exit=0 duration=3.2s │ │
│ └────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ app:reports:generate │
│ Generate monthly reports │
│ │
│ format: [pdf ▼] ☐ json │
│ [Run] [Copy] [Clear] │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Ready │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
- Each card has its own terminal — outputs persist across commands
- Copy copies clean output (no timestamps, no
[OK] exit=...chrome) - Clear resets only that card's terminal
- Cards can run simultaneously (each is independent)
Web Component
A single <symfony-command> custom element with Shadow DOM. Zero dependencies. Served by the bundle as a static asset — no npm, no webpack, no build step.
<!-- Auto-discovery mode (recommended) --> <symfony-command endpoint="/symfony-console"></symfony-command> <!-- Static mode (provide commands directly) --> <symfony-command endpoint="/symfony-console" commands='[{"command":"app:example","label":"Example","config":{"--verbose":false}}]'> </symfony-command>
Theming
Override CSS custom properties to match your app:
/* Dark theme (default) */ symfony-command { --cmd-bg: #0a0a1a; --cmd-surface: #1a1a2e; --cmd-text: #e0e0e0; --cmd-accent: #4ecca3; } /* Light theme */ symfony-command { --cmd-bg: #ffffff; --cmd-surface: #f8f9fa; --cmd-text: #212529; --cmd-accent: #0d6efd; --cmd-success: #198754; --cmd-error: #dc3545; --cmd-batch: #0d6efd; --cmd-info: #6c757d; --cmd-border: rgba(0,0,0,0.12); --cmd-font: 'SF Mono', 'Fira Code', monospace; --cmd-radius: 6px; }
Custom events
const el = document.querySelector('symfony-command'); el.addEventListener('command-started', e => console.log('Started:', e.detail)); el.addEventListener('command-completed', e => console.log('Done:', e.detail)); el.addEventListener('command-error', e => console.log('Error:', e.detail));
Using with AI agents
Discovery + Execute (any HTTP client)
import requests, json BASE = "https://app.com/symfony-console" # 1. Discover available commands commands = requests.get(f"{BASE}/commands").json() for cmd in commands: print(f" {cmd['command']}: {cmd['label']}") # app:users:sync: Synchronize users with external services # app:payments:process: Process pending payments # ... # 2. Execute a command response = requests.post(f"{BASE}/execute", json={"command": "app:users:sync", "options": {"--limit": 100, "--dry-run": True}}, stream=True ) # 3. Stream output for line in response.iter_lines(): event = json.loads(line) print(event.get("text", "")) if event["type"] == "complete": print(f"Exit code: {event['exitCode']}, Duration: {event['duration']}")
With Claude Code / MCP
Wrap these two endpoints as MCP tools and your AI assistant can operate your Symfony app conversationally:
You: "Check if there are any pending payments over $1000"
Claude: GET /commands → finds
app:payments:listPOST /execute with{"command":"app:payments:list","options":{"--min-amount":1000,"--status":"pending","--json":true}}Claude: "There are 3 pending payments over $1000: #4521 ($2,340), #4523 ($1,100), #4529 ($5,600). Want me to process them?"
You: "Process them with dry-run first"
Claude: POST /execute with
{"command":"app:payments:process","options":{"--ids":"4521,4523,4529","--dry-run":true}}Claude: "Dry run complete. All 3 would process successfully. Total: $9,040. Run for real?"
MCP tool definition example
{
"name": "symfony_console",
"description": "Execute Symfony console commands on the application server",
"input_schema": {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["discover", "execute"]},
"command": {"type": "string"},
"options": {"type": "object"}
}
}
}
Security
This bundle does NOT include authentication. You must protect the routes yourself:
# Option 1: Symfony security security: access_control: - { path: ^/symfony-console, roles: ROLE_ADMIN } # Option 2: IP whitelist in your web server (nginx/apache) # Option 3: VPN-only access # Option 4: Your own middleware / event subscriber
The bundle provides allowed_commands as a whitelist — only commands in this list can be discovered and executed. But route-level access control is your responsibility.
Configuration reference
symfony_command_ui: # URL prefix for all bundle endpoints route_prefix: /symfony-console # default # Whitelist: only these commands can be discovered and executed allowed_commands: - app:my:command - app:another:command # Override auto-discovered options with rich UI elements # Arrays become dropdown selects instead of text inputs overrides: app:my:command: --option-name: [value1, value2, value3]
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | {prefix}/ |
HTML page with <symfony-command> Web Component |
| GET | {prefix}/asset/symfony-command.js |
The Web Component JS file |
| GET | {prefix}/commands |
Auto-discovered command list (JSON) |
| POST | {prefix}/execute |
Execute a command (NDJSON stream) |
Requirements
- PHP >= 7.4
- Symfony 5.4, 6.x, or 7.x
symfony/processcomponent (included in most Symfony installs)
License
MIT — Pascual Munoz Galian