drupal/one_line_installer

A bash installer for AI assisted tools with Drupal best practices.

Maintainers

Package info

git.drupalcode.org/project/one_line_installer.git

Issues

pkg:composer/drupal/one_line_installer

Transparency log

Statistics

Installs: 13

Dependents: 0

Suggesters: 0

2.0.0-alpha1 2026-07-06 08:46 UTC

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

  1. Installs DDEV (and Docker, if missing) — the only host dependency this installer manages.
  2. Collects every question up front (profile, coding agent(s), skills, command denylist, and any custom questions from a .drupalaibp.json) so the rest of the build runs unattended.
  3. Creates the DDEV project (ddev config), stages the coding-agent add-ons (these only touch .ddev/), then does a single vanilla ddev start.
  4. For Drupal builds (is_drupal: true, the default): runs composer create-project for the chosen profile/site-template into the still-empty project dir, applies Composer stability + allow-plugins config, and runs site:install.
  5. Installs the built-in Drupal dev defaults — drupal/core-dev and drupal/ai_best_practices — and writes a starter phpunit.xml. These are always installed for is_drupal: true builds; there's no flag to turn them off.
  6. Adds any config ddev_addons (redis, selenium, …) now — after the site exists, since some add-ons drop files outside .ddev/ that would otherwise break composer create-project — and applies them with a single ddev restart.
  7. Applies the rest of the .drupalaibp.json provisioning (composer repositories, composer projects, drush en modules, recipes), imports any .drupalaibp/config/ (partial drush config:import), and writes the custom questions answers to .ddev/.env.
  8. Installs the chosen coding agent(s) and agent skills, writes the agent command denylist, and updates the managed block in AGENTS.md.
  9. 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.
  10. 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:

  • skills is 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-skills takes the four defaults without prompting and --add-skills 'owner/repo|skill-a skill-b' installs the defaults plus those (a bare owner/repo means all its skills; quote the value — it contains | and often *). Used with a config, --add-skills extends the config's skills instead of the defaults.
  • composer_projects entries may carry a version constraint (drupal/ai:^1.1) or omit one (drupal/pathauto).
  • questions types are text, password, single-select-options, and multi-select-options. default pre-fills an editable prompt; value (distinct from default) pre-answers the question so it's never shown. Answers are written to .ddev/.env (mode 600, gitignored) so DDEV injects them into the web container on every ddev restart — they're never stored back into the config.
  • hooks keys are event names (see below); each entry has a script (path relative to the config file, no whitespace), an integer weight (lower runs first), and where: host, web (default — runs in the DDEV web container via ddev exec), or a named DDEV service. A hook on a pre-start event (before_ddev_start, ddev_addon_added, …) must set where: host explicitly, since the container isn't up yet.
  • copy_paths is a list of extra files/directories (paths relative to the config file, same convention as hooks.*.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. Like hooks, this is hand-authored and does not round-trip through config export.
  • extra_scripts is 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-chrome to ddev_addons for headed Chrome + noVNC on port 7900 (a built-in handler rewrites its config for you).
  • AI Best Practices (drupal/ai_best_practices) and drupal/core-dev + phpunit.xml are built-in defaults for every is_drupal: true build — 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:

ProfileCreates
cms (default)drupal/cms
standarddrupal/recommended-project, standard install profile
minimaldrupal/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:

  1. 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 container
    

    Keep .ddev/.env out of version control — the installer chmods it 600 and adds it to .gitignore for you whenever it writes to it.

  2. Wrap it in a Drupal Key where a module can consume one (AI provider keys, etc.) with drupal/key via drush — 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 -y
    
  3. Otherwise reference it from settings.php directly with getenv('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:

IdDefaultWhat it installs
dtkyesDrush Token Killerdrupal/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_apinoTool API + Toolbelt (experimental) — drupal/tool + drupal/tool_belt, so agents discover and run Drupal actions with drush tool:search / tool:info / tool:run.
droostnoDroost (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_kitnoAgent-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 --extras list 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

CommandPurpose
setup-site (default)Everything above. Runs when no command is given. Reads a .drupalaibp.json in the current directory when present.
installInstall this tool as a global drupalaibp command in ~/.local/bin (added to your PATH permanently). Afterwards just run drupalaibp.
deleteDelete 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

FieldValue
Usernameadmin
Passwordadmin
Mailadmin@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+
  • sudo access
  • 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:

DirectoryContents
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:

PathPurpose
src/bashly.ymlCLI definition (commands, flags, help, version)
src/setup_site_command.shsetup-site command (default): globals, run-mode/config detection, dispatch
src/install_command.shinstall command: install this tool globally as drupalaibp
src/delete_command.shdelete 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.ymlBashly 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.shtests/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