diff --git a/.agents/skills/querying-the-database/SKILL.md b/.agents/skills/querying-the-database/SKILL.md new file mode 100644 index 00000000..ab1ac5a1 --- /dev/null +++ b/.agents/skills/querying-the-database/SKILL.md @@ -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 "" +``` + +### 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. diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.claude/skills/cashier-stripe-development/SKILL.md b/.claude/skills/cashier-stripe-development/SKILL.md deleted file mode 100644 index 355e8a92..00000000 --- a/.claude/skills/cashier-stripe-development/SKILL.md +++ /dev/null @@ -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 - - -```php -use Laravel\Cashier\Billable; - -class User extends Authenticatable -{ - use Billable; -} -``` - -For a non-User model, register it in a service provider: - - -```php -// In AppServiceProvider::boot() -Cashier::useCustomerModel(Team::class); -``` - -### Creating a 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. \ No newline at end of file diff --git a/.claude/skills/cashier-stripe-development/references/subscriptions.md b/.claude/skills/cashier-stripe-development/references/subscriptions.md deleted file mode 100644 index ad7113ea..00000000 --- a/.claude/skills/cashier-stripe-development/references/subscriptions.md +++ /dev/null @@ -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); -``` \ No newline at end of file diff --git a/.claude/skills/cashier-stripe-development/references/testing.md b/.claude/skills/cashier-stripe-development/references/testing.md deleted file mode 100644 index 97e91f91..00000000 --- a/.claude/skills/cashier-stripe-development/references/testing.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.claude/skills/cashier-stripe-development/references/webhooks.md b/.claude/skills/cashier-stripe-development/references/webhooks.md deleted file mode 100644 index 19d2f708..00000000 --- a/.claude/skills/cashier-stripe-development/references/webhooks.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/.claude/skills/fortify-development/SKILL.md b/.claude/skills/fortify-development/SKILL.md deleted file mode 100644 index bd05dd6d..00000000 --- a/.claude/skills/fortify-development/SKILL.md +++ /dev/null @@ -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` | \ No newline at end of file diff --git a/.claude/skills/inertia-react-development/SKILL.md b/.claude/skills/inertia-react-development/SKILL.md deleted file mode 100644 index bb01fd9c..00000000 --- a/.claude/skills/inertia-react-development/SKILL.md +++ /dev/null @@ -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 ,
, 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 `` or `useForm`) -- Implementing client-side navigation with `` 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 - - -```react -export default function UsersIndex({ users }) { - return ( -
-

Users

-
    - {users.map(user =>
  • {user.name}
  • )} -
-
- ) -} -``` - -## Client-Side Navigation - -### Basic Link Component - -Use `` for client-side navigation instead of traditional `` tags: - - -```react -import { Link, router } from '@inertiajs/react' - -Home -Users -View User -``` - -### Link with Method - - -```react -import { Link } from '@inertiajs/react' - - - Logout - -``` - -### Prefetching - -Prefetch pages to improve perceived performance: - - -```react -import { Link } from '@inertiajs/react' - - - Users - -``` - -### Programmatic Navigation - - -```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 `` component: - - -```react -import { Form } from '@inertiajs/react' - -export default function CreateUser() { - return ( - - {({ errors, processing, wasSuccessful }) => ( - <> - - {errors.name &&
{errors.name}
} - - - {errors.email &&
{errors.email}
} - - - - {wasSuccessful &&
User created!
} - - )} - - ) -} -``` - -### Form Component With All Props - - -```react -import { Form } from '@inertiajs/react' - -
- {({ - errors, - hasErrors, - processing, - progress, - wasSuccessful, - recentlySuccessful, - clearErrors, - resetAndClearErrors, - defaults, - isDirty, - reset, - submit - }) => ( - <> - - {errors.name &&
{errors.name}
} - - - - {progress && ( - - {progress.percentage}% - - )} - - {wasSuccessful &&
Saved!
} - - )} -
-``` - -### Form Component Reset Props - -The `
` 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. - - -```react -import { Form } from '@inertiajs/react' - - - {({ errors, processing, wasSuccessful }) => ( - <> - - {errors.name &&
{errors.name}
} - - - - )} -
-``` - -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: - - -```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 ( -
- setData('name', e.target.value)} - /> - {errors.name &&
{errors.name}
} - - setData('email', e.target.value)} - /> - {errors.email &&
{errors.email}
} - - setData('password', e.target.value)} - /> - {errors.password &&
{errors.password}
} - - -
- ) -} -``` - -## Inertia v2 Features - -### Deferred Props - -Use deferred props to load data after initial page render: - - -```react -export default function UsersIndex({ users }) { - // users will be undefined initially, then populated - return ( -
-

Users

- {!users ? ( -
-
-
-
- ) : ( -
    - {users.map(user => ( -
  • {user.name}
  • - ))} -
- )} -
- ) -} -``` - -### Polling - -Automatically refresh data at intervals: - - -```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 ( -
-

Dashboard

-
Active Users: {stats.activeUsers}
-
- ) -} -``` - -### WhenVisible - -Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold: - - -```react -import { WhenVisible } from '@inertiajs/react' - -export default function Dashboard({ stats }) { - return ( -
-

Dashboard

- - {/* stats prop is loaded only when this section scrolls into view */} - Loading stats...
}> - {({ fetching }) => ( -
-

Total Users: {stats.total_users}

-

Revenue: {stats.revenue}

- {fetching && Refreshing...} -
- )} - - - ) -} -``` - -## Server-Side Patterns - -Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines. - -## Common Pitfalls - -- Using traditional `
` links instead of Inertia's `` 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 `
` without preventing default submission (use `` component or `e.preventDefault()`) -- Forgetting to check if `` component is available in your Inertia version \ No newline at end of file diff --git a/.claude/skills/pennant-development/SKILL.md b/.claude/skills/pennant-development/SKILL.md deleted file mode 100644 index 3b364661..00000000 --- a/.claude/skills/pennant-development/SKILL.md +++ /dev/null @@ -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 - - -```php -use Laravel\Pennant\Feature; - -Feature::define('new-dashboard', function (User $user) { - return $user->isAdmin(); -}); -``` - -### 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 -@feature('new-dashboard') - -@else - -@endfeature -``` - -### Activating / Deactivating - - -```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 \ No newline at end of file diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md deleted file mode 100644 index ba774e71..00000000 --- a/.claude/skills/pest-testing/SKILL.md +++ /dev/null @@ -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 - - -```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()`: - - -```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.): - - -```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. - - -```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: - - -```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): - - -```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 \ No newline at end of file diff --git a/.claude/skills/tailwindcss-development/SKILL.md b/.claude/skills/tailwindcss-development/SKILL.md deleted file mode 100644 index 7c8e295e..00000000 --- a/.claude/skills/tailwindcss-development/SKILL.md +++ /dev/null @@ -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 -@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: - - -```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: - - -```html -
-
Item 1
-
Item 2
-
-``` - -## 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: - - -```html -
- Content adapts to color scheme -
-``` - -## Common Patterns - -### Flexbox Layout - - -```html -
-
Left content
-
Right content
-
-``` - -### Grid Layout - - -```html -
-
Card 1
-
Card 2
-
Card 3
-
-``` - -## 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 \ No newline at end of file diff --git a/.claude/skills/wayfinder-development/SKILL.md b/.claude/skills/wayfinder-development/SKILL.md deleted file mode 100644 index 06ece8fb..00000000 --- a/.claude/skills/wayfinder-development/SKILL.md +++ /dev/null @@ -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 - - -```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 - - -```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 `` component: - -```typescript - -``` - -## 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 \ No newline at end of file diff --git a/app/Console/Commands/AgentDatabaseCommand.php b/app/Console/Commands/AgentDatabaseCommand.php new file mode 100644 index 00000000..0d1a203d --- /dev/null +++ b/app/Console/Commands/AgentDatabaseCommand.php @@ -0,0 +1,57 @@ +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; + } +} diff --git a/config/database.php b/config/database.php index 53dcae02..dc99a073 100644 --- a/config/database.php +++ b/config/database.php @@ -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'), diff --git a/tests/Feature/Console/AgentDatabaseCommandTest.php b/tests/Feature/Console/AgentDatabaseCommandTest.php new file mode 100644 index 00000000..0918a68d --- /dev/null +++ b/tests/Feature/Console/AgentDatabaseCommandTest.php @@ -0,0 +1,35 @@ +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(); +});