{"id":51150975,"url":"https://github.com/irving-frias/drupal-watcher","last_synced_at":"2026-07-08T00:00:37.787Z","repository":{"id":221563340,"uuid":"754742489","full_name":"irving-frias/drupal-watcher","owner":"irving-frias","description":"This script monitors specified directories for file changes and clears the Drupal cache using Drush whenever a file is modified.","archived":false,"fork":false,"pushed_at":"2026-06-29T21:01:07.000Z","size":463,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-06-29T22:25:23.496Z","etag":null,"topics":["automation","drupal","drupal-project","drush","macosx","shell-script","watchdog"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/irving-frias.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2024-02-08T17:22:24.000Z","updated_at":"2026-06-29T21:01:16.000Z","dependencies_parsed_at":"2024-02-08T18:44:42.452Z","dependency_job_id":"d8781e78-542b-433a-b592-f64cd5767971","html_url":"https://github.com/irving-frias/drupal-watcher","commit_stats":{"total_commits":5,"total_committers":1,"mean_commits":5.0,"dds":0.0,"last_synced_commit":"4949a5afd0649d34e0f5bc38b796e3d8105e0934"},"previous_names":["irving-frias/drupal_watcher","irving-frias/drupal-watcher"],"tags_count":133,"template":false,"template_full_name":null,"purl":"pkg:github/irving-frias/drupal-watcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irving-frias%2Fdrupal-watcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irving-frias%2Fdrupal-watcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irving-frias%2Fdrupal-watcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irving-frias%2Fdrupal-watcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/irving-frias","download_url":"https://codeload.github.com/irving-frias/drupal-watcher/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/irving-frias%2Fdrupal-watcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35246561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","drupal","drupal-project","drush","macosx","shell-script","watchdog"],"created_at":"2026-06-26T06:01:01.077Z","updated_at":"2026-07-08T00:00:37.764Z","avatar_url":"https://github.com/irving-frias.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Drupal Watcher\r\n\r\nFile watcher for Drupal development. Monitors custom modules and themes and auto-runs `drush` cache clears whenever a file changes. Works with DDEV, Lando, and any local environment.\r\n\r\n## Requirements\r\n\r\n- **PHP 8.1+** (for Composer-based install)\r\n- **Drush** installed in your Drupal project\r\n\r\n\u003e Go is **not required**. The binary is auto-downloaded during `composer install`.\r\n\r\n\u003e **Development tool only.** Install with `--dev` to exclude from production deployments via `composer install --no-dev`.\r\n\r\n## Installation\r\n\r\n```bash\r\ncomposer require --dev irving-frias/drupal-watcher\r\nvendor/bin/drupal-watcher help\r\n```\r\n\r\nOn first run, the shell launcher downloads the correct binary for your OS/architecture from GitHub Releases. No compilation needed.\r\n\r\n\u003e If `vendor/bin/drupal-watcher` doesn't exist (e.g. on Windows), use the full path: `vendor/irving-frias/drupal-watcher/bin/drupal-watcher`\r\n\r\n### Manual download\r\n\r\nDownload the binary for your platform from [GitHub Releases](https://github.com/irving-frias/drupal-watcher/releases), make it executable, and run it from your Drupal project root.\r\n\r\n## Quick Start\r\n\r\n```bash\r\ncd /path/to/drupal/project\r\nvendor/bin/drupal-watcher start\r\n```\r\n\r\nOn first run, a `configs/config.yaml` is auto-created with defaults. Edit it to customize routes, patterns, and cache clear commands. You can also override any config value via environment variables (e.g. `DRUPAL_WATCHER_DEBOUNCE=150`).\r\n\r\nYou can validate your configuration and environment with:\r\n\r\n```bash\r\nvendor/bin/drupal-watcher validate\r\n```\r\n\r\nThis checks YAML syntax, Drupal root, watched routes, drush, PHPCS, sites, and `commandsPerPattern`.\r\n\r\nThe TUI opens automatically. Events appear in real-time, and you can type commands at the prompt:\r\n\r\n```\r\n  ● drupal-watcher  PID: 12345  Uptime: 5m\r\n  Memory: 2.1 MB ▂▃▄▅▆▇█  |  Changes: 14  |  Clears: 3  |  ⚡ x5  ▓▓▓▓░░░░  |  site1: 2  site2: 1\r\n\r\n  ┌──────────────────────────────────────────────────────────────┐\r\n  │ 10:00:01  ℹ  Waiting for file changes...                     │\r\n  │ 10:02:15  ℹ  Change detected: docroot/modules/custom/foo.module │\r\n  │ 10:02:16  ✔  drush cc plugin (312ms, exit 0)                │\r\n  │ 10:03:22  ✖  Error in docroot/modules/custom/bad.php:       │\r\n  │            PHP Parse error: syntax error, unexpected ...     │\r\n  └──────────────────────────────────────────────────────────────┘\r\n\r\n  ┌──────────────────────────────────────────────────────────────┐\r\n  \u003e help                                                         │\r\n  └──────────────────────────────────────────────────────────────┘\r\n```\r\n\r\nUse `--no-tui` to run the classic interactive CLI instead.\r\n\r\n## Commands\r\n\r\n| Command                    | Description                            |\r\n|----------------------------|----------------------------------------|\r\n| `start`                    | Start watching (opens TUI by default)  |\r\n| `validate`                 | Validate config, paths, drush, PHPCS   |\r\n| `tui`                      | Terminal UI (default for `start`)      |\r\n| `status`                   | Show running status and uptime         |\r\n| `list` / `config`          | Display current configuration          |\r\n| `add` \u003croute\u003e [pattern]    | Add route and/or pattern to watch      |\r\n| `remove` / `rm` \u003croute\u003e    | Remove route and/or pattern            |\r\n| `restart`                  | Restart the watcher                    |\r\n| `stop` / `reset`           | Stop the watcher and clear PID         |\r\n| `version`                  | Show version and Go runtime            |\r\n| `help`                     | Show usage information                 |\r\n\r\n## Options\r\n\r\n| Flag                    | Description                                   |\r\n|-------------------------|-----------------------------------------------|\r\n| `--root \u003cpath\u003e`         | Drupal root directory (default: cwd)          |\r\n| `--debounce \u003cms\u003e`       | Debounce interval (default: 800ms)            |\r\n| `--no-dotfiles`         | Exclude dotfiles from watching                |\r\n| `--no-tui`              | Disable TUI, use interactive CLI mode         |\r\n| `--notify`              | Send desktop notification on cache clear      |\r\n| `--log-file \u003cpath\u003e`     | Write logs to file                            |\r\n| `--config \u003cpath\u003e`       | Custom config file path                       |\r\n| `--commands-per-pattern \u003cjson\u003e` | Override per-pattern commands        |\r\n| `--site \u003cnames\u003e`        | Only watch specified sites (comma-separated)  |\r\n| `--exclude-site \u003cnames\u003e` | Watch all sites except specified              |\r\n| `--uri \u003curi\u003e`           | Single-site mode with explicit URI            |\r\n| `--help` / `-h`         | Show help                                     |\r\n| `--version` / `-V`      | Show version and Go runtime                   |\r\n\r\n### --notify\r\n\r\nSends a native OS desktop notification each time a cache clear completes:\r\n\r\n```bash\r\nvendor/bin/drupal-watcher start --notify\r\n```\r\n\r\nUses the `beeep` Go library for cross-platform desktop notifications. No OS-specific configuration required — works on macOS (via `osascript`), Linux (via `notify-send` or D-Bus), and Windows (via Toast notifications).\r\n\r\n### --root\r\n\r\nPoint the watcher at a different Drupal root:\r\n\r\n```bash\r\nvendor/bin/drupal-watcher start --root /var/www/html\r\nvendor/bin/drupal-watcher status --root /var/www/html\r\n```\r\n\r\n### --commands-per-pattern\r\n\r\nOverride per-pattern drush commands without editing the config file:\r\n\r\n```bash\r\nvendor/bin/drupal-watcher start --commands-per-pattern '{\"css\":\"cc css-js\",\"js\":\"cc css-js\"}'\r\n```\r\n\r\nAccepts a JSON object mapping file extensions to drush commands.\r\n\r\n## TUI Commands\r\n\r\nWhile the TUI is running, type commands at the prompt:\r\n\r\n| Command                       | Description                               |\r\n|-------------------------------|-------------------------------------------|\r\n| `status`                      | Show stats, memory, and kernel watches    |\r\n| `stats`                       | Clear counts per site                     |\r\n| `filter \u003csite\u003e`               | Filter events by site name                |\r\n| `help`                        | Show available commands and keybinds      |\r\n| `star`                        | Open GitHub repo in browser               |\r\n| `dismiss`                     | Hide the star banner permanently          |\r\n| `powermode`                   | Toggle PowerMode visual effects           |\r\n| `logo`                        | Toggle Drupal logo side panel             |\r\n| `dashboard`                   | Toggle live dashboard panel               |\r\n| `stop` / `quit` / `exit`      | Stop the watcher                          |\r\n\r\n### TUI keybinds\r\n\r\n| Key       | Description                                |\r\n|-----------|--------------------------------------------|\r\n| `Ctrl+C` / `Ctrl+D` | Quit                              |\r\n| `?`       | Toggle help / Esc to close                 |\r\n| `↑` / `↓` | Navigate command history                   |\r\n| `PgUp` / `PgDn` | Page up/down in event log              |\r\n| `Home`    | Scroll to top                              |\r\n| `End`     | Toggle auto-scroll                         |\r\n| `Tab`     | Complete commands / site names             |\r\n| `Insert`  | File system path scan for autocomplete     |\r\n| `Delete`  | Cancel pending completions                 |\r\n| `F2`      | Open interactive filter panel (extension)  |\r\n| `F4`      | Toggle PowerMode visual effects            |\r\n| `r`       | Get a context-aware training suggestion    |\r\n| `Ctrl+X`  | Disable Xdebug if detected                 |\r\n\r\nThe status line shows memory usage with a live sparkline, change/clear counters, and per-site clear breakdowns when multiple sites are active.\r\n\r\n## PowerMode — Overheat System 🔥\r\n\r\nPowerMode adds energetic visual feedback when the watcher detects rapid file changes. Inspired by the VS Code Power Mode extension, it turns bursts of activity into a dynamic overheating effect — as if the watcher is working so hard it's about to catch fire.\r\n\r\nWhen multiple file changes or cache clears arrive within a short window (2s), a combo counter builds up and the UI progressively **overheats**:\r\n\r\n| Combo | Level | Visual Effects |\r\n|---|---|---|\r\n| 0-2 | Normal | No effects |\r\n| 3-5 | Warm | Status bar border turns orange, combo counter (`⚡`) and energy bar appear |\r\n| 6-10 | Hot | Border shifts to deep orange, **sparks** (✦ ✧ ⚡) fly upward, screen pulses, combo icon becomes 🔥 |\r\n| 11+ | 🔥 Power | Red animated border, **fire explosions** (🔥 💥) burst on each level-up, embers float up, intense screen pulse, combo icon becomes 💥 |\r\n\r\nThe **combo counter** (⚡ x5 / 🔥 x8 / 💥 x12) and **energy bar** (`▓▓▓▓░░░░`) appear on the status line when activity ramps up. Energy decays during idle periods, cooling the system down.\r\n\r\n### Particle system\r\n\r\n| Particle type | Behavior | Characters |\r\n|---|---|---|\r\n| Sparks | Fast, short-lived, fly upward at random angles | ✦ ✧ ⚡ ★ ♦ |\r\n| Fire | Medium-lived, wobbling upward trajectory | 🔥 💥 ⚡ |\r\n| Smoke | Slow, rising, expands horizontally, long fade | · ‧ ∘ ° ≈ |\r\n\r\nOn each level transition (Warm→Hot, Hot→Power) a **radial explosion** bursts particles outward in all directions — 15 sparks at Hot, 25 at Power. The energy bar pulses yellow-white while the glow effect is active.\r\n\r\n### Cooldown ❄\r\n\r\nWhen events stop arriving and energy decays, the system enters **cooldown mode**:\r\n- Status bar border turns **blue**\r\n- Combo icon changes to ❄ (snowflake)\r\n- Energy bar pulses with a blue draining animation\r\n- **Smoke particles rise from the bottom** as the system cools — more smoke at higher levels\r\n- Status line shows a \"❄ cooling\" indicator\r\n- Cooldown tapers off gradually over ~30 ticks\r\n- A new event immediately cancels cooldown and resumes heating\r\n\r\n### Massive batch — 💀 Skull of Death\r\n\r\nWhen 50+ files change in a single batch (e.g. `drush cex`, git checkout, composer install), PowerMode triggers a **skull event**:\r\n- Combo icon becomes 💀, status line shows \"💀 MASSIVE BATCH\"\r\n- Border flashes white/red rapidly\r\n- Energy maxes out instantly, particles explode\r\n- Large 💀 particles float upward for several seconds\r\n- At 100+ files: 2 skulls; at 200+ files: 3 skulls\r\n- Skull timer lasts ~20 ticks\r\n\r\nToggle PowerMode on/off at any time with `F4` or the `powermode` command.\r\n\r\n## Drupal Logo — Side Panel\r\n\r\nAn animated ASCII Drupal drop logo rotates in a side panel on the right of the events viewport. The logo cycles through6 frames at 1 frame per second (6-second full rotation).\r\n\r\nThe side panel appears when `showLogo` is enabled (default: `true`). On narrow terminals (\u003c 60 cols), the logo auto-hides to preserve readability.\r\n\r\n```bash\r\nlogo    # toggle the side panel on/off\r\n```\r\n\r\nThe preference is persisted to `configs/config.yaml` and survives restarts. Override with `DRUPAL_WATCHER_SHOW_LOGO=false`.\r\n\r\n## Interactive CLI Commands\r\n\r\nWhen running with `--no-tui`, type commands at the prompt:\r\n\r\n| Command                | Description                            |\r\n|------------------------|----------------------------------------|\r\n| `status`               | Show stats, memory, and kernel watches |\r\n| `list` / `config`      | Show current configuration             |\r\n| `stats`                | Show runtime statistics and memory     |\r\n| `add \u003croute\u003e`          | Add a route and restart watcher        |\r\n| `remove \u003croute\u003e`       | Remove a route and restart watcher     |\r\n| `reload`               | Reload config from file                |\r\n| `help`                 | Show available commands                |\r\n| `stop` / `quit` / `exit` | Stop the watcher                     |\r\n\r\n## Configuration\r\n\r\nConfig is read from `configs/config.yaml` (auto-created on first run). Falls back to `watcher.config.json` for legacy setups.\r\n\r\n```yaml\r\nroutes:\r\n  - docroot/modules/custom\r\n  - docroot/themes/custom\r\npatterns:\r\n  - .php; .module; .inc; .yml; .html.twig; .twig; .css; .js\r\ndebounce: 800\r\ncommandsPerPattern:\r\n  .html.twig: cc render\r\n  .twig: cc render\r\n  .theme: cc theme-registry\r\n  .module: cc plugin\r\n  .inc: cc plugin\r\n  .php: cc plugin\r\n  .yml: cc plugin\r\n  .info.yml: cr\r\n  .services.yml: cr\r\n  .routing.yml: cr\r\n  .permissions.yml: cr\r\n  .links.menu.yml: cr\r\n  .css: cc css-js\r\n  .js: cc css-js\r\nskipLint: false\r\nlintCommands:\r\n  .php: php -l\r\n  .yml: yaml\r\n  .yaml: yaml\r\nphpCsStandard: \"\"\r\nwatchMode: auto\r\npollInterval: 2000\r\neventBufferSize: 500\r\n```\r\n\r\nAny value can be overridden via environment variables with the `DRUPAL_WATCHER_` prefix:\r\n\r\n| Env var | Config key | Example |\r\n|---|---|---|\r\n| `DRUPAL_WATCHER_DEBOUNCE` | `debounce` | `DRUPAL_WATCHER_DEBOUNCE=500` |\r\n| `DRUPAL_WATCHER_DRUSH_COMMAND` | `drushCommand` | `DRUPAL_WATCHER_DRUSH_COMMAND=cc all` |\r\n| `DRUPAL_WATCHER_SKIP_LINT` | `skipLint` | `DRUPAL_WATCHER_SKIP_LINT=true` |\r\n| `DRUPAL_WATCHER_WATCH_MODE` | `watchMode` | `DRUPAL_WATCHER_WATCH_MODE=poll` |\r\n| `DRUPAL_WATCHER_SHOW_LOGO` | `showLogo` | `DRUPAL_WATCHER_SHOW_LOGO=false` |\r\n| `DRUPAL_WATCHER_POLL_INTERVAL` | `pollInterval` | `DRUPAL_WATCHER_POLL_INTERVAL=1000` |\r\n\r\n| Field                 | Description                                                  |\r\n|-----------------------|--------------------------------------------------------------|\r\n| `routes`              | Directories to watch (relative to Drupal root)               |\r\n| `patterns`            | File extensions to trigger cache clears on                   |\r\n| `debounce`            | Milliseconds to wait before running drush after a change     |\r\n| `drushCmd`            | Custom path to the drush binary (auto-detected if omitted)   |\r\n| `drushCommand`        | Default drush command (default: `cr`)                        |\r\n| `drushArgs`           | Extra arguments to pass to drush                             |\r\n| `commandsPerPattern`  | Maps file extensions to specific drush commands              |\r\n| `excludePatterns`     | Path substrings to exclude from watching                     |\r\n| `Sites`               | Site names to watch in multi-site setups (resolved via `drush/sites.yml`) |\r\n| `skipLint`            | Disable lint checking before cache clear                     |\r\n| `lintCommands`        | Per-extension lint commands (default: `php -l` for PHP, Go yaml parser for YAML). Only checks files inside `routes`. |\r\n| `phpCsStandard`       | PHPCS standard for PHP linting, e.g. `\"auto\"`, `\"Drupal\"`, `\"DrupalStrict\"`. When set, replaces `php -l` with `phpcs` using Drupal coding standards. `\"auto\"` detects Drupal 11 → `DrupalStrict`, else `Drupal`. Empty string (default) keeps `php -l`. |\r\n| `watchMode`           | File watching mode: `auto`, `fsnotify`, `poll`, `hybrid`     |\r\n| `pollInterval`        | Polling interval in ms (default 2000, only used in poll/hybrid modes) |\r\n| `showLogo`            | Show animated Drupal logo in side panel (default: `true`). Toggle with `logo` command. |\r\n\r\n**commandsPerPattern** maps file extensions to drush commands. The most specific match wins (e.g., `.info.yml` matches before `.yml`). Falls back to `cr` if no pattern matches.\r\n\r\n### Watch modes\r\n\r\n| Mode | Description |\r\n|---|---|\r\n| `auto` (default) | Tries fsnotify first; falls back to polling if OS limits are hit |\r\n| `fsnotify` | Native file system events only (lowest latency) |\r\n| `poll` | Periodic file tree scan at `pollInterval` (works around OS limits) |\r\n| `hybrid` | Runs both fsnotify and polling simultaneously; events are deduplicated within a 1s window. Provides the reliability of polling with the low latency of fsnotify. |\r\n\r\nConfig via `watchMode` in `configs/config.yaml` (or legacy `watcher.config.json`) or override per session with `DRUPAL_WATCHER_WATCH_MODE=poll`. Polling mode is useful in large projects that exceed `fs.inotify.max_user_watches` on Linux, or when running in shared filesystems like NFS.\r\n\r\n## How it works\r\n\r\n1. `drupal-watcher start` loads config, detects the Drupal docroot, and writes a PID file\r\n2. Uses `fsnotify` to watch all subdirectories under the configured routes (falls back to polling if fsnotify fails, or use hybrid mode for both)\r\n3. When files change, debounces (default 800ms) collecting all changes into a batch. File changes appear instantly in the TUI event log — only the cache clear waits for the debounce window.\r\n4. **PHP and YAML files are linted** before running drush (`php -l` or `phpcs` for PHP, Go yaml parser for YAML). Lint results are cached with a SHA-1 content hash and 5-minute TTL — unchanged files are not re-checked. Only files inside watched `routes` are checked. If linting fails, the cache clear is skipped and the error (with file path) is displayed in the TUI.\r\n\r\n   When `phpCsStandard` is set in the config, PHP files are checked with `phpcs` using Drupal coding standards (auto-detects `DrupalStrict` for Drupal 11, `Drupal` for Drupal 10). Requires `drupal/coder` and `squizlabs/php_codesniffer` installed via Composer.\r\n5. Compatible cache clear commands are merged into a single `drush` call (e.g. `drush cc render,plugin,css-js`)\r\n6. If any change requires a full rebuild (`cr`), it applies a **lazy rebuild** — a separate 2-second debounce timer accumulates all changes and executes a single `drush cr` at the end of the burst, avoiding redundant full rebuilds\r\n7. Drush output is displayed in the TUI or printed to the terminal\r\n8. A health file is written to `~/.cache/drupal-watcher/health` every 30s (cleaned up on shutdown)\r\n9. Metrics (changes, clears, errors per minute) are tracked in-memory for the training mode and `stats` command\r\n10. `Ctrl+C` (or `SIGTERM`) cancels the context, stops all modules with a 10s timeout, removes PID and health files\r\n\r\n### Drush optimizations\r\n\r\nThe watcher applies several optimizations to minimize overhead:\r\n\r\n| Optimization | Description |\r\n|---|---|\r\n| **Binary caching** | Resolved `drush` path cached after first lookup, avoids repeated `$PATH` scans |\r\n| **Batch cache clears** | Multiple `cc \u003ctype\u003e` commands merged into a single `drush cc type1,type2,...` call |\r\n| **`cr` overrides** | If any change requires `drush cr`, a 2-second lazy rebuild timer accumulates all changes first |\r\n| **Lint cache** | SHA-1 content hash avoids re-linting unchanged files (5-minute TTL, max 1000 entries) |\r\n| **Parallel multi-site** | Per-site `drush` runs in a worker pool (up to 3 concurrent) for multi-site projects |\r\n| **Quiet mode** | Drush runs with `--quiet --no-ansi` by default for minimal output overhead |\r\n\r\nOn large debounce windows, rapid changes to different file types share a single PHP bootstrap instead of spawning separate processes.\r\n\r\n### Drupal root detection\r\n\r\nThe watcher scans for `docroot/`, `web/`, `public/`, or `html/` directories containing `core/`, `modules/`, `themes/`, or `index.php`. The detected root is stored in the config file.\r\n\r\n### Cache clear per pattern\r\n\r\n| Extension          | Drush command       |\r\n|--------------------|---------------------|\r\n| `.html.twig`       | `cc render`         |\r\n| `.twig`            | `cc render`         |\r\n| `.theme`           | `cc theme-registry` |\r\n| `.module`          | `cc plugin`         |\r\n| `.inc`             | `cc plugin`         |\r\n| `.php`             | `cc plugin`         |\r\n| `.yml`             | `cc plugin`         |\r\n| `.info.yml`        | `cr`                |\r\n| `.services.yml`    | `cr`                |\r\n| `.routing.yml`     | `cr`                |\r\n| `.permissions.yml` | `cr`                |\r\n| `.links.menu.yml`  | `cr`                |\r\n| `.css`             | `cc css-js`         |\r\n| `.js`              | `cc css-js`         |\r\n\r\n### Twig debug mode (development only)\r\n\r\nEnable Twig development mode without touching settings.php:\r\n\r\n```bash\r\ndrush twig:debug on   # enables Twig debug + auto-disable cache\r\ndrush twig:debug off  # restores production settings\r\n```\r\n\r\nAvailable since Drush 12.1+. Handles `twig.config` settings automatically — no manual cache clears needed.\r\n\r\n## Multi-site\r\n\r\nDrupal Watcher supports [multi-site](https://www.drupal.org/docs/developing/multisite-drupal) setups with a single watcher process. When multiple sites are detected under `sites/`, drush runs in parallel goroutines for each site on every file change.\r\n\r\n### Auto-detection\r\n\r\nThe watcher checks for directories under `sites/` beyond `default/` that contain `settings.php`. If only `sites/default/` exists, single-site mode is used (no changes to existing workflows).\r\n\r\n### Site aliases (required for multi-site)\r\n\r\nWhen multiple sites are detected, you **must** configure site aliases so the watcher knows each site's URI. The watcher supports both formats Drush accepts:\r\n\r\n#### Combined file: `drush/sites.yml`\r\n\r\n```yaml\r\n# docroot/drush/sites.yml\r\nsite1:\r\n  uri: 'https://site1.local'\r\nsite2:\r\n  uri: 'https://site2.local'\r\n```\r\n\r\n#### Per-site files: `drush/sites/{name}.site.yml`\r\n\r\n```\r\ndrush/\r\n├── drush.yml\r\n└── sites\r\n    ├── site1.site.yml\r\n    ├── site2.site.yml\r\n    └── site3.site.yml\r\n```\r\n\r\nEach `{name}.site.yml` file defines one site alias. For example:\r\n\r\n```yaml\r\n# drush/sites/site1.site.yml\r\nsite1:\r\n  uri: 'https://site1.local'\r\n```\r\n\r\n```yaml\r\n# drush/sites/site2.site.yml\r\nsite2:\r\n  uri: 'https://site2.local'\r\n```\r\n\r\nThe per-site directory format is discovered automatically by scanning `drush/sites/*.site.yml`. The two formats are mutually exclusive — if `drush/sites.yml` exists, the directory is ignored.\r\n\r\nThe watcher does **not** guess URIs from directory names — a site alias can differ from its directory name. See [Drush site aliases docs](https://www.drush.org/latest/using-drush/site-aliases/) for details.\r\n\r\nIf multi-site is detected and neither `drush/sites.yml` nor `drush/sites/*.site.yml` exists, the watcher exits with an error and instructions.\r\n\r\n### Filtering sites\r\n\r\n| Flag | Example | Description |\r\n|---|---|---|\r\n| `--site` | `--site=site1,site2` | Whitelist — only watch these sites |\r\n| `--exclude-site` | `--exclude-site=site3` | Blacklist — watch all except these |\r\n| `--uri` | `--uri=https://site1.local` | Override single-site mode with a specific URI (skips detection) |\r\n\r\n### Config persistence\r\n\r\nThe `Sites` field in `watcher.config.json` persists the site list from a previous run:\r\n\r\n```json\r\n{\r\n  \"routes\": [\"docroot/modules/custom\"],\r\n  \"Sites\": [\"site1\", \"site2\"]\r\n}\r\n```\r\n\r\nWhen `Sites` is present in the config file, it's auto-resolved against `drush/sites.yml` on startup. This is useful when you always work with the same subset of sites.\r\n\r\n### TUI display\r\n\r\nEvents in the TUI are tagged with the site name:\r\n\r\n```\r\n10:00:01  ✔  drush cc plugin [site1] (312ms, exit 0)\r\n10:00:01  ✔  drush cc plugin [site2] (289ms, exit 0)\r\n```\r\n\r\n## PID management\r\n\r\nThe watcher writes a PID file to `~/.cache/drupal-watcher/.drupal-watcher-\u003cproject-hash\u003e.pid` (`0600` permissions) to prevent multiple instances. The filename includes a hash of the project root, so you can run the watcher in multiple projects without conflicts. If the process crashes, stale PID files are cleaned up automatically on the next `start`.\r\n\r\n## Architecture\r\n\r\nThe codebase uses a **hexagonal (ports \u0026 adapters)** architecture:\r\n\r\n```\r\ncmd/\r\n  drupal-watcher/        → Binary (modular entry point with DI container + EventBus)\r\n\r\ninternal/\r\n  app/\r\n    app.go               → DI setup (Setup/Shutdown via samber/do/v2)\r\n    common/types.go      → Typed string wrappers (WorkDir, DrupalRoot)\r\n    eventbus/\r\n      bus.go             → Pub/sub event bus (async, topic-based)\r\n    modules/\r\n      config/            → Config module (loads configs/config.yaml, stores in container)\r\n      watcher/           → Watcher module (creates FSNotifyWatcher from config)\r\n      executor/          → Executor module (creates DrushExecutor from config)\r\n      orchestrator/      → Orchestrator module (engine with EventBus, starts in goroutine)\r\n      ui/                → UI module (runs Bubble Tea TUI, blocks until quit)\r\n        providers/tui/   → TUI bridge (EventBus → EngineEvent channel)\r\n\r\n  config/                → Config management (YAML + env vars), Drupal root detection, PID files\r\n  health/                → Liveness check (timestamp file every 30s)\r\n  drush/                 → Drush resolution, execution, health checks\r\n  metrics/               → Runtime statistics (changes, clears, errors per minute)\r\n  training/              → Context-aware training suggestions (training.json)\r\n  validate/              → Config and environment validation (`validate` command)\r\n  xdebug/                → Xdebug detection and disable (Ctrl+X)\r\n  utils/                 → Color helpers, format utilities\r\n\r\npkg/\r\n  core/\r\n    interfaces.go        → Watcher, CommandExecutor, EventFilter, LintChecker\r\n    models.go            → FileEvent, ExecutionResult, EngineEvent, SiteInfo\r\n  adapters/\r\n    fsnotify_watcher.go  → core.Watcher via fsnotify\r\n    polling_watcher.go   → core.Watcher via periodic file tree scan\r\n    hybrid_watcher.go    → core.Watcher (fsnotify + polling, deduped)\r\n    drush_executor.go    → core.CommandExecutor via drush\r\n    regex_filter.go      → Pattern/Exclude/Dotfile filters\r\n    php_lint.go          → core.LintChecker via php -l\r\n    yaml_lint.go         → core.LintChecker via Go yaml parser\r\n    lint_cache.go        → Caching wrapper for LintChecker (SHA-1, 5min TTL)\r\n    phpcs_lint.go        → core.LintChecker via phpcs (Drupal standards)\r\n    slog_logger.go       → Structured logger factory\r\n```\n\r\n### Key domain interfaces\r\n\r\n```go\r\n// pkg/core/interfaces.go\r\n\r\ntype Watcher interface {\r\n    Start(ctx context.Context) (\u003c-chan FileEvent, \u003c-chan error)\r\n    Add(path string) error\r\n    Remove(path string) error\r\n    Close() error\r\n}\r\n\r\ntype CommandExecutor interface {\r\n    Execute(ctx context.Context, commands []string, dir string) ExecutionResult\r\n}\r\n\r\ntype EventFilter interface {\r\n    ShouldProcess(event FileEvent) bool\r\n}\r\n\r\ntype LintChecker interface {\r\n    Lint(filePath string) *LintResult\r\n}\r\n\r\ntype LintResult struct {\r\n    File  string\r\n    Error string\r\n}\r\n```\r\n\r\n### Engine event loop\r\n\r\nThe orchestrator (`internal/app/modules/orchestrator/engine.go`) runs the central pipeline:\r\n\r\n1. Watcher emits raw `FileEvent` on a channel (via fsnotify, polling, or hybrid)\r\n2. All `EventFilter` implementations decide if the event should be processed\r\n3. Debounce timer groups rapid changes into a single batch\r\n4. **Lint check**: each changed file is checked by its `LintChecker` (`.php` → `php -l`, `.yml` → Go yaml parser). If any file fails, the batch is skipped and an `error` EventBus event is published.\r\n5. Matching file extensions are resolved to drush commands via `CommandsPerPattern`\r\n6. `CommandExecutor` runs the resolved commands\r\n7. `EngineEvent` is published to the EventBus on `file.change` and `cache.clear` topics\r\n\r\n---\r\n\r\n## Development (requires Go 1.25+)\r\n\r\n```bash\r\ngo test -count=1 ./...\r\ngo vet ./...\r\ngo build -o drupal-watcher ./cmd/drupal-watcher\r\n```\r\n\r\n### Cross-compilation\r\n\r\n```bash\r\nGOOS=linux   GOARCH=amd64 go build -o drupal-watcher-linux-amd64   ./cmd/drupal-watcher\r\nGOOS=linux   GOARCH=arm64 go build -o drupal-watcher-linux-arm64   ./cmd/drupal-watcher\r\nGOOS=darwin  GOARCH=amd64 go build -o drupal-watcher-darwin-amd64  ./cmd/drupal-watcher\r\nGOOS=darwin  GOARCH=arm64 go build -o drupal-watcher-darwin-arm64  ./cmd/drupal-watcher\r\nGOOS=windows GOARCH=amd64 go build -o drupal-watcher-windows-amd64.exe ./cmd/drupal-watcher\r\n```\r\n\r\nCI builds all platforms automatically on push to `main` and publishes releases.\r\n\r\n## License\r\n\r\nMIT\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firving-frias%2Fdrupal-watcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Firving-frias%2Fdrupal-watcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Firving-frias%2Fdrupal-watcher/lists"}