PHPackages                             irving-frias/drupal-watcher - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. irving-frias/drupal-watcher

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

irving-frias/drupal-watcher
===========================

Drupal file watcher — monitors custom modules/themes and auto-runs drush cache clears on file changes. Development tool only, not required in production.

1.2.83(1mo ago)6131MITGoPHP &gt;=8.1CI passing

Since Jun 23Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/irving-frias/drupal-watcher)[ Packagist](https://packagist.org/packages/irving-frias/drupal-watcher)[ RSS](/packages/irving-frias-drupal-watcher/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)DependenciesVersions (140)Used By (0)

Drupal Watcher
==============

[](#drupal-watcher)

File 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.

Requirements
------------

[](#requirements)

- **PHP 8.1+** (for Composer-based install)
- **Drush** installed in your Drupal project

> Go is **not required**. The binary is auto-downloaded during `composer install`.

> **Development tool only.** Install with `--dev` to exclude from production deployments via `composer install --no-dev`.

Installation
------------

[](#installation)

```
composer require --dev irving-frias/drupal-watcher
vendor/bin/drupal-watcher help
```

On first run, the shell launcher downloads the correct binary for your OS/architecture from GitHub Releases. No compilation needed.

> If `vendor/bin/drupal-watcher` doesn't exist (e.g. on Windows), use the full path: `vendor/irving-frias/drupal-watcher/bin/drupal-watcher`

### Manual download

[](#manual-download)

Download 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.

Quick Start
-----------

[](#quick-start)

```
cd /path/to/drupal/project
vendor/bin/drupal-watcher start
```

On 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`).

You can validate your configuration and environment with:

```
vendor/bin/drupal-watcher validate
```

This checks YAML syntax, Drupal root, watched routes, drush, PHPCS, sites, and `commandsPerPattern`.

The TUI opens automatically. Events appear in real-time, and you can type commands at the prompt:

```
  ● drupal-watcher  PID: 12345  Uptime: 5m
  Memory: 2.1 MB ▂▃▄▅▆▇█  |  Changes: 14  |  Clears: 3  |  ⚡ x5  ▓▓▓▓░░░░  |  site1: 2  site2: 1

  ┌──────────────────────────────────────────────────────────────┐
  │ 10:00:01  ℹ  Waiting for file changes...                     │
  │ 10:02:15  ℹ  Change detected: docroot/modules/custom/foo.module │
  │ 10:02:16  ✔  drush cc plugin (312ms, exit 0)                │
  │ 10:03:22  ✖  Error in docroot/modules/custom/bad.php:       │
  │            PHP Parse error: syntax error, unexpected ...     │
  └──────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────┐
  > help                                                         │
  └──────────────────────────────────────────────────────────────┘

```

Use `--no-tui` to run the classic interactive CLI instead.

Commands
--------

[](#commands)

CommandDescription`start`Start watching (opens TUI by default)`validate`Validate config, paths, drush, PHPCS`tui`Terminal UI (default for `start`)`status`Show running status and uptime`list` / `config`Display current configuration`add` \[pattern\]Add route and/or pattern to watch`remove` / `rm` Remove route and/or pattern`restart`Restart the watcher`stop` / `reset`Stop the watcher and clear PID`version`Show version and Go runtime`help`Show usage informationOptions
-------

[](#options)

FlagDescription`--root `Drupal root directory (default: cwd)`--debounce `Debounce interval (default: 800ms)`--no-dotfiles`Exclude dotfiles from watching`--no-tui`Disable TUI, use interactive CLI mode`--notify`Send desktop notification on cache clear`--log-file `Write logs to file`--config `Custom config file path`--commands-per-pattern `Override per-pattern commands`--site `Only watch specified sites (comma-separated)`--exclude-site `Watch all sites except specified`--uri `Single-site mode with explicit URI`--help` / `-h`Show help`--version` / `-V`Show version and Go runtime### --notify

[](#--notify)

Sends a native OS desktop notification each time a cache clear completes:

```
vendor/bin/drupal-watcher start --notify
```

Uses 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).

### --root

[](#--root)

Point the watcher at a different Drupal root:

```
vendor/bin/drupal-watcher start --root /var/www/html
vendor/bin/drupal-watcher status --root /var/www/html
```

### --commands-per-pattern

[](#--commands-per-pattern)

Override per-pattern drush commands without editing the config file:

```
vendor/bin/drupal-watcher start --commands-per-pattern '{"css":"cc css-js","js":"cc css-js"}'
```

Accepts a JSON object mapping file extensions to drush commands.

TUI Commands
------------

[](#tui-commands)

While the TUI is running, type commands at the prompt:

CommandDescription`status`Show stats, memory, and kernel watches`stats`Clear counts per site`filter `Filter events by site name`help`Show available commands and keybinds`star`Open GitHub repo in browser`dismiss`Hide the star banner permanently`powermode`Toggle PowerMode visual effects`logo`Toggle Drupal logo side panel`dashboard`Toggle live dashboard panel`stop` / `quit` / `exit`Stop the watcher### TUI keybinds

[](#tui-keybinds)

KeyDescription`Ctrl+C` / `Ctrl+D`Quit`?`Toggle help / Esc to close`↑` / `↓`Navigate command history`PgUp` / `PgDn`Page up/down in event log`Home`Scroll to top`End`Toggle auto-scroll`Tab`Complete commands / site names`Insert`File system path scan for autocomplete`Delete`Cancel pending completions`F2`Open interactive filter panel (extension)`F4`Toggle PowerMode visual effects`r`Get a context-aware training suggestion`Ctrl+X`Disable Xdebug if detectedThe status line shows memory usage with a live sparkline, change/clear counters, and per-site clear breakdowns when multiple sites are active.

PowerMode — Overheat System 🔥
-----------------------------

[](#powermode--overheat-system-)

PowerMode 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.

When multiple file changes or cache clears arrive within a short window (2s), a combo counter builds up and the UI progressively **overheats**:

ComboLevelVisual Effects0-2NormalNo effects3-5WarmStatus bar border turns orange, combo counter (`⚡`) and energy bar appear6-10HotBorder shifts to deep orange, **sparks** (✦ ✧ ⚡) fly upward, screen pulses, combo icon becomes 🔥11+🔥 PowerRed animated border, **fire explosions** (🔥 💥) burst on each level-up, embers float up, intense screen pulse, combo icon becomes 💥The **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.

### Particle system

[](#particle-system)

Particle typeBehaviorCharactersSparksFast, short-lived, fly upward at random angles✦ ✧ ⚡ ★ ♦FireMedium-lived, wobbling upward trajectory🔥 💥 ⚡SmokeSlow, rising, expands horizontally, long fade· ‧ ∘ ° ≈On 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.

### Cooldown ❄

[](#cooldown-)

When events stop arriving and energy decays, the system enters **cooldown mode**:

- Status bar border turns **blue**
- Combo icon changes to ❄ (snowflake)
- Energy bar pulses with a blue draining animation
- **Smoke particles rise from the bottom** as the system cools — more smoke at higher levels
- Status line shows a "❄ cooling" indicator
- Cooldown tapers off gradually over ~30 ticks
- A new event immediately cancels cooldown and resumes heating

### Massive batch — 💀 Skull of Death

[](#massive-batch---skull-of-death)

When 50+ files change in a single batch (e.g. `drush cex`, git checkout, composer install), PowerMode triggers a **skull event**:

- Combo icon becomes 💀, status line shows "💀 MASSIVE BATCH"
- Border flashes white/red rapidly
- Energy maxes out instantly, particles explode
- Large 💀 particles float upward for several seconds
- At 100+ files: 2 skulls; at 200+ files: 3 skulls
- Skull timer lasts ~20 ticks

Toggle PowerMode on/off at any time with `F4` or the `powermode` command.

Drupal Logo — Side Panel
------------------------

[](#drupal-logo--side-panel)

An 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).

The side panel appears when `showLogo` is enabled (default: `true`). On narrow terminals (&lt; 60 cols), the logo auto-hides to preserve readability.

```
logo    # toggle the side panel on/off
```

The preference is persisted to `configs/config.yaml` and survives restarts. Override with `DRUPAL_WATCHER_SHOW_LOGO=false`.

Interactive CLI Commands
------------------------

[](#interactive-cli-commands)

When running with `--no-tui`, type commands at the prompt:

CommandDescription`status`Show stats, memory, and kernel watches`list` / `config`Show current configuration`stats`Show runtime statistics and memory`add `Add a route and restart watcher`remove `Remove a route and restart watcher`reload`Reload config from file`help`Show available commands`stop` / `quit` / `exit`Stop the watcherConfiguration
-------------

[](#configuration)

Config is read from `configs/config.yaml` (auto-created on first run). Falls back to `watcher.config.json` for legacy setups.

```
routes:
  - docroot/modules/custom
  - docroot/themes/custom
patterns:
  - .php; .module; .inc; .yml; .html.twig; .twig; .css; .js
debounce: 800
commandsPerPattern:
  .html.twig: cc render
  .twig: cc render
  .theme: cc theme-registry
  .module: cc plugin
  .inc: cc plugin
  .php: cc plugin
  .yml: cc plugin
  .info.yml: cr
  .services.yml: cr
  .routing.yml: cr
  .permissions.yml: cr
  .links.menu.yml: cr
  .css: cc css-js
  .js: cc css-js
skipLint: false
lintCommands:
  .php: php -l
  .yml: yaml
  .yaml: yaml
phpCsStandard: ""
watchMode: auto
pollInterval: 2000
eventBufferSize: 500
```

Any value can be overridden via environment variables with the `DRUPAL_WATCHER_` prefix:

Env varConfig keyExample`DRUPAL_WATCHER_DEBOUNCE``debounce``DRUPAL_WATCHER_DEBOUNCE=500``DRUPAL_WATCHER_DRUSH_COMMAND``drushCommand``DRUPAL_WATCHER_DRUSH_COMMAND=cc all``DRUPAL_WATCHER_SKIP_LINT``skipLint``DRUPAL_WATCHER_SKIP_LINT=true``DRUPAL_WATCHER_WATCH_MODE``watchMode``DRUPAL_WATCHER_WATCH_MODE=poll``DRUPAL_WATCHER_SHOW_LOGO``showLogo``DRUPAL_WATCHER_SHOW_LOGO=false``DRUPAL_WATCHER_POLL_INTERVAL``pollInterval``DRUPAL_WATCHER_POLL_INTERVAL=1000`FieldDescription`routes`Directories to watch (relative to Drupal root)`patterns`File extensions to trigger cache clears on`debounce`Milliseconds to wait before running drush after a change`drushCmd`Custom path to the drush binary (auto-detected if omitted)`drushCommand`Default drush command (default: `cr`)`drushArgs`Extra arguments to pass to drush`commandsPerPattern`Maps file extensions to specific drush commands`excludePatterns`Path substrings to exclude from watching`Sites`Site names to watch in multi-site setups (resolved via `drush/sites.yml`)`skipLint`Disable lint checking before cache clear`lintCommands`Per-extension lint commands (default: `php -l` for PHP, Go yaml parser for YAML). Only checks files inside `routes`.`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`.`watchMode`File watching mode: `auto`, `fsnotify`, `poll`, `hybrid``pollInterval`Polling interval in ms (default 2000, only used in poll/hybrid modes)`showLogo`Show animated Drupal logo in side panel (default: `true`). Toggle with `logo` command.**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.

### Watch modes

[](#watch-modes)

ModeDescription`auto` (default)Tries fsnotify first; falls back to polling if OS limits are hit`fsnotify`Native file system events only (lowest latency)`poll`Periodic file tree scan at `pollInterval` (works around OS limits)`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.Config 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.

How it works
------------

[](#how-it-works)

1. `drupal-watcher start` loads config, detects the Drupal docroot, and writes a PID file
2. Uses `fsnotify` to watch all subdirectories under the configured routes (falls back to polling if fsnotify fails, or use hybrid mode for both)
3. 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.
4. **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.

    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.
5. Compatible cache clear commands are merged into a single `drush` call (e.g. `drush cc render,plugin,css-js`)
6. 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
7. Drush output is displayed in the TUI or printed to the terminal
8. A health file is written to `~/.cache/drupal-watcher/health` every 30s (cleaned up on shutdown)
9. Metrics (changes, clears, errors per minute) are tracked in-memory for the training mode and `stats` command
10. `Ctrl+C` (or `SIGTERM`) cancels the context, stops all modules with a 10s timeout, removes PID and health files

### Drush optimizations

[](#drush-optimizations)

The watcher applies several optimizations to minimize overhead:

OptimizationDescription**Binary caching**Resolved `drush` path cached after first lookup, avoids repeated `$PATH` scans**Batch cache clears**Multiple `cc ` commands merged into a single `drush cc type1,type2,...` call**`cr` overrides**If any change requires `drush cr`, a 2-second lazy rebuild timer accumulates all changes first**Lint cache**SHA-1 content hash avoids re-linting unchanged files (5-minute TTL, max 1000 entries)**Parallel multi-site**Per-site `drush` runs in a worker pool (up to 3 concurrent) for multi-site projects**Quiet mode**Drush runs with `--quiet --no-ansi` by default for minimal output overheadOn large debounce windows, rapid changes to different file types share a single PHP bootstrap instead of spawning separate processes.

### Drupal root detection

[](#drupal-root-detection)

The 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.

### Cache clear per pattern

[](#cache-clear-per-pattern)

ExtensionDrush command`.html.twig``cc render``.twig``cc render``.theme``cc theme-registry``.module``cc plugin``.inc``cc plugin``.php``cc plugin``.yml``cc plugin``.info.yml``cr``.services.yml``cr``.routing.yml``cr``.permissions.yml``cr``.links.menu.yml``cr``.css``cc css-js``.js``cc css-js`### Twig debug mode (development only)

[](#twig-debug-mode-development-only)

Enable Twig development mode without touching settings.php:

```
drush twig:debug on   # enables Twig debug + auto-disable cache
drush twig:debug off  # restores production settings
```

Available since Drush 12.1+. Handles `twig.config` settings automatically — no manual cache clears needed.

Multi-site
----------

[](#multi-site)

Drupal 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.

### Auto-detection

[](#auto-detection)

The 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).

### Site aliases (required for multi-site)

[](#site-aliases-required-for-multi-site)

When multiple sites are detected, you **must** configure site aliases so the watcher knows each site's URI. The watcher supports both formats Drush accepts:

#### Combined file: `drush/sites.yml`

[](#combined-file-drushsitesyml)

```
# docroot/drush/sites.yml
site1:
  uri: 'https://site1.local'
site2:
  uri: 'https://site2.local'
```

#### Per-site files: `drush/sites/{name}.site.yml`

[](#per-site-files-drushsitesnamesiteyml)

```
drush/
├── drush.yml
└── sites
    ├── site1.site.yml
    ├── site2.site.yml
    └── site3.site.yml

```

Each `{name}.site.yml` file defines one site alias. For example:

```
# drush/sites/site1.site.yml
site1:
  uri: 'https://site1.local'
```

```
# drush/sites/site2.site.yml
site2:
  uri: 'https://site2.local'
```

The 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.

The 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.

If multi-site is detected and neither `drush/sites.yml` nor `drush/sites/*.site.yml` exists, the watcher exits with an error and instructions.

### Filtering sites

[](#filtering-sites)

FlagExampleDescription`--site``--site=site1,site2`Whitelist — only watch these sites`--exclude-site``--exclude-site=site3`Blacklist — watch all except these`--uri``--uri=https://site1.local`Override single-site mode with a specific URI (skips detection)### Config persistence

[](#config-persistence)

The `Sites` field in `watcher.config.json` persists the site list from a previous run:

```
{
  "routes": ["docroot/modules/custom"],
  "Sites": ["site1", "site2"]
}
```

When `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.

### TUI display

[](#tui-display)

Events in the TUI are tagged with the site name:

```
10:00:01  ✔  drush cc plugin [site1] (312ms, exit 0)
10:00:01  ✔  drush cc plugin [site2] (289ms, exit 0)

```

PID management
--------------

[](#pid-management)

The watcher writes a PID file to `~/.cache/drupal-watcher/.drupal-watcher-.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`.

Architecture
------------

[](#architecture)

The codebase uses a **hexagonal (ports &amp; adapters)** architecture:

```
cmd/
  drupal-watcher/        → Binary (modular entry point with DI container + EventBus)

internal/
  app/
    app.go               → DI setup (Setup/Shutdown via samber/do/v2)
    common/types.go      → Typed string wrappers (WorkDir, DrupalRoot)
    eventbus/
      bus.go             → Pub/sub event bus (async, topic-based)
    modules/
      config/            → Config module (loads configs/config.yaml, stores in container)
      watcher/           → Watcher module (creates FSNotifyWatcher from config)
      executor/          → Executor module (creates DrushExecutor from config)
      orchestrator/      → Orchestrator module (engine with EventBus, starts in goroutine)
      ui/                → UI module (runs Bubble Tea TUI, blocks until quit)
        providers/tui/   → TUI bridge (EventBus → EngineEvent channel)

  config/                → Config management (YAML + env vars), Drupal root detection, PID files
  health/                → Liveness check (timestamp file every 30s)
  drush/                 → Drush resolution, execution, health checks
  metrics/               → Runtime statistics (changes, clears, errors per minute)
  training/              → Context-aware training suggestions (training.json)
  validate/              → Config and environment validation (`validate` command)
  xdebug/                → Xdebug detection and disable (Ctrl+X)
  utils/                 → Color helpers, format utilities

pkg/
  core/
    interfaces.go        → Watcher, CommandExecutor, EventFilter, LintChecker
    models.go            → FileEvent, ExecutionResult, EngineEvent, SiteInfo
  adapters/
    fsnotify_watcher.go  → core.Watcher via fsnotify
    polling_watcher.go   → core.Watcher via periodic file tree scan
    hybrid_watcher.go    → core.Watcher (fsnotify + polling, deduped)
    drush_executor.go    → core.CommandExecutor via drush
    regex_filter.go      → Pattern/Exclude/Dotfile filters
    php_lint.go          → core.LintChecker via php -l
    yaml_lint.go         → core.LintChecker via Go yaml parser
    lint_cache.go        → Caching wrapper for LintChecker (SHA-1, 5min TTL)
    phpcs_lint.go        → core.LintChecker via phpcs (Drupal standards)
    slog_logger.go       → Structured logger factory

```

### Key domain interfaces

[](#key-domain-interfaces)

```
// pkg/core/interfaces.go

type Watcher interface {
    Start(ctx context.Context) (
