### Setup commands: clone, install dependencies, and prepare environment Source: https://github.com/qiick-io/externa-core/blob/main/CONTRIBUTING.md Run these commands to clone the repository, switch to Node 24 via nvm, install PHP dependencies with Composer, and install npm dependencies. The npm ci command triggers Lefthook hook installation when a .git directory is present. ```bash git clone https://github.com/qiick-io/externa-core.git cd externa-core nvm use # Node 24 composer install npm ci # prepare installs Lefthook hooks when .git is present ``` -------------------------------- ### Contributor git clone setup Source: https://github.com/qiick-io/externa-core/blob/main/README.md For contributors cloning the repository. Copies `.env.example`, creates SQLite database if missing, installs dependencies, runs migrations, seeds, storage link, generates Wayfinder routes, and builds assets. ```bash git clone https://github.com/qiick-io/externa-core.git cd externa-core nvm use # Node 24 cp .env.example .env # Edit .env: set APP_URL to match how you browse. For no Redis, see “Minimal vs full” below. touch database/database.sqlite # if missing (SQLite default) composer install php artisan key:generate php artisan migrate php artisan db:seed php artisan storage:link npm install php artisan wayfinder:generate --with-form --no-interaction npm run build # or skip and rely on `npm run dev` / Vite HMR ``` -------------------------------- ### Local Docker Compose setup Source: https://github.com/qiick-io/externa-core/blob/main/README.md Copy the Docker environment file and start the full stack with Docker Compose. The app is available at http://localhost:8000, with Reverb on host port 8081. Optional profiles for MinIO, MySQL, MariaDB, and Keycloak OIDC are shown in comments. ```bash cp .env.docker.example .env docker compose up --build # App: http://localhost:8000 (COMPOSE_APP_URL overrides Herd APP_URL inside containers) # Reverb published on host :8081 (avoids Herd Reverb on :8080) # MinIO: docker compose --profile minio up --build # MySQL: docker compose --profile mysql -f compose.yaml -f compose.mysql.yaml up --build # MariaDB (host :3307): docker compose --profile mariadb -f compose.yaml -f compose.mariadb.yaml up --build # Keycloak OIDC QA: docker compose -f compose.yaml -f compose.oidc.yaml --profile oidc up --build # → docker/keycloak/README.md (issuer http://localhost:8080/realms/externa) ``` -------------------------------- ### Install via Composer create-project Source: https://github.com/qiick-io/externa-core/blob/main/README.md Recommended for operators. Creates a new site from Packagist without cloning the repository. Runs the interactive installer `php artisan externa:install`. Requires Node 24 via `nvm use`. ```bash composer create-project qiick/externa-core my-externa cd my-externa nvm use # Node 24 php artisan externa:install ``` -------------------------------- ### Start Keycloak OIDC Compose profile Source: https://github.com/qiick-io/externa-core/blob/main/docker/keycloak/README.md Starts the app with the OIDC overlay and profile. Requires .env with APP_KEY set. Override host port with FORWARD_KEYCLOAK_PORT if :8080 is taken. ```bash # From repo root. Uses Track A compose + this overlay. cp -n .env.docker.example .env # if needed; ensure APP_KEY is set docker compose -f compose.yaml -f compose.oidc.yaml --profile oidc up --build ``` -------------------------------- ### Run minimal stack without Redis Source: https://github.com/qiick-io/externa-core/blob/main/README.md Starts the development server, queue listener, pail, and Vite. Requires setting `QUEUE_CONNECTION=database` and `BROADCAST_CONNECTION=log` in `.env`. ```bash composer run dev # serve + queue:listen + pail + vite ``` -------------------------------- ### Install Externa via Composer Source: https://github.com/qiick-io/externa-core/blob/main/README.md Use this command to create a new Externa project. It is the primary installation method. ```bash composer create-project qiick/externa-core ``` -------------------------------- ### Run standalone all-in-one Source: https://github.com/qiick-io/externa-core/blob/main/README.md Starts its own Reverb instance. Conflicts if Herd already binds port 8080. ```bash composer run dev:full ``` -------------------------------- ### Run full stack with Redis and Reverb Source: https://github.com/qiick-io/externa-core/blob/main/README.md Starts Horizon, Pulse worker, and Vite for the full stack with Redis and Reverb. Requires Redis enabled and Reverb/Pulse keys from `.env.example`. Do not run `reverb:start` if Herd Reverb is already on port 8080. ```bash php artisan horizon php artisan pulse:work npm run dev ``` -------------------------------- ### Quick production deploy with Docker Compose Source: https://github.com/qiick-io/externa-core/blob/main/README.md Pull the multi-arch image from GHCR and start the production stack. Requires setting APP_KEY, DB_PASSWORD, APP_URL, and optionally RUN_SEED and INITIAL_SUPER_ADMIN_* for the first admin. The app is available at http://localhost:8080 with health check at /health/ready. ```bash cp .env.docker.quick.example .env # Set APP_KEY, DB_PASSWORD, APP_URL; for first admin set RUN_SEED=true + INITIAL_SUPER_ADMIN_* docker compose -f compose.quick.yaml up -d # App: http://localhost:8080 · health: GET /health/ready ``` -------------------------------- ### Explain a concept with graphify Source: https://github.com/qiick-io/externa-core/blob/main/CLAUDE.md Use graphify explain to get focused explanations of concepts from the knowledge graph. ```bash graphify explain "" ``` -------------------------------- ### PHP Constructor Property Promotion Source: https://github.com/qiick-io/externa-core/blob/main/GEMINI.md Use PHP 8 constructor property promotion in __construct(). Example: public function __construct(public GitHub $github) { } ```php public function __construct(public GitHub $github) { } ``` -------------------------------- ### Wayfinder: Import and use an invokable controller Source: https://github.com/qiick-io/externa-core/blob/main/GEMINI.md Import a controller action from '@/actions/' and call it directly. The example shows importing StorePost from a controller path and invoking it as StorePost(). ```typescript import StorePost from '@/actions/.../StorePostController'; StorePost() ``` -------------------------------- ### PHP Explicit Return Type and Method Params Source: https://github.com/qiick-io/externa-core/blob/main/GEMINI.md Always use explicit return type declarations and appropriate type hints. Example: protected function isAccessible(User $user, ?string $path = null): bool ```php protected function isAccessible(User $user, ?string $path = null): bool { ... } ``` -------------------------------- ### Run tests with artisan Source: https://github.com/qiick-io/externa-core/blob/main/AGENTS.md Run the narrowest set of tests covering your change. Pass a file path or --filter=testName. ```bash php artisan test --compact ``` -------------------------------- ### Run Pint to fix formatting Source: https://github.com/qiick-io/externa-core/blob/main/AGENTS.md Run this command to fix any formatting issues. Do not use --test flag. ```bash vendor/bin/pint --format agent ``` -------------------------------- ### Run Pint on dirty PHP files Source: https://github.com/qiick-io/externa-core/blob/main/AGENTS.md Run this command after modifying PHP files to fix formatting issues. Use --format agent to output results in agent-friendly format. ```bash vendor/bin/pint --dirty --format agent ``` -------------------------------- ### Run tests and quality checks Source: https://github.com/qiick-io/externa-core/blob/main/README.md Commands for running tests and quality checks. Use Node 24 for frontend checks (nvm use). composer test runs config:clear, Pint --test, and Pest. php artisan test runs Pest only. composer lint runs Pint (fix), composer lint:check runs Pint --test, and composer ci:check runs npm lint/format/types plus composer test. npm run lint:check, format:check, and types:check run frontend checks. ```bash composer test # config:clear + Pint --test + Pest php artisan test # Pest only composer lint # Pint (fix) composer lint:check # Pint --test composer ci:check # npm lint/format/types + composer test npm run lint:check npm run format:check npm run types:check ``` -------------------------------- ### Seed OIDC settings via artisan tinker Source: https://github.com/qiick-io/externa-core/blob/main/docker/keycloak/README.md Seeds OIDC settings and a local user via artisan tinker. With JIT off, SSO only links an existing local user by verified email. ```bash docker compose -f compose.yaml -f compose.oidc.yaml --profile oidc exec app php artisan tinker --execute=" \$s = app(\App\Services\Settings\SettingsRepository::class); \$s->setMany('project', 'project', [ 'oidc_enabled' => true, 'oidc_issuer' => 'http://localhost:8080/realms/externa', 'oidc_client_id' => 'externa-app', 'oidc_client_secret' => \App\Services\Settings\ProjectSettings::encryptOidcClientSecret('externa-oidc-qa-secret'), 'oidc_button_label' => 'Sign in with SSO', 'oidc_jit_provisioning' => false, ]); \App\Models\User::query()->firstOrCreate( ['email' => 'oidc-qa@example.com'], ['first_name' => 'OIDC', 'last_name' => 'QA', 'password' => 'password', 'is_active' => true, 'email_verified_at' => now()] ); echo 'OIDC settings + local user ready'; " ``` -------------------------------- ### Create a Pest test Source: https://github.com/qiick-io/externa-core/blob/main/AGENTS.md Use this artisan command to create a new Pest test. Do not include the test suite directory in the name. ```bash php artisan make:test --pest {name} ``` -------------------------------- ### Pest: Run tests with filter Source: https://github.com/qiick-io/externa-core/blob/main/GEMINI.md Run all tests with --compact, or filter by test name using --filter=testName. ```bash php artisan test --compact ``` ```bash php artisan test --compact --filter=testName ``` -------------------------------- ### Quality gates: lint, analyse, test, and format checks Source: https://github.com/qiick-io/externa-core/blob/main/CONTRIBUTING.md Run these commands to enforce code quality. composer analyse uses Larastan/PHPStan with a baseline in phpstan-baseline.neon. New PHPStan findings outside the baseline fail CI; regenerate the baseline intentionally with composer analyse:baseline. ```bash composer lint:check composer analyse # Larastan/PHPStan (baseline in phpstan-baseline.neon) composer test npm run lint:check && npm run format:check && npm run types:check ``` -------------------------------- ### Find relationships between paths Source: https://github.com/qiick-io/externa-core/blob/main/CLAUDE.md Use graphify path to find relationships between two paths in the codebase. ```bash graphify path "" "" ``` -------------------------------- ### Query the knowledge graph Source: https://github.com/qiick-io/externa-core/blob/main/CLAUDE.md Use graphify query to ask codebase questions when graphify-out/graph.json exists. Returns a scoped subgraph. ```bash graphify query "" ``` -------------------------------- ### Run tests with Pest directly Source: https://github.com/qiick-io/externa-core/blob/main/AGENTS.md Call the test runner directly. Accepts the same file path and --filter=testName arguments. ```bash vendor/bin/pest ``` -------------------------------- ### Wayfinder: Parameter binding with route keys Source: https://github.com/qiick-io/externa-core/blob/main/GEMINI.md Wayfinder detects route keys like {post:slug} and generates functions that accept an object with the key. Call show({ slug: "my-post" }) to bind the slug parameter. ```typescript show({ slug: "my-post" }) ``` -------------------------------- ### Update the knowledge graph Source: https://github.com/qiick-io/externa-core/blob/main/CLAUDE.md Run graphify update after modifying code to keep the graph current. This is AST-only and has no API cost. ```bash graphify update . ``` -------------------------------- ### Wayfinder: Query merging with mergeQuery Source: https://github.com/qiick-io/externa-core/blob/main/GEMINI.md Pass mergeQuery to merge query parameters with the current URL. Use null to remove a parameter, as shown with sort: null. ```typescript show(1, { mergeQuery: { page: 2, sort: null } }) ```