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
2 changes: 1 addition & 1 deletion .github/wiki
Submodule wiki updated from 170a0a to 20540e
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add a standalone DevTools `self-update` command plus global `--working-dir` and `--auto-update` binary options for local or global installations (#272)

## [1.23.0] - 2026-04-26

### Added
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ composer dev-tools

# Automatically fix code standards issues where applicable
composer dev-tools:fix

# Run the standalone binary from another project directory
vendor/bin/dev-tools --working-dir=/path/to/project tests

# Update the installed DevTools package
vendor/bin/dev-tools self-update
```

You can also run individual commands for specific development tasks:
Expand Down Expand Up @@ -288,6 +294,7 @@ skills they depend on.
| `composer codeowners` | Generates managed `.github/CODEOWNERS` content from local repository metadata. |
| `composer gitattributes` | Manages export-ignore rules in `.gitattributes`. |
| `composer dev-tools:sync` | Updates scripts, CODEOWNERS, funding metadata, workflow stubs, `.editorconfig`, `.gitignore`, `.gitattributes`, wiki setup, packaged skills, and packaged agents. |
| `vendor/bin/dev-tools self-update` / `composer dev-tools:self-update` | Updates the local DevTools package, or the Composer global installation when the active binary is globally installed. |

## 🔌 Integration

Expand Down
Binary file modified docs/_static/mascot-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/_static/mascot.png
Binary file not shown.
18 changes: 11 additions & 7 deletions docs/api/commands.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
Command Classes
===============

All public CLI commands extend ``Composer\Command\BaseCommand``. Most command
classes are resolved lazily through ``DevToolsCommandLoader`` and receive
their collaborators from the shared ``DevToolsServiceProvider`` container,
while orchestration commands such as ``standards`` dispatch other commands
through the console application itself. The architecture also relies on
``ProcessBuilder`` and ``ProcessQueue`` for fluent process management where
subprocess execution is needed.
All public CLI commands are Symfony Console commands resolved lazily through
``DevToolsCommandLoader``. Command classes receive their collaborators from
the shared ``DevToolsServiceProvider`` container, while orchestration commands
such as ``standards`` dispatch other commands through the console application
itself. Composer integration uses proxy commands to expose that same Symfony
command set without forcing each command to extend Composer's ``BaseCommand``.
The architecture also relies on ``ProcessBuilder`` and ``ProcessQueue`` for
fluent process management where subprocess execution is needed.

.. list-table::
:header-rows: 1
Expand Down Expand Up @@ -95,3 +96,6 @@ subprocess execution is needed.
* - ``FastForward\DevTools\Console\Command\UpdateComposerJsonCommand``
- ``update-composer-json``
- Updates the composer.json file to match the packaged schema.
* - ``FastForward\DevTools\Console\Command\SelfUpdateCommand``
- ``dev-tools:self-update``
- Updates the local or global DevTools installation through Composer.
6 changes: 5 additions & 1 deletion docs/api/composer-integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ Composer Plugin Classes
- Registers the command provider and runs ``dev-tools:sync`` after
Composer install and update.
* - ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider``
- Instantiates and returns the available command classes.
- Exposes Symfony command instances to Composer through proxy commands
while filtering names and aliases already registered by Composer or the
root project's scripts.
* - ``FastForward\DevTools\Composer\Command\ProxyCommand``
- Adapts one Symfony command to Composer's command provider contract.
* - ``FastForward\DevTools\Console\DevTools``
- Console application used by the local binary.

Expand Down
1 change: 1 addition & 0 deletions docs/commands/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Detailed documentation for each dev-tools command.
agents
skills
sync
self-update
funding
codeowners
gitignore
Expand Down
53 changes: 53 additions & 0 deletions docs/commands/self-update.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
self-update
===========

``self-update`` updates the installed ``fast-forward/dev-tools`` package
through Composer.

Usage
-----

.. code-block:: bash

vendor/bin/dev-tools self-update
composer dev-tools:self-update

When the standalone ``dev-tools`` binary is itself loaded from Composer's
global installation, ``self-update`` automatically targets
``composer global update fast-forward/dev-tools``. Local project
installations update the current project by default.

Global runtime options
----------------------

The standalone DevTools binary also accepts Composer-like global runtime
options before the command name:

.. code-block:: bash

vendor/bin/dev-tools --working-dir=/path/to/project tests
vendor/bin/dev-tools --auto-update tests

``--working-dir`` (or ``-d``) switches the process directory before resolving
paths, managed files, or command defaults. This lets a globally installed
binary operate on another project without first changing shell directories.
Composer executions can use Composer's own ``--working-dir``/``-d`` option.

``--auto-update`` runs the self-update flow before the requested command. The
same behavior MAY be enabled with ``FAST_FORWARD_AUTO_UPDATE=1``. When the
active ``dev-tools`` binary is already installed globally, auto-update also
targets the global installation by default. Auto-update failures are reported
as warnings and do not block the requested command.

Version freshness check
-----------------------

When DevTools runs from an installed package, the binary checks Composer
metadata for the latest stable ``fast-forward/dev-tools`` release. If a newer
stable version is available, DevTools prints a warning recommending
``dev-tools self-update``. This check is best-effort: network, Composer, or
metadata failures are ignored so the requested command can continue normally.
The check is skipped automatically in CI environments, including GitHub
Actions, so freshly installed consumer workflows do not spend time querying
release metadata. Set ``FAST_FORWARD_SKIP_VERSION_CHECK=1`` to disable the
warning in other non-interactive contexts.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Documentation

.. container:: col-lg-5 text-center

.. image:: _static/mascot.png
.. image:: _static/mascot-banner.png
:alt: Fast Forward DevTools mascot
:class: img-fluid w-100 rounded-4 shadow-sm border border-light-subtle bg-body-tertiary p-2

Expand Down
7 changes: 5 additions & 2 deletions docs/internals/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,8 @@ command list:
- Git ignore, Git attributes, license, resource diffing, and coverage
summary services support consumer sync and reporting workflows.
* - ``Shared infrastructure``
- ``LoggerInterface``, ``ClockInterface``, and Twig's
``LoaderInterface`` provide reusable runtime infrastructure.
- ``LoggerInterface``, ``ClockInterface``,
``RuntimeEnvironmentInterface``, and Twig's ``LoaderInterface``
provide reusable runtime infrastructure, including centralized checks
for GitHub Actions, generic CI, Composer test runs, and truthy
environment flags.
46 changes: 44 additions & 2 deletions src/Composer/Capability/DevToolsCommandProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,32 @@

use Composer\Plugin\Capability\CommandProvider;
use FastForward\DevTools\Composer\Command\ProxyCommand;
use FastForward\DevTools\Composer\DevToolsPluginInterface;
use FastForward\DevTools\Console\DevTools;
use Symfony\Component\Console\Command\Command;

/**
* Provides a registry of custom dev-tools commands mapped for Composer integration.
* This capability struct MUST implement the defined `CommandProvider`.
*/
final class DevToolsCommandProvider implements CommandProvider
final readonly class DevToolsCommandProvider implements CommandProvider
{
/**
* @var string the namespace prefix for dev-tools console commands to be registered as Composer commands
*/
private const string COMMAND_NAMESPACE = 'FastForward\DevTools\Console\Command';

private ?DevToolsPluginInterface $plugin;

/**
* @param array<string, mixed> $constructorArguments the Composer capability constructor arguments
*/
public function __construct(array $constructorArguments = [])
{
$plugin = $constructorArguments['plugin'] ?? null;
$this->plugin = $plugin instanceof DevToolsPluginInterface ? $plugin : null;
}

/**
* {@inheritDoc}
*/
Expand All @@ -55,9 +68,38 @@ public function getCommands()
continue;
}

$commands[] = new ProxyCommand($command);
if ($this->isRegisteredCommand($command->getName())) {
continue;
}

$commands[] = new ProxyCommand($command, $this->getComposerAliases($command));
}

return $commands;
}

/**
* Returns command aliases that may be safely exposed to Composer.
*
* @param Command $command the Symfony command being proxied
*
* @return list<string>
*/
private function getComposerAliases(Command $command): array
{
return array_values(array_filter(
$command->getAliases(),
fn(string $alias): bool => ! $this->isRegisteredCommand($alias),
));
}

/**
* Detects names already owned by Composer's active command surface.
*
* @param string|null $name the command name or alias being evaluated
*/
private function isRegisteredCommand(?string $name): bool
{
return $this->plugin?->isRegisteredCommand($name) ?? false;
}
}
12 changes: 8 additions & 4 deletions src/Composer/Command/ProxyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,29 @@ final class ProxyCommand extends BaseCommand
{
/**
* @param Command $command the Symfony command adapted for Composer plugin execution
* @param list<string>|null $aliases the optional alias list exposed to Composer
*/
public function __construct(
private readonly Command $command,
?array $aliases = null,
) {
parent::__construct($this->command->getName());

$this
->setAliases($this->command->getAliases())
->setAliases($aliases ?? $this->command->getAliases())
->setDescription($this->command->getDescription())
->setHelp($this->command->getHelp())
->setDefinition(clone $this->command->getDefinition())
->setHidden($this->command->isHidden());
}

/**
* @param InputInterface $input
* @param OutputInterface $output
* Executes the proxied Symfony command through Composer's command contract.
*
* @return int
* @param InputInterface $input the Composer command input
* @param OutputInterface $output the Composer command output
*
* @return int the proxied command status code
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
Expand Down
93 changes: 93 additions & 0 deletions src/Composer/DevToolsPluginInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

/**
* Fast Forward Development Tools for PHP projects.
*
* This file is part of fast-forward/dev-tools project.
*
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*
* @see https://github.com/php-fast-forward/
* @see https://github.com/php-fast-forward/dev-tools
* @see https://github.com/php-fast-forward/dev-tools/issues
* @see https://php-fast-forward.github.io/dev-tools/
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/

namespace FastForward\DevTools\Composer;

use Composer\Plugin\PluginInterface;

/**
* Defines DevTools-specific Composer plugin conventions.
*/
interface DevToolsPluginInterface extends PluginInterface
{
/**
* @var list<string> composer command names and aliases that DevTools MUST NOT override
*/
public const array COMPOSER_COMMAND_NAMES = [
'_complete',
'about',
'archive',
'audit',
'browse',
'bump',
'cc',
'check-platform-reqs',
'clear-cache',
'clearcache',
'completion',
'config',
'create-project',
'depends',
'diagnose',
'dump-autoload',
'dumpautoload',
'exec',
'fund',
'global',
'help',
'home',
'i',
'info',
'init',
'install',
'licenses',
'list',
'outdated',
'prohibits',
'r',
'reinstall',
'remove',
'repo',
'repository',
'require',
'rm',
'run',
'run-script',
'search',
'self-update',
'selfupdate',
'show',
'status',
'suggests',
'u',
'uninstall',
'update',
'upgrade',
'validate',
'why',
'why-not',
];

/**
* Detects whether a command name or alias is already registered in Composer's command surface.
*
* @param string|null $name the command name or alias being evaluated
*/
public function isRegisteredCommand(?string $name): bool;
}
Loading
Loading