Skip to content

Plugin Loader

The plugin system allows extending dde with custom shell scripts that are registered as Symfony Console commands.

Plugins are loaded from two directories:

  • Global plugins: ~/.dde/plugins/*.sh
  • Project plugins: .dde/plugins/*.sh

Project plugins override global plugins that have the same @command name.

A plugin is a shell script with annotation comments:

#!/usr/bin/env bash
# @command deploy
# @description Deploy the project to staging
set -e
echo "Deploying..."
# Your deployment logic here

Required annotations:

  • @command — the command name (registered as project:exec:{name})

Optional annotations:

  • @description — description shown in dde list and dde help

Scripts without a @command annotation are ignored.

The PluginLoader class handles discovery and parsing:

public function loadPlugins(?string $projectDir = null): array
  1. Scans ~/.dde/plugins/ for *.sh files (via Symfony Finder, sorted by name)
  2. Parses each file for @command and @description annotations using regex
  3. Creates a PluginDefinition for each valid plugin
  4. If a project directory is given, scans .dde/plugins/ similarly
  5. Merges project plugins into global plugins (project overrides global by command name)
final readonly class PluginDefinition
{
public function __construct(
public string $command,
public string $description,
public string $scriptPath,
) {}
}

Each plugin is wrapped in a PluginProxyCommand, which is a standard Symfony Console command:

  • Registered as project:exec:{command} (e.g. project:exec:deploy)
  • Accepts optional arguments via an args variadic argument
  • Executes the shell script via symfony/process
  • Supports TTY mode for interactive scripts
  • Returns the script’s exit code

The PluginCommandLoader implements Symfony’s CommandLoaderInterface for lazy loading. It:

  1. Calls PluginLoader::loadPlugins() to discover all plugins
  2. Registers each as a PluginProxyCommand
  3. Returns commands on demand when Symfony needs them

When a global and project plugin share the same @command name:

~/.dde/plugins/deploy.sh # @command deploy
.dde/plugins/deploy.sh # @command deploy (wins)

The project plugin takes precedence. This uses a simple array_merge():

private function mergePlugins(array $global, array $project): array
{
return array_merge($global, $project);
}

Since both arrays are keyed by command name, project entries overwrite global entries.

Create a global plugin:

~/.dde/plugins/hello.sh
# @command hello
# @description Say hello from a plugin
echo "Hello from dde plugin!"
echo "Arguments: $@"

Use it:

Terminal window
dde project:exec:hello world
# Output:
# Hello from dde plugin!
# Arguments: world