* chore: drop laravel/boost, make AGENTS.md the source of truth The two agent files were inverted here relative to 3.x. AGENTS.md held nothing but Laravel Boost's generated guidelines, while CLAUDE.md carried the actual project guide — so an agent reading AGENTS.md, the file the convention points at, got generic Laravel advice and none of the specifics for this repository. Boost is not used, so it goes rather than being worked around: the dependency, the boost:update hook on post-update-cmd, the .cursor MCP config that existed only to launch boost:mcp, and the three skill packages it installed there. AGENTS.md now carries the project guide and is the committed source of truth. CLAUDE.md, GEMINI.md and .github/copilot-instructions.md become gitignored symlinks to it, created by bin/ai-docs.php from post-autoload-dump — the same arrangement, and the same script, as 3.x. * chore: regenerate .phpstorm.meta.php Removes every boost.* entry now that the package is gone. Most of the diff is unrelated churn — the file had not been regenerated in a while, so it also picks up drift in routes and config. Kept separate so the boost removal stays readable.
5.8 KiB
AGENTS.md
Canonical guide for AI coding agents working in this repository. The tool-specific files
(CLAUDE.md, GEMINI.md, .github/copilot-instructions.md) are gitignored symlinks to this file —
run composer run ai-docs to (re)create them.
Project Overview
InvoiceShelf is an open-source invoicing and expense tracking application built with Laravel 13 (PHP 8.4) and Vue 3. It supports multi-company tenancy, customer portals, recurring invoices, and PDF generation.
Common Commands
Development
composer run dev # Starts PHP server, queue listener, log tail, and Vite dev server concurrently
pnpm dev # Vite dev server only
pnpm build # Production frontend build
Testing
php artisan test --compact # Run all tests
php artisan test --compact --filter=testName # Run specific test
./vendor/bin/pest --stop-on-failure # Run via Pest directly
make test # Makefile shortcut
Tests use SQLite in-memory DB, configured in phpunit.xml. Tests seed via DatabaseSeeder + DemoSeeder in beforeEach. Authenticate with Sanctum::actingAs() and set the company header.
Code Style
vendor/bin/pint --dirty --format agent # Fix style on modified PHP files
vendor/bin/pint --test # Check style without fixing (CI uses this)
Artisan Generators
Always use php artisan make:* with --no-interaction to create new files (models, controllers, migrations, tests, etc.).
Architecture
Multi-Tenancy
Every major model has a company_id foreign key. The CompanyMiddleware sets the active company from the company request header. Bouncer authorization is scoped to the company level via DefaultScope (app/Bouncer/Scopes/DefaultScope.php).
Authentication
Three guards: web (session), api (Sanctum tokens for /api/v1/), customer (session for customer portal). API routes use auth:sanctum middleware; customer portal uses auth:customer.
Routing
- API: All endpoints under
/api/v1/inroutes/api.php, grouped withauth:sanctum,company, andbouncermiddleware - Web:
routes/web.phpserves PDF endpoints, auth pages, and catch-all SPA routes (/admin/{vue?},/{company:slug}/customer/{vue?})
Frontend
- Entry point:
resources/scripts/main.js - Vue Router:
resources/scripts/admin/admin-router.js(admin),resources/scripts/customer/customer-router.js(customer portal) - State: Pinia stores in
resources/scripts/admin/stores/ - Path aliases:
@=resources/,$fonts,$imagesfor static assets - Vite dev server expects
invoiceshelf.testhostname
Backend Patterns
- Authorization: Silber/Bouncer with policies in
app/Policies/. Controllers use$this->authorize(). - Validation: Form Request classes, never inline validation
- API responses: Eloquent API Resources in
app/Http/Resources/ - PDF generation: DomPDF (
GeneratesPdfTrait) or Gotenberg - Email: Mailable classes with
EmailLogtracking - File storage: Spatie MediaLibrary, supports local/S3/Dropbox
- Serial numbers:
SerialNumberFormatterservice - Company settings:
CompanySettingmodel (key-value per company)
Database
Supports MySQL, PostgreSQL, and SQLite. Prefer Eloquent over raw queries. Use Model::query() instead of DB::. Use eager loading to prevent N+1 queries.
Code Conventions
- PHP: snake_case, constructor property promotion, explicit return types, PHPDoc blocks over inline comments
- JS: camelCase
- Always check sibling files for patterns before creating new ones
- Use
config()helper, neverenv()outside config files - Every change must have tests (feature tests preferred over unit tests)
- Run
vendor/bin/pint --dirty --format agentafter modifying PHP files
Releasing
Releases are cut by pushing a tag. Nothing is typed into GitHub by hand.
# 1. Add a "## <version> — <date>" section to CHANGELOG.md and bump version.md,
# in a PR like any other change — the notes are reviewed with the code.
# 2. Once merged, tag the merge commit:
git tag 2.4.3 && git push origin 2.4.3
release.yaml then runs the tests, reads the CHANGELOG.md section for that tag,
builds the package with make clean dist, and creates a draft release with the
zip attached. It stops there and prints the draft URL in the run summary.
You publish the draft yourself. That is deliberate, not an omission: GitHub does
not start workflow runs from events created with GITHUB_TOKEN, so a release
published by the workflow reaches nothing downstream. Pressing Publish fires
release: published under your own identity, which triggers docker.yaml to
register the release on the updater and build the Docker images. Until you
publish, no install is offered anything.
Notes on the mechanics:
- A tag with no
CHANGELOG.mdsection fails the run before anything is built, so a release can never go out with empty notes. prereleaseand "mark as latest" are set on the draft, derived from the tag: a-suffix (2.4.3-beta.1) means pre-release, which routes it to the insider channel so ordinary installs are not offered it. "Latest" is gated onLATEST_MAJORin the workflows — bump it there when 3.x becomes the stable line.- If registration fails, re-run it without cutting a new release: run the
Docker Build and Pushworkflow manually withregister_tagset to the version. That path is idempotent and does not rebuild the Docker images. .github/scripts/changelog-section.php <version>prints what the updater will be sent, so you can check the notes locally before tagging.
CI Pipeline
GitHub Actions (check.yaml): runs Pint style check, then builds frontend and runs Pest tests on PHP 8.4.