Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 37 additions & 17 deletions bin/dq-init
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,59 @@
/**
* dq-init: Step 1 of the drupalquick workflow.
*
* Full workflow:
* 1. composer exec dq-init [--interactive] [--ddev] ← this script
* Full workflow (DDEV-first — prefix each step with `ddev`):
* 1. ddev composer exec dq-init [--interactive] [--no-ddev] ← this script
* Delivers config.dq.yml to the project root.
* 2. composer exec dq-install
* 2. ddev composer exec dq-install
* Installs any drupalquick-managed recipe packages via Composer.
* 3. drush dq:scaffold
* 3. ddev drush dq:scaffold
* Installs Drupal, generates the theme, applies recipes, builds assets.
*
* The sole output of this script is config.dq.yml — a single YAML file the
* developer reviews and owns before anything is installed. No Drupal code is
* touched here; dq:scaffold is what actually builds the site.
*
* Without flags — copies the template and exits. The user edits the file
* then runs `composer exec dq-install`.
* DDEV is the default context: dq-init also drops the DDEV local config into
* .ddev/ (config.local.yaml + the deploy-credentials example). Running on the
* host inside a DDEV project is refused (see Comingling) — use `ddev`.
*
* Without flags — copies the template + DDEV local config and exits. The user
* edits config.dq.yml then runs `ddev composer exec dq-install`.
*
* With --interactive — walks the user through site, theme, and recipe
* choices via CLI prompts and writes a pre-filled config.dq.yml.
*
* With --ddev — copies the drupalquick DDEV local config template into the
* project's .ddev/ directory if one exists. Can be combined with either of
* the above modes.
* With --no-ddev — bare-host setup: skips the DDEV local config (you supply
* your own PHP/Node/database/webserver). Combines with the modes above.
*/

$packageRoot = dirname(__DIR__);
$projectRoot = getcwd();
$destination = $projectRoot . '/config.dq.yml';
$interactive = in_array('--interactive', $argv, true);
$ddev = in_array('--ddev', $argv, true);
// DDEV is Quick's default install context; --no-ddev opts into a bare-host
// setup (you then supply your own PHP/Node/DB/webserver). --ddev is still
// accepted as an explicit no-op for clarity.
$ddev = !in_array('--no-ddev', $argv, true);

// Preset discovery is a plain class in this package — required directly, so
// the script keeps working without any Composer autoloader.
// Plain classes, required directly so the script works without a Composer
// autoloader.
require_once $packageRoot . '/src/Config/PresetDiscovery.php';
require_once $packageRoot . '/src/Environment/Comingling.php';

use DrupalQuick\Config\PresetDiscovery;
use DrupalQuick\Environment\Comingling;

// Refuse a host run inside a DDEV project — mixing host and container tooling
// diverges composer/npm state and the host can't reach DDEV's database.
if ($guard = Comingling::hostInDdevProjectError($projectRoot, Comingling::env())) {
fwrite(STDERR, "❌ {$guard}\n");
exit(1);
}

// Next-step guidance is copy-paste correct for the project's mode: prefix with
// `ddev` when this is a DDEV project, bare otherwise.
$cmd = is_file($projectRoot . '/.ddev/config.yaml') ? 'ddev ' : '';

// ------------------------------------------------------------------ Helpers

Expand Down Expand Up @@ -88,7 +107,7 @@ function chooseMany(string $question, array $options): array {
if (!$interactive) {
if (file_exists($destination)) {
echo "ℹ️ config.dq.yml already exists. Edit it, then run:\n";
echo " composer exec dq-install\n";
echo " {$cmd}composer exec dq-install\n";
} else {
$source = $packageRoot . '/templates/config.dq.yml';
if (!copy($source, $destination)) {
Expand All @@ -97,7 +116,7 @@ if (!$interactive) {
}
echo "✅ config.dq.yml created in project root.\n";
echo " Edit it to configure your site, then run:\n";
echo " composer exec dq-install\n";
echo " {$cmd}composer exec dq-install\n";
}

// Handle --ddev regardless of whether config already existed.
Expand All @@ -119,7 +138,7 @@ if (file_exists($destination)) {
$overwrite = strtolower(ask('config.dq.yml already exists. Overwrite it? (y/N)', 'n'));
if ($overwrite !== 'y' && $overwrite !== 'yes') {
echo "ℹ️ Keeping the existing config.dq.yml. Edit it, then run:\n";
echo " composer exec dq-install\n";
echo " {$cmd}composer exec dq-install\n";
if ($ddev) {
ddev_copy($packageRoot, $projectRoot);
}
Expand Down Expand Up @@ -270,7 +289,7 @@ if (file_put_contents($destination, $yaml) === false) {

echo "\n✅ config.dq.yml created.\n";
echo " Review it, make any edits, then run:\n";
echo " composer exec dq-install\n";
echo " {$cmd}composer exec dq-install\n";

if ($ddev) {
ddev_copy($packageRoot, $projectRoot);
Expand All @@ -286,7 +305,8 @@ function ddev_copy(string $packageRoot, string $projectRoot): void {
$src = $packageRoot . '/templates/ddev/config.local.yaml';

if (!is_dir($ddevDir)) {
echo "ℹ️ --ddev passed but no .ddev directory found in project root. Skipping.\n";
echo "ℹ️ No .ddev directory found. Quick is DDEV-first — run `ddev config`\n";
echo " to set up DDEV, then re-run. (For a bare-host setup, pass --no-ddev.)\n";
return;
}

Expand Down
19 changes: 15 additions & 4 deletions bin/dq-install
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Full workflow:
* 1. composer exec dq-init — creates config.dq.yml
* 2. composer exec dq-install — THIS SCRIPT
* 3. drush dq:scaffold — installs Drupal and builds the site
* 3. ddev drush dq:scaffold — installs Drupal and builds the site
*
* Reads config.dq.yml and resolves each recipe against the registry
* (templates/recipe-registry.json). Registry recipes are standalone Composer
Expand Down Expand Up @@ -44,13 +44,24 @@ require_once $autoload;
// so they resolve regardless of the consumer autoloader's state.
require_once $packageRoot . '/src/Config/RecipeOptions.php';
require_once $packageRoot . '/src/Config/RecipeBlocks.php';
require_once $packageRoot . '/src/Environment/Comingling.php';

use DrupalQuick\Config\RecipeOptions;
use DrupalQuick\Config\RecipeBlocks;
use DrupalQuick\Environment\Comingling;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Process\Process;

// Refuse a host run inside a DDEV project — run `ddev composer exec dq-install`.
if ($guard = Comingling::hostInDdevProjectError($projectRoot, Comingling::env())) {
fwrite(STDERR, "❌ {$guard}\n");
exit(1);
}

// Next-step guidance prefixed with `ddev` in a DDEV project (bare otherwise).
$cmd = is_file($projectRoot . '/.ddev/config.yaml') ? 'ddev ' : '';

/**
* Runs an external command via Symfony Process, streaming its output.
*
Expand Down Expand Up @@ -86,7 +97,7 @@ try {
$recipes = $config['recipes'] ?? [];
if (empty($recipes)) {
echo "ℹ️ No recipes listed in config.dq.yml. Nothing to install.\n";
echo " Run `drush dq:scaffold` when ready.\n";
echo " Run `{$cmd}drush dq:scaffold` when ready.\n";
exit(0);
}

Expand Down Expand Up @@ -200,7 +211,7 @@ if (!empty($toRequire)) {
if (empty($toRequire)) {
echo "ℹ️ No drupalquick-managed recipes to install (core/contrib recipes\n";
echo " are already available on disk).\n";
echo " Run `drush dq:scaffold` when ready.\n";
echo " Run `{$cmd}drush dq:scaffold` when ready.\n";
exit(0);
}

Expand Down Expand Up @@ -329,4 +340,4 @@ if ($needsOptionWrite || $needsBlockWrite) {
}
}

echo "\n Run `drush dq:scaffold` to build your site.\n";
echo "\n Run `{$cmd}drush dq:scaffold` to build your site.\n";
3 changes: 3 additions & 0 deletions src/Drush/Commands/CleanupCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ protected function configure(): void {

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->io = new DrushStyle($input, $output);
if (!$this->guardDdevEnvironment()) {
return self::FAILURE;
}
$purge = $input->getOption('purge') || $input->getOption('remove-everything');

if (!$input->getOption('force')) {
Expand Down
3 changes: 3 additions & 0 deletions src/Drush/Commands/DeployCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ protected function configure(): void {

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->io = new DrushStyle($input, $output);
if (!$this->guardDdevEnvironment()) {
return self::FAILURE;
}
$self = Drush::aliasManager()->getSelf();

// 1. Resolve the target (flag overrides persisted/config setting).
Expand Down
17 changes: 17 additions & 0 deletions src/Drush/Commands/DrupalQuickHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Drupal\Component\Serialization\Json;
use Drupal\Component\Serialization\Yaml;
use DrupalQuick\Environment\Comingling;
use Drush\Drush;
use Drush\Style\DrushStyle;
use Symfony\Component\Process\Process;
Expand All @@ -22,6 +23,22 @@ trait DrupalQuickHelpers {
*/
protected DrushStyle $io;

/**
* Refuses to run on the host inside a DDEV project (see Comingling).
*
* Call at the top of execute(), after $this->io is set:
* if (!$this->guardDdevEnvironment()) { return self::FAILURE; }
* Returns FALSE (with an error printed) when the command should abort.
*/
protected function guardDdevEnvironment(): bool {
$msg = Comingling::hostInDdevProjectError(getcwd(), Comingling::env());
if ($msg !== NULL) {
$this->io->error($msg);
return FALSE;
}
return TRUE;
}

/**
* Returns the Drupal web root (the docroot, e.g. the project's web/ dir).
*
Expand Down
35 changes: 32 additions & 3 deletions src/Drush/Commands/ScaffoldCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,17 @@ protected function configure(): void {
$this
->addOption('interactive', NULL, InputOption::VALUE_NONE, 'Prompt the user to fill out or override config values interactively.')
->addOption('force', NULL, InputOption::VALUE_NONE, 'Scaffold even when Drupal is already installed. This reinstalls the site and DESTROYS the existing database.')
->addOption('theme-dev', NULL, InputOption::VALUE_NEGATABLE, 'Enable Twig development mode (twig debug + auto-reload, caches off) via `drush theme:dev` after the build, for live theme iteration. Defaults to on inside a DDEV web container; --no-theme-dev opts out. Reverse any time with `drush theme:dev off`.', NULL)
->addUsage('dq:scaffold')
->addUsage('dq:scaffold --interactive');
->addUsage('dq:scaffold --interactive')
->addUsage('dq:scaffold --no-theme-dev');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$this->io = new DrushStyle($input, $output);
if (!$this->guardDdevEnvironment()) {
return self::FAILURE;
}

$configFile = getcwd() . '/config.dq.yml';
if (!file_exists($configFile)) {
Expand Down Expand Up @@ -103,11 +108,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->io->writeln("✅ Configuration updated for this session.\n");
}

// Resolve theme-dev: explicit flag wins; otherwise default on inside a
// DDEV web container (the local-iteration context), like dq:static's
// --ddev-preview. Applied at the end of a successful build.
$themeDevOpt = $input->getOption('theme-dev');
$enableThemeDev = $themeDevOpt ?? (getenv('IS_DDEV_PROJECT') === 'true');

// Run the build phases inside one try/catch: every Drush call below uses
// mustRun(), so any failure aborts with a clear "site may be partially
// built" message instead of a raw stack trace.
try {
return $this->runBuild($config, $registry);
return $this->runBuild($config, $registry, $enableThemeDev);
}
catch (\Throwable $e) {
$this->io->error('Scaffold failed: ' . $e->getMessage());
Expand All @@ -119,7 +130,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* Runs the build phases: install → theme → recipes → config → assets.
*/
private function runBuild(array $config, array $registry): int {
private function runBuild(array $config, array $registry, bool $enableThemeDev = FALSE): int {
$siteName = $config['site']['name'] ?? 'Drupal Site';
$accountName = $config['site']['admin_user'] ?? 'admin';
// Secure default: when no admin_pass is configured, generate a strong one
Expand Down Expand Up @@ -500,6 +511,24 @@ private function runBuild(array $config, array $registry): int {
$this->io->writeln('🎉 [drupalquick] Scaffold complete. Rebuilding caches...');
Drush::drush(Drush::aliasManager()->getSelf(), 'cache:rebuild')->mustRun();

// Enable Twig development mode for live theme iteration (twig debug +
// auto-reload, render/page/dynamic caches off). This is Drupal's own
// development-settings mechanism (a key-value in the DB, not a file), so it
// survives rebuilds, is never committed or deployed, and reverses with
// `drush theme:dev off`. dq:static turns it off for the export so no debug
// comments leak into the static HTML.
if ($enableThemeDev) {
$this->io->writeln('🧑‍🎨 [drupalquick] Enabling Twig development mode (drush theme:dev on)...');
$devProc = Drush::drush(Drush::aliasManager()->getSelf(), 'theme:dev', ['on'], ['yes' => TRUE]);
$devProc->run();
if ($devProc->isSuccessful()) {
$this->io->writeln(' Twig debug + auto-reload on, caches off. Turn off with `drush theme:dev off`.');
}
else {
$this->io->warning('Could not enable Twig development mode (drush theme:dev on). Needs Drush 13.6+. Skipped; the build is otherwise complete.');
}
}

return self::SUCCESS;
}

Expand Down
Loading
Loading