Files
InvoiceShelf/AGENTS.md
T
Darko Gjorgjijoski 8fe6975a81 chore: drop laravel/boost, make AGENTS.md the source of truth (#721)
* 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.
2026-07-29 17:39:20 +02:00

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/ in routes/api.php, grouped with auth:sanctum, company, and bouncer middleware
  • Web: routes/web.php serves 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, $images for static assets
  • Vite dev server expects invoiceshelf.test hostname

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 EmailLog tracking
  • File storage: Spatie MediaLibrary, supports local/S3/Dropbox
  • Serial numbers: SerialNumberFormatter service
  • Company settings: CompanySetting model (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, never env() outside config files
  • Every change must have tests (feature tests preferred over unit tests)
  • Run vendor/bin/pint --dirty --format agent after 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.md section fails the run before anything is built, so a release can never go out with empty notes.
  • prerelease and "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 on LATEST_MAJOR in 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 Push workflow manually with register_tag set 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.