drupal / one_line_installer
A bash installer for AI assisted tools with Drupal best practices.
Package info
git.drupalcode.org/project/one_line_installer.git
pkg:composer/drupal/one_line_installer
This package is auto-updated.
Last update: 2026-07-15 12:46:31 UTC
README
Bootstrap a fresh Drupal CMS site — or any DDEV-based project — with a single command on macOS or Linux. Coding agents (Claude Code, Codex, OpenCode), agent skills, an agent command denylist, and provisioning (modules, recipes, composer packages, DDEV add-ons) are all wired up for you. DDEV is the only host dependency the installer manages — everything else (Composer, Drush, npm, ...) runs inside the DDEV web container.
Quick Start
bash <(curl -fsSL https://project.pages.drupalcode.org/one_line_installer/drupalaibp)
To skip all interactive prompts:
bash <(curl -fsSL https://project.pages.drupalcode.org/one_line_installer/drupalaibp) --yolo
Run drupalaibp --help to see all options.
The same command works on macOS and Linux — the installer detects your OS automatically.
What it does
- Installs DDEV (and Docker, if missing) — the only host dependency this installer manages.
- Collects every question up front (profile, coding agent(s), skills,
command denylist, and any custom
questionsfrom a.drupalaibp.json) so the rest of the build runs unattended. - Creates the DDEV project (
ddev config), stages the coding-agent add-ons (these only touch.ddev/), then does a single vanilladdev start. - For Drupal builds (
is_drupal: true, the default): runscomposer create-projectfor the chosen profile/site-template into the still-empty project dir, applies Composer stability + allow-plugins config, and runssite:install. - Installs the built-in Drupal dev defaults —
drupal/core-devanddrupal/ai_best_practices— and writes a starterphpunit.xml. These are always installed foris_drupal: truebuilds; there's no flag to turn them off. - Adds any config
ddev_addons(redis, selenium, …) now — after the site exists, since some add-ons drop files outside.ddev/that would otherwise breakcomposer create-project— and applies them with a singleddev restart. - Applies the rest of the
.drupalaibp.jsonprovisioning (composer repositories, composer projects,drush enmodules, recipes), imports any.drupalaibp/config/(partialdrush config:import), and writes the customquestionsanswers to.ddev/.env. - Installs the chosen coding agent(s) and agent skills, writes the agent
command denylist, and updates the managed block in
AGENTS.md. - Exports Drupal configuration to
config/sync(Drupal builds only), opens the site in your browser, and — if you opted in — pushes the project to a new GitHub repository. - Hands the shell over to the chosen coding agent, if any.
Running the installer inside an existing DDEV project opens management mode instead (see below) — it never re-runs the build.
The project directory defaults to my-drupalcms-site (you'll be prompted for
a name) in the current working directory.
.drupalaibp.json — config-driven builds
Drop a .drupalaibp.json in the directory where you launch the installer and
it pre-configures the entire build — profile, coding agents, skills,
provisioning, custom questions, and hook scripts fired at named build events.
Hook script paths resolve relative to the config file and are copied
into the new project root before they run.
The structural choices (profile, agents, skills, deny rules, provisioning) are
taken from the config and not prompted. The config's questions are still
asked interactively, each with its default pre-filled, so you can review or
adjust them — a question with a value is treated as already answered and is
not shown. Add --yolo to take every default silently and run fully
unattended (walk-away).
See docs/examples/ for two complete, runnable configs (a Drupal site with
provisioning, and a non-Drupal Node/Storybook project) and their scripts.
Schema
{
"ddev_project_type": "drupal11", // any `ddev config --project-type` value; default drupal11
"docroot": "web", // `ddev config --docroot` value; default "web" (is_drupal:true) or "." (is_drupal:false)
"launch_path": "", // `ddev launch <path>` override; use e.g. ":6017" when nothing is served at the docroot root (a dev-server daemon on its own web_extra_exposed_ports port). Default "" launches the project root.
"is_drupal": true, // false skips composer create-project/drush/config-export entirely
"profile": "cms", // cms (default) | standard | minimal | template
"site_template": "", // drupal.org site-template project name, when profile=template
"agents": ["claude"], // claude | codex | opencode — empty = bring your own
"composer_stability": "dev", // Composer minimum-stability (prefer-stable is always set)
"composer_allow_plugins": ["tbachert/spi"],
"skills": [
{ "repo": "ivanboring/drupal-module-finder", "skills": ["drupal-module-finder"] },
"obra/superpowers", // shorthand for "all skills in this repo"
{ "repo": "grasmash/drupal-claude-skills", "skills": ["*"] }
],
"deny_commands": ["git push", "ssh"],
"repositories": [
{ "type": "vcs", "url": "https://github.com/acme/acme_ai_glue" }
],
"composer_projects": [ // version constraints allowed inline
"drupal/ai:^1.1",
"drupal/token:^1.15",
"drupal/pathauto",
"acme/acme_ai_glue:dev-main"
],
"install_modules": ["ai", "token", "pathauto"],
"recipes": ["drupal/drupal_cms_blog"],
"ddev_addons": ["ddev/ddev-selenium-standalone-chrome", "ddev/ddev-redis"],
"extra_scripts": ["tool_api", "dtk"], // extra Drupal installations (see below): tool_api | dtk | droost
"agents_md": [
"This is the ACME demo site. Never edit web/sites/default/settings.php directly."
],
"questions": [
{
"env": "OPENAI_API_KEY",
"label": "OpenAI API key (stored in .ddev/.env, never committed)",
"type": "password"
},
{
"env": "SITE_FLAVOR",
"label": "Which content flavor should the demo seed?",
"type": "single-select-options",
"select_options": ["blog", "shop", "docs"],
"default": "blog"
},
{
"env": "SITE_ENV",
"label": "Deployment environment",
"type": "text",
"value": "demo"
}
],
"hooks": {
"ddev_started": [
{ "script": "./scripts/seed-content.sh", "weight": 10, "where": "web" }
],
"coding_agent_added": [
{ "script": "./scripts/agent-extras.sh", "weight": 0, "where": "host" }
]
},
"copy_paths": ["./scripts/assets"] // extra files/dirs (not hook scripts) staged into the project root
}
Key notes:
skillsis a single list — an entry is either a plain"owner/repo"string (all skills in that repo) or{ "repo": "…", "skills": [...] }(["*"]also means all). There's no separate "extra skills" list. On the CLI,--default-skillstakes the four defaults without prompting and--add-skills 'owner/repo|skill-a skill-b'installs the defaults plus those (a bareowner/repomeans all its skills; quote the value — it contains|and often*). Used with a config,--add-skillsextends the config'sskillsinstead of the defaults.composer_projectsentries may carry a version constraint (drupal/ai:^1.1) or omit one (drupal/pathauto).questionstypes aretext,password,single-select-options, andmulti-select-options.defaultpre-fills an editable prompt;value(distinct fromdefault) pre-answers the question so it's never shown. Answers are written to.ddev/.env(mode600, gitignored) so DDEV injects them into the web container on everyddev restart— they're never stored back into the config.hookskeys are event names (see below); each entry has ascript(path relative to the config file, no whitespace), an integerweight(lower runs first), andwhere:host,web(default — runs in the DDEV web container viaddev exec), or a named DDEV service. A hook on a pre-start event (before_ddev_start,ddev_addon_added, …) must setwhere: hostexplicitly, since the container isn't up yet.copy_pathsis a list of extra files/directories (paths relative to the config file, same convention ashooks.*.script) staged into the new project root before any hook runs — for hook-consumed assets that aren't scripts themselves (e.g. a hand-authored.storybook/config a hook then copies into the build output). Directories are copied recursively. Likehooks, this is hand-authored and does not round-trip through config export.extra_scriptsis a list of "extra Drupal installation" ids —tool_api,dtk,droost(see below). They are the same scripts the interactive multi-select offers, so a config gets them without listing their packages.- Selenium is plain provisioning, not a dedicated flag: add
ddev/ddev-selenium-standalone-chrometoddev_addonsfor headed Chrome + noVNC on port 7900 (a built-in handler rewrites its config for you). - AI Best Practices (
drupal/ai_best_practices) anddrupal/core-dev+phpunit.xmlare built-in defaults for everyis_drupal: truebuild — they are not listed in the config and can't be turned off from it.
Events
Every pipeline step fires before_<step> then, after it runs, <step>_done,
for each of (in order): ensure_ddev, collect_answers, create_project_dir,
configure_ddev, answers_to_env, install_coding_agents, start_ddev,
create_drupal_site, setup_core_dev, install_extras, install_ddev_addons,
restart_ddev, provision_from_config, import_config, install_skills,
write_agents_md_block, write_agent_denylist, link_claude_skills,
export_drupal_config, github_finalize, announce_complete, launch_site,
agent_handoff.
Plus these semantic events, fired from inside the relevant step:
before_ddev_start, ddev_started, coding_agent_added, ddev_addon_added,
extra_installed, skill_added, ask_questions.
2.0 clean break
Version 2.0 replaced the old per-flag config format (get_modules,
extra_skills, a selenium/toolbelt/dtk boolean per feature) with the
schema above. This is a clean break — configs saved by 1.x must be
recreated; the loader does not read the old keys.
Install options
Pick the Drupal install with --profile, or answer the interactive prompt:
| Profile | Creates |
|---|---|
cms (default) | drupal/cms |
standard | drupal/recommended-project, standard install profile |
minimal | drupal/recommended-project, minimal install profile |
bash <(curl -fsSL https://project.pages.drupalcode.org/one_line_installer/drupalaibp) --profile minimal
Site template
In interactive mode the second option is Site template — a pre-built
Drupal CMS design. Pick one of the free Drupal.org site templates
(Haven, Byte, Healthcare, Archimedes, …) from the list — each shown with a
one-line description and a preview URL — or type any template's project name
yourself. The installer builds on drupal/cms, runs
composer require drupal/<name>, and installs the site from that template's
recipe. (Site templates are interactive only — the --profile flag covers
cms/standard/minimal.)
Coding agent
In interactive mode you can install a coding agent into the DDEV web
container — Claude Code, Codex, and/or OpenCode. Each is
installed into the web image and gets a DDEV command, so you can start it
with ddev claude, ddev codex, or ddev opencode. With --yolo, Claude
Code is installed by default.
Secrets (API keys)
The installer writes secret-handling guidance into the project's AGENTS.md
so the coding agent never hard-codes or commits an API key. The recommended
flow uses tooling DDEV and Drupal already provide:
Store the value in an environment variable with DDEV's built-in
dotenv set:ddev dotenv set .ddev/.env --openai-api-key=<value> # → OPENAI_API_KEY ddev restart # loads it into the containerKeep
.ddev/.envout of version control — the installer chmods it600and adds it to.gitignorefor you whenever it writes to it.Wrap it in a Drupal Key where a module can consume one (AI provider keys, etc.) with
drupal/keyviadrush— after confirming the variable is set without printing its value:ddev exec 'test -n "$OPENAI_API_KEY"' # exit 0 = present ddev drush key:save openai_api_key \ --label='OpenAI API Key' --key-type=authentication \ --key-provider=env \ --key-provider-settings='{"env_variable":"OPENAI_API_KEY","base64_encoded":false,"strip_line_breaks":true}' \ --key-input=none -yOtherwise reference it from
settings.phpdirectly withgetenv('OPENAI_API_KEY').
Extra Drupal installations
Optional Drupal tools, offered as one multi-select during the questions ("Any
extra Drupal installations?"). Defaults are pre-ticked and are what --yolo
installs; the rest start unticked. The ones that ship today:
| Id | Default | What it installs |
|---|---|---|
dtk | yes | Drush Token Killer — drupal/dtk, which compacts drush output so agents spend far fewer tokens reading it. Also runs drush dtk:install <agent> for each coding agent you chose, since DTK compresses nothing until an agent opts in. |
tool_api | no | Tool API + Toolbelt (experimental) — drupal/tool + drupal/tool_belt, so agents discover and run Drupal actions with drush tool:search / tool:info / tool:run. |
droost | no | Droost (experimental) — drupal/droost + drupal/mcp_server, letting AI agents build inside the site over MCP. Dev-only, read-only by default. |
agent_ready_drupal_build_kit | no | Agent-Ready Drupal Build Kit — installs no module: it adds the kit's agent skill, which drives an agent to rebuild an existing website as this Drupal CMS site (content model, verification, review packet). Requires the Drupal CMS profile — on standard/minimal it installs nothing and says so, loudly. |
Pick them non-interactively in one of two ways:
--extras tool_api,dtk— an explicit list that replaces the defaults (--extras ""installs none).--add-extras droost— the defaults plus these. Pass both and the--extraslist becomes the base the additions land on.
From a config, "extra_scripts": ["tool_api", "dtk"] sets the same list;
--add-extras on top of a config extends it rather than replacing it.
Each extra is a single script in src/lib/extras/ that declares its own
menu entry (and whether it's a default) — see AGENTS.md for how to
add one (drop in a file, regenerate; no prompt, global, pipeline, or config
change).
Selenium (plain provisioning)
Selenium used to be a dedicated --yolo-gated flag; it is now expressed as
ordinary .drupalaibp.json provisioning (see the schema above and
docs/examples/drupal/): add ddev/ddev-selenium-standalone-chrome to
ddev_addons. A built-in handler rewrites its config for headed Chrome,
watchable over noVNC at https://<project>.ddev.site:7900; tests reach the
browser at selenium-chrome:4444.
Commands
| Command | Purpose |
|---|---|
setup-site (default) | Everything above. Runs when no command is given. Reads a .drupalaibp.json in the current directory when present. |
install | Install this tool as a global drupalaibp command in ~/.local/bin (added to your PATH permanently). Afterwards just run drupalaibp. |
delete | Delete a DDEV project — the one you are in, or (if you are not inside one) a running project picked from a subdirectory. Confirms, runs ddev delete -Oy, then removes the directory. |
# Install once, then reuse the short command:
bash <(curl -fsSL https://project.pages.drupalcode.org/one_line_installer/drupalaibp) install
drupalaibp # set up a site
drupalaibp delete # tear one down
# Or run any command straight from curl without installing:
bash <(curl -fsSL https://project.pages.drupalcode.org/one_line_installer/drupalaibp) delete
Saved configs (installed global command)
Once installed, running drupalaibp shows a third run-mode option — Use a
config — alongside Trust the script and Let me pick my own choices. It
reads .drupalaibp.json configs from ~/.drupalaibp/configs/ (created by
install) and builds a site from one without prompting. The same menu
lets you create a config (answer the normal questions once, then save it
— free-text prompts fill in the provisioning lists) or delete one. This
mode is pure bash — it no longer needs jq on the host.
A saved config captures the same schema documented above, minus
repositories, agents_md, questions, and hooks (hand-edit the JSON to
add those — create_config tells you where the file lives). The project name
and GitHub setup are not stored — you're asked for those on every build,
config mode included.
Management mode (existing DDEV project)
Running setup-site inside a directory that's already a DDEV project
(.ddev/config.yaml present) opens an interactive menu to add/remove the
tooling this installer manages: AI Best Practices, coding agents (and their
command denylist), agent skills, DDEV add-ons (a generic add/remove menu —
type any add-on name), and GitHub (add-only). It refuses --yolo (it's
interactive) and restarts DDEV once at the end if anything that touches the
web image changed.
Default credentials
| Field | Value |
|---|---|
| Username | admin |
| Password | admin |
| admin@example.com |
Requirements
DDEV is the only host dependency this installer manages — on both macOS and Linux, it installs Docker (if missing) and then DDEV itself. Everything else (Composer, Drush, npm, coding agent CLIs, …) runs inside the DDEV web container.
macOS
- macOS (Intel or Apple Silicon)
- Internet connection
Linux
- Ubuntu 20.04+ / Debian 11+, or Fedora 38+ / RHEL 9+
sudoaccess- Internet connection
Testing a merge request fork (contributors)
test-mr.sh is a developer tool for testing a drupal/ai_best_practices
merge request locally before it is merged. It fetches the fork via the
GitLab API, clones it, spins up a fresh DDEV Drupal CMS site, and installs
the addon from the local clone so you can edit and push code directly.
Prerequisites: curl, git, docker (daemon running), ddev — no auto-install.
bash <(curl -fsSL https://git.drupalcode.org/project/one_line_installer/-/raw/1.0.x/test-mr.sh) <MR-number>
For example, to test MR !64:
bash <(curl -fsSL https://git.drupalcode.org/project/one_line_installer/-/raw/1.0.x/test-mr.sh) 64
This creates two directories in your current working directory:
| Directory | Contents |
|---|---|
mr-64/ | The fork clone — edit code here and push |
mr-64-site/ | The DDEV Drupal CMS site |
After the install completes, the site opens in your browser (admin / admin).
To re-sync after editing fork code:
cd mr-64-site && ddev exec composer update drupal/ai_best_practices
Developing the installer (maintainers)
drupalaibp is generated by Bashly — do not edit
it by hand. The source of truth is:
| Path | Purpose |
|---|---|
src/bashly.yml | CLI definition (commands, flags, help, version) |
src/setup_site_command.sh | setup-site command (default): globals, run-mode/config detection, dispatch |
src/install_command.sh | install command: install this tool globally as drupalaibp |
src/delete_command.sh | delete command entry |
src/lib/core/ | Constants, invocation detection, colors/console helpers, the event registry (events.sh), the build pipeline (pipeline.sh), the typed question engine (ask.sh), and the pure-bash JSON reader (json.sh) |
src/lib/config/ | .drupalaibp.json load/export/hooks and the "Use a config" run mode |
src/lib/steps/ | The build-pipeline steps shared by every build (Drupal or not) |
src/lib/defaults/ | Built-in event handlers for optional features (e.g. the Selenium headed/VNC rewrite) |
src/lib/extras/ | The "extra Drupal installations" — one script per extra (extra_<id> + extra_<id>_meta), discovered at runtime |
src/lib/platform/ | The only OS-specific code: Docker + DDEV CLI install per platform |
src/lib/shared/ | Steps grouped by role: prompts/ (front-loaded questions), denylist/, manage/ (management mode), mcp/ |
src/lib/install/ | Global-install command steps |
src/lib/delete/ | delete_project implementation |
settings.yml | Bashly build settings |
Every file under src/lib/ holds one primary function (named after the file),
with any tightly-scoped private helpers alongside it, so the whole installer can
be scanned at a glance. Bashly bundles them (recursively) into drupalaibp. See AGENTS.md for the full source-layout table, the globals
list, and the event-driven architecture in detail.
End users need nothing but the committed drupalaibp. Maintainers need
Bashly only to regenerate it, via the wrapper:
bin/generate # uses Docker — no Ruby install required
BASHLY=ruby bin/generate # or a locally installed bashly gem
The wrapper pins Bashly to the version in .bashly-version (the same version
CI's bashly_verify job uses — Bashly stamps its version into the generated
file, so an unpinned local Bashly would fail CI on a one-line diff) and verifies
the generated script is complete before you commit it.
After editing anything under src/, regenerate and commit the updated
drupalaibp. CI (bashly_verify) fails the pipeline if the committed
drupalaibp has drifted from src/.
Tests and linting
Three wrappers, each pinning its tool's version and running it in Docker — so there is nothing to install, and CI runs exactly what you run:
bin/test # bats test suite (CI job: bats)
bin/lint # ShellCheck (CI job: shellcheck)
bin/generate # regenerate + verify (CI job: bashly_verify)
Prefer your own tools? BATS=local bin/test, SHELLCHECK=local bin/lint, and
BASHLY=ruby bin/generate use the binaries on your PATH instead (each still
checks the version matches the pin).
The bats suite is modular — one .bats file per source file, mirroring the
tree (src/lib/core/json.sh → tests/bats/lib/core/json.bats), so a new source
file comes with its own test file. Tests are hermetic: every external
command (ddev, docker, git, gh, npx, curl, sudo) is stubbed and
recorded, so the suite never starts DDEV, hits the network, or writes outside a
temp dir. Run part of it with bin/test tests/bats/lib/core. Live end-to-end
testing stays manual, by design.
ShellCheck is a hard gate with zero findings — no baseline, no blanket
exclusions. It lints the generated drupalaibp (which is every src/lib
fragment concatenated, so all of src/ is covered with full context) plus the
standalone scripts. Intentional patterns carry a targeted
# shellcheck disable=SCxxxx in src/ explaining why.
Limitations
- Windows is not supported