feat(console): add agent:db command for querying local and prod DB (#522)

## What

Adds an `agent:db` artisan command so agents (and humans) can run read
queries against the local or production database from the CLI.

```bash
php artisan agent:db "select id, email from users limit 5"        # local, JSON (default)
php artisan agent:db --format=table "select count(*) from transactions"  # console table
php artisan agent:db --prod "select count(*) from users"          # production
```

### Options
- `--format=json` (default) — pretty-printed JSON
- `--format=table` — classic console table
- `--prod` — target the production connection (new `prod` connection
backed by `PROD_DB_URL`)

## Notes
- Read-only: uses `DB::select()`, so it won't run
`INSERT`/`UPDATE`/`DELETE`. Query errors are caught and reported.
- Adds a `querying-the-database` skill documenting the command
(auto-activates on prod/DB questions).
- De-duplicates the skills tree: `.agents/skills` is now the canonical
directory and `.claude/skills` is a symlink to it (previously both were
tracked as identical copies).

## Test plan
- [x] `php artisan test --filter=AgentDatabaseCommand` (4 passing: json,
table, invalid format, query error)
- [x] `vendor/bin/pint`
This commit is contained in:
Víctor Falcón 2026-06-12 18:35:14 +02:00 committed by GitHub
parent 08bb42979a
commit d2a4412118
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 163 additions and 1322 deletions

View File

@ -0,0 +1,56 @@
---
name: querying-the-database
description: "Query the local or production database from the CLI via the `agent:db` artisan command. Activates when the user asks to inspect, count, look up, or run a query against the database; asks 'how many X', 'what's in prod', 'check the prod DB', 'query the database'; or mentions production data, the prod database, or running SQL."
metadata:
author: whisper-money
---
# Querying the Database
## When to Apply
Activate this skill whenever you need to read data from the database to answer a
question — especially anything about **production** data. Prefer this command over
the `tinker`, `database-query`, or `database-schema` Boost tools when the user asks
about prod, since those default to the local connection.
## The `agent:db` command
Runs a read query (`SELECT`, `SHOW`, `DESCRIBE`, etc.) and prints the result.
```bash
php artisan agent:db "<query>"
```
### Options
- `--format=json` (default) — pretty-printed JSON, best for parsing the result yourself.
- `--format=table` — classic console table, best when showing the result to the user.
- `--prod` — run against the **production** database (the `prod` connection backed by
`PROD_DB_URL`). Without this flag the query runs against the local DB.
### Examples
```bash
# Local, JSON (default)
php artisan agent:db "select id, email from users limit 5"
# Local, human-readable table
php artisan agent:db --format=table "select count(*) as total from transactions"
# Production
php artisan agent:db --prod "select count(*) from users"
php artisan agent:db --prod --format=table "select status, count(*) from subscriptions group by status"
```
## Guidelines
- **Read-only**: the command uses `DB::select()`, so only read statements work.
It will not run `INSERT`/`UPDATE`/`DELETE`. Never attempt to mutate prod data this way.
- **Be careful with `--prod`**: this is live customer data. Only run prod queries the
user explicitly asked for, keep them scoped (add `LIMIT`, filter by id), and never
dump large or sensitive datasets unprompted. This app is privacy-first.
- Use `--format=json` when you need to read the values to continue working; use
`--format=table` when presenting results back to the user.
- Inspect schema first with `database-schema` (local) when you're unsure of column
names before writing a query.

1
.claude/skills Symbolic link
View File

@ -0,0 +1 @@
../.agents/skills

View File

@ -1,108 +0,0 @@
---
name: cashier-stripe-development
description: "Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues."
license: MIT
metadata:
author: laravel
---
# Cashier Stripe Development
## When to Apply
Activate this skill when:
- Installing or configuring Laravel Cashier Stripe
- Setting up subscriptions, trials, quantities, or plan swapping
- Handling webhooks or SCA/3DS payment failures
- Working with Stripe Checkout, invoices, or charges
- Testing billing scenarios with Stripe test cards or tokens
## Documentation
Use `search-docs` for detailed Cashier patterns and documentation covering subscriptions, webhooks, Stripe Checkout, invoices, payment methods, and testing.
For deeper guidance on specific topics, read the relevant reference file before implementing:
- `references/subscriptions.md` covers subscription creation, status checks, swapping, trials, quantities, and multiple products
- `references/webhooks.md` covers webhook setup, custom handlers, CSRF exclusion, and local development with the Stripe CLI
- `references/testing.md` covers Stripe test cards, payment method tokens, and feature test patterns
## Basic Usage
### Installation
```bash
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
php artisan vendor:publish --tag="cashier-config"
```
### Environment Variables
```
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
CASHIER_CURRENCY=usd
CASHIER_CURRENCY_LOCALE=en_US
```
### Billable Model
<!-- Add Billable Trait -->
```php
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
```
For a non-User model, register it in a service provider:
<!-- Custom Billable Model -->
```php
// In AppServiceProvider::boot()
Cashier::useCustomerModel(Team::class);
```
### Creating a Subscription
<!-- Create Subscription -->
```php
use Laravel\Cashier\Exceptions\IncompletePayment;
try {
$user->newSubscription('default', 'price_xxxx')->create($paymentMethodId);
} catch (IncompletePayment $e) {
return redirect()->route('cashier.payment', [$e->payment->id, 'redirect' => route('home')]);
}
```
Always wrap subscription creation in a try/catch for `IncompletePayment`. When a card requires 3DS authentication, Cashier throws this exception. The `cashier.payment` route is auto-registered and handles the confirmation flow.
## Verification
1. Run migrations and confirm `stripe_id`, `pm_type`, `pm_last_four`, and `trial_ends_at` columns exist on the billable model table
2. Test the webhook endpoint with `stripe listen --forward-to localhost/stripe/webhook` if you use the default path, or swap `stripe` for your configured `CASHIER_PATH`
3. Confirm `$user->subscribed('default')` returns the expected value for active and incomplete subscriptions
## Common Pitfalls
- The migration publish tag is `cashier-migrations`, not `cashier`. Running `migrate` before publishing results in missing columns and tables.
- `CASHIER_CURRENCY` must be set explicitly. It defaults to USD, which silently breaks non-US apps.
- The Stripe CLI generates its own webhook signing secret. It is different from the Dashboard endpoint secret. Using the wrong one causes signature verification failures.
- The webhook route must be excluded from CSRF verification using your configured `cashier.path`. If you change `CASHIER_PATH` from `stripe` to `billing`, exclude `billing/*`, not `stripe/*`.
- `canceled()` returns true as soon as `cancel()` is called, but the user still has access during the grace period. Use `ended()` to confirm access is fully revoked.
- `subscribed()` returns true during the grace period even though the subscription is canceled.
- `subscribed()` returns false for `incomplete` and `past_due` subscriptions by default.
- Prices cannot be swapped and quantity cannot be updated while a subscription has an incomplete payment.
- When extending `WebhookController`, call `Cashier::ignoreRoutes()` in a service provider and re-register both `cashier.payment` and `cashier.webhook` under the configured `cashier.path`.
- Use `Cashier::useCustomerModel()` in a service provider to set a custom billable model. There is no `CASHIER_MODEL` env var.
- `trial_ends_at` is a local database column synced via webhooks. It will be stale if webhooks are not configured in production.
- In MySQL, the `stripe_id` column must use `utf8_bin` collation to avoid case-sensitivity issues.
- `noProrate()` has no effect when combined with `swapAndInvoice()`. That method always prorates.
- Methods like `withPromotionCode()` require the Stripe API ID such as `promo_xxxx`, not the customer-facing code. Use `findPromotionCode()` to resolve a code to its ID.
- Always use `search-docs` for the latest Cashier documentation rather than relying on this skill alone.

View File

@ -1,108 +0,0 @@
# Subscriptions Reference
Use `search-docs` for authoritative documentation on subscriptions.
## Status Checks
| Method | Returns true when |
|---|---|
| `$user->subscribed('default')` | Active or on grace period |
| `->onTrial()` | Trial period active |
| `->onGracePeriod()` | Canceled, period not yet ended |
| `->canceled()` | `ends_at` is set, may still have access |
| `->ended()` | Canceled and grace period expired |
| `->incomplete()` | Awaiting SCA/3DS confirmation |
| `->pastDue()` | Payment overdue |
| `->recurring()` | Active and not on trial |
Check by product or price:
```php
$user->subscribedToProduct('prod_premium', 'default');
$user->subscribedToPrice('price_monthly', 'default');
```
## Swapping Plans
```php
$user->subscription('default')->swap('price_new');
$user->subscription('default')->noProrate()->swap('price_new');
$user->subscription('default')->swapAndInvoice('price_new');
$user->subscription('default')->skipTrial()->swap('price_new');
```
## Quantity
```php
$user->subscription('default')->incrementQuantity();
$user->subscription('default')->decrementQuantity();
$user->subscription('default')->updateQuantity(10);
$user->subscription('default')->noProrate()->updateQuantity(10);
```
## Trials
```php
$user->newSubscription('default', 'price_xxxx')
->trialDays(14)
->create($paymentMethodId);
$subscription->extendTrial(now()->addDays(7));
```
## Multiple Products on One Subscription
```php
$user->newSubscription('default', ['price_monthly', 'price_chat'])
->quantity(5, 'price_chat')
->create($paymentMethod);
$user->subscription('default')->addPrice('price_chat');
$user->subscription('default')->removePrice('price_chat');
$user->subscription('default')->swap(['price_pro', 'price_chat']);
```
## Multiple Subscriptions
```php
$user->newSubscription('swimming', 'price_swimming_monthly')->create($pm);
$user->newSubscription('gym', 'price_gym_monthly')->create($pm);
$user->subscription('swimming')->swap('price_swimming_yearly');
$user->subscription('gym')->cancel();
```
## Cancellation and Resumption
```php
$user->subscription('default')->cancel(); // At end of billing period
$user->subscription('default')->cancelNow(); // Immediately
$user->subscription('default')->resume(); // During grace period only
```
## Incomplete Payment Handling
```php
if ($user->hasIncompletePayment('default')) {
$paymentId = $user->subscription('default')->latestPayment()->id;
return redirect()->route('cashier.payment', $paymentId);
}
```
Opt out of default deactivation behavior:
```php
Cashier::keepPastDueSubscriptionsActive();
Cashier::keepIncompleteSubscriptionsActive();
```
## Metered / Usage-Based Billing
```php
$user->newSubscription('default')
->meteredPrice('price_metered')
->create($paymentMethodId);
$user->reportMeterEvent('emails-sent');
$user->reportMeterEvent('emails-sent', quantity: 15);
```

View File

@ -1,52 +0,0 @@
# Testing Reference
Use `search-docs` for authoritative documentation on testing Cashier integrations.
## Test Cards and Tokens
Use card numbers for browser-based flows (Stripe.js / Checkout). Use `pm_card_*` tokens directly in feature tests that call the Stripe API.
| Card Number | Token | Behavior |
|---|---|---|
| `4242 4242 4242 4242` | `pm_card_visa` | Succeeds immediately |
| `4000 0025 0000 3155` | `pm_card_threeDSecure2Required` | Requires SCA/3DS |
| `4000 0027 6000 3184` | `pm_card_authenticationRequired` | Requires authentication |
| `4000 0000 0000 9995` | `pm_card_chargeDeclinedInsufficientFunds` | Declined, insufficient funds |
| `4000 0000 0000 0002` | `pm_card_chargeDeclined` | Declined |
Use expiry `12/34`, any CVC, any ZIP for card number inputs.
## Feature Test Example
Feature tests that hit the real Stripe test API use `pm_card_*` tokens:
```php
public function test_user_can_subscribe(): void
{
$user = User::factory()->create();
$user->newSubscription('default', 'price_xxxx')
->create('pm_card_visa');
$this->assertTrue($user->subscribed('default'));
}
public function test_incomplete_payment_is_handled(): void
{
$user = User::factory()->create();
try {
$user->newSubscription('default', 'price_xxxx')
->create('pm_card_threeDSecure2Required');
} catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) {
$this->assertTrue($user->subscription('default')->incomplete());
}
}
```
## Setup Notes
- Use Stripe test mode keys (`sk_test_...`, `pk_test_...`) in your test environment
- Cashier does not ship a global `fake()` helper. Tests hit the real Stripe test API by default.
- Refer to `tests/Feature/` in the Cashier package itself for integration test patterns covering subscription creation, payment methods, and webhook handling
- Use `search-docs` for current guidance on mocking Stripe HTTP calls or using Stripe's test clock feature for time-sensitive scenarios

View File

@ -1,132 +0,0 @@
# Webhooks Reference
Use `search-docs` for authoritative documentation on webhooks.
## Auto-Registered Routes
Cashier registers two routes automatically under the `cashier.path` prefix (`config('cashier.path')`, default `stripe`):
- `POST /{cashier.path}/webhook` named `cashier.webhook`
- `GET /{cashier.path}/payment/{id}` named `cashier.payment`
With the default config these are `/stripe/webhook` and `/stripe/payment/{id}`. If you set `CASHIER_PATH=billing`, they become `/billing/webhook` and `/billing/payment/{id}`.
## CSRF Exclusion
Use the same path prefix you configured for Cashier here. If `CASHIER_PATH=billing`, exclude `billing/*` instead of `stripe/*`.
**Laravel 11+ (`bootstrap/app.php`, default path example):**
```php
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['stripe/*']);
})
```
**Laravel 10 (`app/Http/Middleware/VerifyCsrfToken.php`, default path example):**
```php
protected $except = [
'stripe/*',
];
```
## Local Development with Stripe CLI
If you changed `cashier.path`, forward Stripe CLI events to that URL instead of `/stripe/webhook`.
```bash
stripe login
stripe listen --forward-to your-app.test/stripe/webhook
stripe trigger invoice.payment_succeeded
```
The CLI outputs a `whsec_...` signing secret specific to that session. Set it as `STRIPE_WEBHOOK_SECRET` locally. It is not the same as the Dashboard endpoint secret.
## Registering Events in the Stripe Dashboard
Use the Artisan command to create the endpoint automatically with all required events:
```bash
php artisan cashier:webhook
```
Cashier's `cashier:webhook` command registers these events by default:
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `customer.updated` / `customer.deleted`
- `invoice.payment_action_required`
- `invoice.payment_succeeded`
- `payment_method.automatically_updated`
Cashier's `WebhookController` has built-in handlers for all of the above except `invoice.payment_succeeded`. For renewal hooks, prefer `WebhookReceived` / `WebhookHandled` listeners unless you intentionally add your own controller method.
## Custom Handlers: Extending WebhookController
Method name pattern: `handle` + StudlyCase of event type with dots replaced by underscores.
`customer.subscription.created` becomes `handleCustomerSubscriptionCreated`.
```php
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class StripeWebhookController extends CashierController
{
public function handleCustomerSubscriptionCreated(array $payload)
{
$response = parent::handleCustomerSubscriptionCreated($payload);
// your logic after Cashier syncs the subscription
return $response;
}
}
```
If you add a method for an event Cashier does not handle internally, such as `invoice.payment_succeeded`, do not call `parent::handle...()` unless the base controller actually defines that method.
In a service provider, disable auto-registration and re-register both Cashier routes so the incomplete-payment flow and `cashier:webhook` command keep working:
```php
Cashier::ignoreRoutes();
```
```php
// routes/web.php
use App\Http\Controllers\StripeWebhookController;
use Illuminate\Support\Facades\Route;
use Laravel\Cashier\Http\Controllers\PaymentController;
Route::prefix(config('cashier.path'))
->name('cashier.')
->group(function () {
Route::get('payment/{id}', [PaymentController::class, 'show'])->name('payment');
Route::post('webhook', [StripeWebhookController::class, 'handleWebhook'])->name('webhook');
});
```
Keep the `cashier.webhook` route name unless you plan to pass `--url` explicitly to `php artisan cashier:webhook`.
## Custom Handlers: Listening to Events
The simpler option when you do not need to replace Cashier's internal logic, or when you want to react to events such as `invoice.payment_succeeded` that Cashier does not process itself:
```php
use Laravel\Cashier\Events\WebhookReceived;
use Laravel\Cashier\Events\WebhookHandled;
// WebhookReceived fires for every event before Cashier processes it
// WebhookHandled fires after Cashier processes it
Event::listen(WebhookReceived::class, function (WebhookReceived $event) {
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// handle renewal
}
});
```
## Signature Verification
`VerifyWebhookSignature` middleware is applied automatically when `cashier.webhook.secret` is set. No extra wiring is needed.

View File

@ -1,128 +0,0 @@
---
name: fortify-development
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
---
# Laravel Fortify Development
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
## Documentation
Use `search-docs` for detailed Laravel Fortify patterns and documentation.
## Usage
- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.
## Available Features
Enable in `config/fortify.php` features array:
- `Features::registration()` - User registration
- `Features::resetPasswords()` - Password reset via email
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
- `Features::updateProfileInformation()` - Profile updates
- `Features::updatePasswords()` - Password changes
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes
> Use `search-docs` for feature configuration options and customization patterns.
## Setup Workflows
### Two-Factor Authentication Setup
```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
```
> Use `search-docs` for TOTP implementation and recovery code handling patterns.
### Email Verification Setup
```
- [ ] Enable emailVerification feature in config
- [ ] Implement MustVerifyEmail interface on User model
- [ ] Set up verifyEmailView callback
- [ ] Add verified middleware to protected routes
- [ ] Test verification email flow
```
> Use `search-docs` for MustVerifyEmail implementation patterns.
### Password Reset Setup
```
- [ ] Enable resetPasswords feature in config
- [ ] Set up requestPasswordResetLinkView callback
- [ ] Set up resetPasswordView callback
- [ ] Define password.reset named route (if views disabled)
- [ ] Test reset email and link flow
```
> Use `search-docs` for custom password reset flow patterns.
### SPA Authentication Setup
```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```
> Use `search-docs` for integration and SPA authentication patterns.
#### Two-Factor Authentication in SPA Mode
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
```json
{
"two_factor": true
}
```
## Best Practices
### Custom Authentication Logic
Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.
### Registration Customization
Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.
### Rate Limiting
Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.
## Key Endpoints
| Feature | Method | Endpoint |
|------------------------|----------|---------------------------------------------|
| Login | POST | `/login` |
| Logout | POST | `/logout` |
| Register | POST | `/register` |
| Password Reset Request | POST | `/forgot-password` |
| Password Reset | POST | `/reset-password` |
| Email Verify Notice | GET | `/email/verify` |
| Resend Verification | POST | `/email/verification-notification` |
| Password Confirm | POST | `/user/confirm-password` |
| Enable 2FA | POST | `/user/two-factor-authentication` |
| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` |
| 2FA Challenge | POST | `/two-factor-challenge` |
| Get QR Code | GET | `/user/two-factor-qr-code` |
| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |

View File

@ -1,361 +0,0 @@
---
name: inertia-react-development
description: "Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation."
license: MIT
metadata:
author: laravel
---
# Inertia React Development
## When to Apply
Activate this skill when:
- Creating or modifying React page components for Inertia
- Working with forms in React (using `<Form>` or `useForm`)
- Implementing client-side navigation with `<Link>` or `router`
- Using v2 features: deferred props, prefetching, WhenVisible, InfiniteScroll, once props, flash data, or polling
- Building React-specific features with the Inertia protocol
## Documentation
Use `search-docs` for detailed Inertia v2 React patterns and documentation.
## Basic Usage
### Page Components Location
React page components should be placed in the `resources/js/pages` directory.
### Page Component Structure
<!-- Basic React Page Component -->
```react
export default function UsersIndex({ users }) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
</div>
)
}
```
## Client-Side Navigation
### Basic Link Component
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
<!-- Inertia React Navigation -->
```react
import { Link, router } from '@inertiajs/react'
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
<Link href={`/users/${user.id}`}>View User</Link>
```
### Link with Method
<!-- Link with POST Method -->
```react
import { Link } from '@inertiajs/react'
<Link href="/logout" method="post" as="button">
Logout
</Link>
```
### Prefetching
Prefetch pages to improve perceived performance:
<!-- Prefetch on Hover -->
```react
import { Link } from '@inertiajs/react'
<Link href="/users" prefetch>
Users
</Link>
```
### Programmatic Navigation
<!-- Router Visit -->
```react
import { router } from '@inertiajs/react'
function handleClick() {
router.visit('/users')
}
// Or with options
router.visit('/users', {
method: 'post',
data: { name: 'John' },
onSuccess: () => console.log('Success!'),
})
```
## Form Handling
### Form Component (Recommended)
The recommended way to build forms is with the `<Form>` component:
<!-- Form Component Example -->
```react
import { Form } from '@inertiajs/react'
export default function CreateUser() {
return (
<Form action="/users" method="post">
{({ errors, processing, wasSuccessful }) => (
<>
<input type="text" name="name" />
{errors.name && <div>{errors.name}</div>}
<input type="email" name="email" />
{errors.email && <div>{errors.email}</div>}
<button type="submit" disabled={processing}>
{processing ? 'Creating...' : 'Create User'}
</button>
{wasSuccessful && <div>User created!</div>}
</>
)}
</Form>
)
}
```
### Form Component With All Props
<!-- Form Component Full Example -->
```react
import { Form } from '@inertiajs/react'
<Form action="/users" method="post">
{({
errors,
hasErrors,
processing,
progress,
wasSuccessful,
recentlySuccessful,
clearErrors,
resetAndClearErrors,
defaults,
isDirty,
reset,
submit
}) => (
<>
<input type="text" name="name" defaultValue={defaults.name} />
{errors.name && <div>{errors.name}</div>}
<button type="submit" disabled={processing}>
{processing ? 'Saving...' : 'Save'}
</button>
{progress && (
<progress value={progress.percentage} max="100">
{progress.percentage}%
</progress>
)}
{wasSuccessful && <div>Saved!</div>}
</>
)}
</Form>
```
### Form Component Reset Props
The `<Form>` component supports automatic resetting:
- `resetOnError` - Reset form data when the request fails
- `resetOnSuccess` - Reset form data when the request succeeds
- `setDefaultsOnSuccess` - Update default values on success
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
<!-- Form with Reset Props -->
```react
import { Form } from '@inertiajs/react'
<Form
action="/users"
method="post"
resetOnSuccess
setDefaultsOnSuccess
>
{({ errors, processing, wasSuccessful }) => (
<>
<input type="text" name="name" />
{errors.name && <div>{errors.name}</div>}
<button type="submit" disabled={processing}>
Submit
</button>
</>
)}
</Form>
```
Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
### `useForm` Hook
For more programmatic control or to follow existing conventions, use the `useForm` hook:
<!-- useForm Hook Example -->
```react
import { useForm } from '@inertiajs/react'
export default function CreateUser() {
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
email: '',
password: '',
})
function submit(e) {
e.preventDefault()
post('/users', {
onSuccess: () => reset('password'),
})
}
return (
<form onSubmit={submit}>
<input
type="text"
value={data.name}
onChange={e => setData('name', e.target.value)}
/>
{errors.name && <div>{errors.name}</div>}
<input
type="email"
value={data.email}
onChange={e => setData('email', e.target.value)}
/>
{errors.email && <div>{errors.email}</div>}
<input
type="password"
value={data.password}
onChange={e => setData('password', e.target.value)}
/>
{errors.password && <div>{errors.password}</div>}
<button type="submit" disabled={processing}>
Create User
</button>
</form>
)
}
```
## Inertia v2 Features
### Deferred Props
Use deferred props to load data after initial page render:
<!-- Deferred Props with Empty State -->
```react
export default function UsersIndex({ users }) {
// users will be undefined initially, then populated
return (
<div>
<h1>Users</h1>
{!users ? (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
) : (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
)
}
```
### Polling
Automatically refresh data at intervals:
<!-- Polling Example -->
```react
import { router } from '@inertiajs/react'
import { useEffect } from 'react'
export default function Dashboard({ stats }) {
useEffect(() => {
const interval = setInterval(() => {
router.reload({ only: ['stats'] })
}, 5000) // Poll every 5 seconds
return () => clearInterval(interval)
}, [])
return (
<div>
<h1>Dashboard</h1>
<div>Active Users: {stats.activeUsers}</div>
</div>
)
}
```
### WhenVisible
Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:
<!-- WhenVisible Example -->
```react
import { WhenVisible } from '@inertiajs/react'
export default function Dashboard({ stats }) {
return (
<div>
<h1>Dashboard</h1>
{/* stats prop is loaded only when this section scrolls into view */}
<WhenVisible data="stats" buffer={200} fallback={<div className="animate-pulse">Loading stats...</div>}>
{({ fetching }) => (
<div>
<p>Total Users: {stats.total_users}</p>
<p>Revenue: {stats.revenue}</p>
{fetching && <span>Refreshing...</span>}
</div>
)}
</WhenVisible>
</div>
)
}
```
## Server-Side Patterns
Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.
## Common Pitfalls
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
- Forgetting to add loading states (skeleton screens) when using deferred props
- Not handling the `undefined` state of deferred props before data loads
- Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`)
- Forgetting to check if `<Form>` component is available in your Inertia version

View File

@ -1,77 +0,0 @@
---
name: pennant-development
description: "Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems."
license: MIT
metadata:
author: laravel
---
# Pennant Features
## When to Apply
Activate this skill when:
- Creating or checking feature flags
- Managing feature rollouts
- Implementing A/B testing
## Documentation
Use `search-docs` for detailed Pennant patterns and documentation.
## Basic Usage
### Defining Features
<!-- Defining Features -->
```php
use Laravel\Pennant\Feature;
Feature::define('new-dashboard', function (User $user) {
return $user->isAdmin();
});
```
### Checking Features
<!-- Checking Features -->
```php
if (Feature::active('new-dashboard')) {
// Feature is active
}
// With scope
if (Feature::for($user)->active('new-dashboard')) {
// Feature is active for this user
}
```
### Blade Directive
<!-- Blade Directive -->
```blade
@feature('new-dashboard')
<x-new-dashboard />
@else
<x-old-dashboard />
@endfeature
```
### Activating / Deactivating
<!-- Activating Features -->
```php
Feature::activate('new-dashboard');
Feature::for($user)->activate('new-dashboard');
```
## Verification
1. Check feature flag is defined
2. Test with different scopes/users
## Common Pitfalls
- Forgetting to scope features for specific users/entities
- Not following existing naming conventions

View File

@ -1,157 +0,0 @@
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
```
## Pest 4 Features
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
```
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests

View File

@ -1,119 +0,0 @@
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
author: laravel
---
# Tailwind CSS Development
## Documentation
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
## Basic Usage
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
## Tailwind CSS v4 Specifics
- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
### CSS-First Configuration
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
<!-- CSS-First Config -->
```css
@theme {
--color-brand: oklch(0.72 0.11 178);
}
```
### Import Syntax
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```
### Replaced Utilities
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
## Spacing
Use `gap` utilities instead of margins for spacing between siblings:
<!-- Gap Utilities -->
```html
<div class="flex gap-8">
<div>Item 1</div>
<div>Item 2</div>
</div>
```
## Dark Mode
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Content adapts to color scheme
</div>
```
## Common Patterns
### Flexbox Layout
<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
<div>Left content</div>
<div>Right content</div>
</div>
```
### Grid Layout
<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
```
## Common Pitfalls
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode

View File

@ -1,80 +0,0 @@
---
name: wayfinder-development
description: "Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions."
license: MIT
metadata:
author: laravel
---
# Wayfinder Development
## Documentation
Use `search-docs` for detailed Wayfinder patterns and documentation.
## Quick Reference
### Generate Routes
Run after route changes if Vite plugin isn't installed:
```bash
php artisan wayfinder:generate --no-interaction
```
For form helpers, use `--with-form` flag:
```bash
php artisan wayfinder:generate --with-form --no-interaction
```
### Import Patterns
<!-- Controller Action Imports -->
```typescript
// Named imports for tree-shaking (preferred)...
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
// Named route imports...
import { show as postShow } from '@/routes/post'
```
### Common Methods
<!-- Wayfinder Methods -->
```typescript
// Get route object...
show(1) // { url: "/posts/1", method: "get" }
// Get URL string...
show.url(1) // "/posts/1"
// Specific HTTP methods...
show.get(1)
store.post()
update.patch(1)
destroy.delete(1)
// Form attributes for HTML forms...
store.form() // { action: "/posts", method: "post" }
// Query parameters...
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
```
## Wayfinder + Inertia
Use Wayfinder with the `<Form>` component:
<!-- Wayfinder Form (React) -->
```typescript
<Form {...store.form()}><input name="title" /></Form>
```
## Verification
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
2. Check TypeScript imports resolve correctly
3. Verify route URLs match expected paths
## Common Pitfalls
- Using default imports instead of named imports (breaks tree-shaking)
- Forgetting to regenerate after route changes
- Not using type-safe parameter objects for route model binding

View File

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Throwable;
class AgentDatabaseCommand extends Command
{
protected $signature = 'agent:db
{query : The SQL query to run}
{--format=json : Output format: "json" or "table"}
{--prod : Run against the production database connection}';
protected $description = 'Run a database query and return the result in the selected format';
public function handle(): int
{
$format = strtolower((string) $this->option('format'));
if (! in_array($format, ['json', 'table'], true)) {
$this->error('Invalid format. Use "json" or "table".');
return self::FAILURE;
}
$connection = $this->option('prod') ? 'prod' : config('database.default');
try {
$rows = array_map(
static fn (object $row): array => (array) $row,
DB::connection($connection)->select($this->argument('query')),
);
} catch (Throwable $exception) {
$this->error($exception->getMessage());
return self::FAILURE;
}
if ($format === 'json') {
$this->line((string) json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return self::SUCCESS;
}
if ($rows === []) {
$this->info('Empty result set.');
return self::SUCCESS;
}
$this->table(array_keys($rows[0]), $rows);
return self::SUCCESS;
}
}

View File

@ -83,6 +83,20 @@ return [
]) : [],
],
'prod' => [
'driver' => 'mysql',
'url' => env('PROD_DB_URL'),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),

View File

@ -0,0 +1,35 @@
<?php
use App\Models\User;
use function Pest\Laravel\artisan;
test('runs a query and returns json by default', function () {
$user = User::factory()->create(['email' => 'agent-db@example.com']);
artisan('agent:db', ['query' => "select email from users where id = '{$user->id}'"])
->expectsOutputToContain('agent-db@example.com')
->assertSuccessful();
});
test('renders the result as a table when requested', function () {
User::factory()->create(['email' => 'table-format@example.com']);
artisan('agent:db', [
'query' => "select email from users where email = 'table-format@example.com'",
'--format' => 'table',
])
->expectsOutputToContain('table-format@example.com')
->assertSuccessful();
});
test('rejects an unknown format', function () {
artisan('agent:db', ['query' => 'select 1', '--format' => 'xml'])
->expectsOutputToContain('Invalid format')
->assertFailed();
});
test('reports query errors gracefully', function () {
artisan('agent:db', ['query' => 'select * from non_existent_table'])
->assertFailed();
});