This file provides guidance to AI agents when working with code in this repository.
This is a Drupal extension scaffold template for creating contributed modules or themes. install.php turns the example extension below into yours.
Your Extension (your_extension) is a Drupal module. The project provides a complete development environment with CI configuration, testing setup, and deployment automation for publishing to Drupal.org.
HARD RULE - use the provided command wrappers, never the tool binaries directly. When make or ahoy exposes a command for a task, use that command; do not call the underlying binary directly. Each wrapper runs the tool where CI runs it, in build/ for most tools, with the config, plugins, and environment that CI uses, so a raw invocation from the repository root silently diverges from CI - it can pass locally while CI fails (or vice versa), or crash outright when a relative path resolves against the wrong directory. If no wrapped command covers what you need, extend the make or ahoy target rather than making a one-off raw call; if that is not feasible, stop and ask.
HARD RULE - run each tool exactly as CONTRIBUTING.md shows. This project has no command wrapper, so the commands below run directly. Most tools run from build/, which holds the config, plugins, and environment that CI uses, so a raw invocation from the repository root silently diverges from CI - it can pass locally while CI fails (or vice versa), or crash outright when a relative path resolves against the wrong directory.
HARD RULE - never modify scripts/eddy-tooling. It comes from Eddy as-is, and a scaffold update replaces it with the release's copy. To change what a tooling command does, declare a patch for the drevops/eddy-tooling package in composer.dev.json.
Run each tool through its make wrapper, never the binary directly:
- PHPCS / PHPCBF:
make lint/make lint-fix- nevervendor/bin/phpcsorvendor/bin/phpcbf.
- PHPStan:
make lint- nevervendor/bin/phpstan.
- Rector:
make lint(dry-run) /make lint-fix- nevervendor/bin/rector.
- Twig CS Fixer:
make lint/make lint-fix- nevervendor/bin/twig-cs-fixer.
- ESLint / Stylelint:
make lint/make lint-fix- nevernpx eslintornpx stylelint.
- CSpell:
make lint- nevernpx cspell.
- PHPUnit:
make test/make test-unit/make test-kernel/make test-functional- nevervendor/bin/phpunit.
- Jest:
make test-javascript- nevernpx jest.
- Drush:
make drush <command>- neverbuild/vendor/bin/drushdirectly.
Run each tool through its ahoy wrapper, never the binary directly:
- PHPCS / PHPCBF:
ahoy lint/ahoy lint-fix- nevervendor/bin/phpcsorvendor/bin/phpcbf.
- PHPStan:
ahoy lint- nevervendor/bin/phpstan.
- Rector:
ahoy lint(dry-run) /ahoy lint-fix- nevervendor/bin/rector.
- Twig CS Fixer:
ahoy lint/ahoy lint-fix- nevervendor/bin/twig-cs-fixer.
- ESLint / Stylelint:
ahoy lint/ahoy lint-fix- nevernpx eslintornpx stylelint.
- CSpell:
ahoy lint- nevernpx cspell.
- PHPUnit:
ahoy test/ahoy test-unit/ahoy test-kernel/ahoy test-functional- nevervendor/bin/phpunit.
- Jest:
ahoy test-javascript- nevernpx jest.
- Drush:
ahoy drush <command>- neverbuild/vendor/bin/drushdirectly.
Using Make:
make build- Complete build (stop → assemble → start → provision)make assemble- Assemble codebase with dependenciesmake start- Start PHP development servermake stop- Stop development servermake provision- Install/provision Drupal sitemake reset- Clean build directory, logs and installed tooling (aliases:make delete,make destroy)
Using Ahoy:
ahoy build- Complete build processahoy assemble- Assemble codebaseahoy start- Start development serverahoy stop- Stop development serverahoy provision- Provision Drupal siteahoy reset- Clean build directory, logs and installed tooling (aliases:ahoy delete,ahoy destroy)
scripts/eddy-tooling- Install the tooling commands intovendor/bin/. Run it before the commands belowvendor/bin/eddy-assemble- Assemble codebase with dependenciesvendor/bin/eddy-start- Start PHP development servervendor/bin/eddy-stop- Stop development servervendor/bin/eddy-provision- Install/provision Drupal site
Linting:
make lint- Run all linting toolsmake lint-fix- Auto-fix coding standards violations
ahoy lint- Run all linting toolsahoy lint-fix- Auto-fix coding standards violations
Testing:
make test- Run all tests
make test-unit- Run unit tests onlymake test-kernel- Run kernel tests onlymake test-functional- Run functional tests only
make test-functional-javascript- Run FunctionalJavascript tests (uses the local Chrome by default; setWEBDRIVER_BACKEND=seleniumfor Docker)
make test-javascript- Run JavaScript unit tests with Jest (alias:make test-js)
make browser-start- Start the browser for FunctionalJavascript tests (local Chrome by default; setWEBDRIVER_BACKEND=seleniumfor Docker)make browser-stop- Stop the browser
ahoy test- Run all tests
ahoy test-unit- Run unit tests onlyahoy test-kernel- Run kernel tests onlyahoy test-functional- Run functional tests only
ahoy test-functional-javascript- Run FunctionalJavascript tests (uses the local Chrome by default; setWEBDRIVER_BACKEND=seleniumfor Docker)
ahoy test-javascript- Run JavaScript unit tests with Jest (alias:ahoy test-js)
ahoy browser-start- Start the browser for FunctionalJavascript tests (local Chrome by default; setWEBDRIVER_BACKEND=seleniumfor Docker)ahoy browser-stop- Stop the browser
Run the linters and the tests with the commands that CONTRIBUTING.md lists.
make drush <command>- Run Drush commands. Put--before the arguments when they include an option, such asmake drush -- config:get system.site --format=json, becausemakereads an option on its command line as its own. An argument can't hold a space or name amaketarget, such ashelp, unless it's an option value joined with=make login- Get one-time login link
ahoy drush <command>- Run Drush commandsahoy login- Get one-time login link
build/vendor/bin/drush -l "$(vendor/bin/eddy-info site-url)" <command>- Run Drush commandsbuild/vendor/bin/drush -l "$(vendor/bin/eddy-info site-url)" uli- Get one-time login link
make info- Print a read-only summary of PHP/Drupal/Composer/Drush/Node/npm versions, webserver host/port (with source), site URL, XDebug state, WebDriver port, build directory, database path, and active profile. (alias:make describe)
ahoy info- Print a read-only summary of PHP/Drupal/Composer/Drush/Node/npm versions, webserver host/port (with source), site URL, XDebug state, WebDriver port, build directory, database path, and active profile. (alias:ahoy describe)
vendor/bin/eddy-info- Print a read-only summary of PHP/Drupal/Composer/Drush/Node/npm versions, webserver host/port (with source), site URL, XDebug state, WebDriver port, build directory, database path, and active profile.
Key Directories:
src/- Extension source code (services, forms, etc.)
tests/src/- PHPUnit tests (Unit/, Kernel/, Functional/)
config/schema/- Configuration schema definitions
css/andjs/- Stylesheets and JavaScript behaviors of the theme's global library
tests/src/- PHPUnit tests (Unit/, FunctionalJavascript/)
build/- Assembled Drupal codebase (symlinked extension)vendor/- The installeddrevops/eddy-toolingpackage, whose commands (vendor/bin/eddy-*) assemble, start, provision and deploy the extension. Not committed and not part of the extensionscripts/eddy-tooling- Installs the tooling package intovendor/from the constraint incomposer.dev.json. The command wrappers and CI run it before the tooling commandsscripts/- Custom lifecycle hooks: post-assemble (assemble-*.sh), post-provision (provision-*.sh), post-start (start-*.sh), and pre-stop (stop-*.sh). Run automatically during each phase in lexicographic order; non-zero exit aborts the parent. Excluded from distribution archives via.gitattributes
Template Files (before installation):
your_extension.*- Template extension filesYourExtensionService.php- Main service class template
- Service-based architecture: Main functionality in services registered via
*.services.yml - Configuration-driven: Uses Drupal configuration system with schema validation
- Test coverage: Unit, kernel, and functional test examples provided
- Form integration: Admin forms in
src/Form/for configuration
- No base theme:
base theme: false, so Drupal's default templates render every page until the theme overrides them intemplates/ - Theme hooks:
your_extension.themeadds adiv[data-your-extension-time]element to every page, whichjs/your_extension.jsfills with the page load time - Global library: The
librarieskey inyour_extension.info.ymlattaches thegloballibrary fromyour_extension.libraries.ymlto every page
- Test coverage: Unit test example for the theme hooks provided, plus a FunctionalJavascript smoke test example with the theme installed as the default theme
DRUPAL_VERSION- Target Drupal version (e.g.,10,11,11@alpha,12@beta). Defaults toextra.eddy.drupal-versionincomposer.dev.jsonWEBSERVER_HOST- Development server host (default: localhost)WEBSERVER_PORT- Development server port. Auto-discovered from range 8000-8099 and written to.envif not already set
WEBDRIVER_BACKEND- FunctionalJavascript WebDriver backend:chromedriver(default, drives the locally installed Chrome with no Docker) orselenium(Docker container)WEBDRIVER_PORT- Port for the WebDriver endpoint (both backends). Auto-discovered from 4444 and written to.envif not already set, so several projects can run FunctionalJavascript tests simultaneously. The endpoint inphpunit.xmlis the default for port 4444; tests reach the resolved port because the FunctionalJavascript base class rewrites the port inMINK_DRIVER_ARGS_WEBDRIVERfrom this variable, so FunctionalJavascript tests must extend that base class (or export their ownMINK_DRIVER_ARGS_WEBDRIVER)WEBDRIVER_HOST- Address the FunctionalJavascript browser uses to reach the PHP webserver. Defaults tolocalhostwithchromedriver, and tohost.docker.internal(macOS) or172.17.0.1(other systems) withselenium
WEBSERVER_WAIT_TIMEOUT- Seconds thatstartwaits for the development server to accept connections (default: 5)DRUPAL_PROFILE- Install profile thatprovisioninstalls the site from (default:standard)XDEBUG- Set to1to start the development server with XDebug step-debuggingCLOUDFLARE_TUNNEL- Set to1to expose the development server through a public Cloudflare quick tunnel.startwrites the tunnel URL to.envasTUNNEL_URL
LOGIN_QRCODE- Set to1to also render theloginlink as a terminal QR code (needsqrencode)
GITHUB_TOKEN- GitHub API token to avoid rate limitsDEBUG- Set to1to stream the full output of the underlying commands (Composer, npm, Drush). By default this output is suppressed and shown only when a command fails
Run php install.php once, in a fresh copy of the template, to turn it into your extension. The script deletes itself when it finishes, unless you answer no to "Remove this script". Then:
- Build environment:
make buildorahoy build
- Build environment:
scripts/eddy-tooling, thenvendor/bin/eddy-assemble,vendor/bin/eddy-startandvendor/bin/eddy-provision
- Develop your extension code in
src/
- Develop your theme: Twig templates in
templates/, hooks inyour_extension.theme, styles incss/and behaviors injs/
- Check standards:
make lintorahoy lint - Run tests:
make testorahoy test - Access site at the URL that
make infoorahoy inforeports
- Check standards: run the linters that
CONTRIBUTING.mdlists - Run tests: run the tests that
CONTRIBUTING.mdlists - Access site at the URL that
vendor/bin/eddy-info site-urlprints
- CSpell: Spell checking across the codebase (config at
.cspell.json)
- PHPCS: Drupal, DrupalPractice, DrevOps and PHPCompatibility standards
- PHPStan: Static analysis with Drupal extensions
- Rector: Automated refactoring and deprecation fixes
- Twig CS Fixer: Twig template formatting
- ESLint: JavaScript linting with Prettier formatting (config at
.eslintrc.json)
- Stylelint: CSS linting (config at
.stylelintrc.js)
- GitHub Actions:
.github/workflows/test.ymland deployment - Matrix testing:
- Drupal 10 on PHP 8.3-8.4
- Drupal 11 on PHP 8.3-8.5
- Drupal 12 on PHP 8.5
- Automated deployment: Mirror to Drupal.org after CI passes
- The
build/directory contains the assembled Drupal site - Extension files are symlinked from root into
build/web/modules/custom/(module) orbuild/web/themes/custom/(theme) - SQLite database created in
/tmp/site_[EXTENSION_NAME].sqlite - The PHP and JavaScript quality tools run from within the
build/directory
- CSpell runs from the project root
When the user asks to update this project's scaffold (e.g. "update scaffold" or "update Eddy"), fetch the update skill from GitHub into the local .claude/skills/ directory, then invoke it:
-
Create the target directory if it does not exist:
mkdir -p .claude/skills/update-consumer-eddy
-
Download the skill:
curl -fsSL -o .claude/skills/update-consumer-eddy/SKILL.md \ https://raw.githubusercontent.com/drevops/eddy/main/.eddy/skills/update-consumer-eddy/SKILL.md
-
Invoke the
update-consumer-eddyskill and follow its steps.
The skill directory is git-ignored - it is fetched on demand and not committed to the project.