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 caa2be to 2182cc
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Preserve literal angle brackets around maintainer emails when generating LICENSE files from composer metadata (#179)
- Keep packaged `.agents` payloads exportable and synchronize packaged skills and agents with repository-relative symlink targets so consumer repositories no longer receive broken absolute machine paths (#188)
- Rewrite drifted Git hooks by removing the previous target first, restore the intended `0o755` executable mode, and report unwritable hook replacements cleanly when `.git/hooks` stays locked (#190)
- Keep Composer plugin command discovery compatible with consumer environments by moving unsupported Symfony Console named parameters out of command metadata/configuration and by decoupling the custom filesystem wrapper from Composer's bundled Symfony Filesystem signatures (#185)
- Keep Composer autoload, Rector, and ECS from traversing nested fixture `vendor` directories when the composer-plugin consumer fixture has installed dependencies (#179)

## [1.20.0] - 2026-04-23

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"FastForward\\DevTools\\Tests\\": "tests/"
},
"classmap": [
"tests/Fixtures/"
"tests/Fixtures/Console/"
]
},
"bin": "bin/dev-tools",
Expand Down
9 changes: 6 additions & 3 deletions src/Changelog/DependabotChangelogEntryMessageResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

namespace FastForward\DevTools\Changelog;

use function Safe\preg_replace;
use function Safe\preg_match;

/**
* Normalizes minimal changelog entry messages for Dependabot pull requests.
*/
Expand All @@ -32,10 +35,10 @@
*/
public function resolve(string $title, int $pullRequestNumber): string
{
$message = \preg_replace('/\s+/', ' ', \trim($title)) ?? \trim($title);
$message = \rtrim($message, " \t\n\r\0\x0B.");
$message = preg_replace('/\s+/', ' ', trim($title)) ?? trim($title);
$message = rtrim($message, " \t\n\r\0\x0B.");

if (\preg_match('/\(#\d+\)$/', $message) === 1) {
if (1 === preg_match('/\(#\d+\)$/', $message)) {
return $message;
}

Expand Down
5 changes: 3 additions & 2 deletions src/Config/ECSConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ public static function applyDefaultPathsAndSkips(
ECSConfigBuilder $config,
string $workingDirectory
): ECSConfigBuilder {
$skipPaths = WorkingProjectPathResolver::getToolingExcludedDirectories($workingDirectory);
$paths = WorkingProjectPathResolver::getToolingSourcePaths($workingDirectory);
$skipPaths = WorkingProjectPathResolver::getToolingExcludedDirectories();

return $config
->withPaths([$workingDirectory])
->withPaths($paths)
->withSkip([...$skipPaths, ...self::DEFAULT_SKIPPED_RULES]);
}

Expand Down
5 changes: 3 additions & 2 deletions src/Config/RectorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ public static function configure(?callable $customize = null): callable
{
return static function (RectorConfigInterface $rectorConfig) use ($customize): void {
$workingDirectory = getcwd();
$skipPaths = WorkingProjectPathResolver::getToolingExcludedDirectories($workingDirectory);
$paths = WorkingProjectPathResolver::getToolingSourcePaths();
$skipPaths = WorkingProjectPathResolver::getToolingExcludedDirectories();
$skipRules = self::DEFAULT_SKIPPED_RULES;

$rectorConfig->sets(self::DEFAULT_SETS);
$rectorConfig->paths([$workingDirectory]);
$rectorConfig->paths($paths);
$rectorConfig->skip([...$skipPaths, ...$skipRules]);
$rectorConfig->cacheDirectory(
ManagedWorkspace::getCacheDirectory(ManagedWorkspace::RECTOR, $workingDirectory)
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/AgentsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@
/**
* Synchronizes packaged Fast Forward project agents into the consumer repository.
*/
#[AsCommand(
name: 'agents',
description: 'Synchronizes Fast Forward project agents into .agents/agents directory.'
)]
#[AsCommand(name: 'agents', description: 'Synchronizes Fast Forward project agents into .agents/agents directory.')]
final class AgentsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/ChangelogShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@
/**
* Prints the rendered notes body for a released changelog version.
*/
#[AsCommand(
name: 'changelog:show',
description: 'Prints the notes body for a released changelog version.'
)]
#[AsCommand(name: 'changelog:show', description: 'Prints the notes body for a released changelog version.')]
final class ChangelogShowCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/CodeOwnersCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@
/**
* Generates and synchronizes CODEOWNERS files from local project metadata.
*/
#[AsCommand(
name: 'codeowners',
description: 'Generates .github/CODEOWNERS from local project metadata.'
)]
#[AsCommand(name: 'codeowners', description: 'Generates .github/CODEOWNERS from local project metadata.')]
final class CodeOwnersCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/CopyResourceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@
/**
* Copies packaged or local resources into the consumer repository.
*/
#[AsCommand(
name: 'copy-resource',
description: 'Copies a file or directory resource into the current project.'
)]
#[AsCommand(name: 'copy-resource', description: 'Copies a file or directory resource into the current project.')]
final class CopyResourceCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/DocsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@
* queue so logging and grouped output stay consistent with the rest of the
* command surface.
*/
#[AsCommand(
name: 'docs',
description: 'Generates API documentation.'
)]
#[AsCommand(name: 'docs', description: 'Generates API documentation.')]
final class DocsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasCacheOption;
Expand Down
19 changes: 10 additions & 9 deletions src/Console/Command/GitHooksCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@
/**
* Installs packaged Git hooks for the consumer repository.
*/
#[AsCommand(
name: 'git-hooks',
description: 'Installs Fast Forward Git hooks.'
)]
#[AsCommand(name: 'git-hooks', description: 'Installs Fast Forward Git hooks.')]
final class GitHooksCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down Expand Up @@ -276,8 +273,12 @@ private function shouldReplaceHook(string $hookPath): bool
*
* @return bool true when the hook was installed successfully
*/
private function installHook(string $sourcePath, string $hookPath, bool $replaceExisting, InputInterface $input): bool
{
private function installHook(
string $sourcePath,
string $hookPath,
bool $replaceExisting,
InputInterface $input
): bool {
try {
if ($replaceExisting && $this->filesystem->exists($hookPath)) {
$this->filesystem->remove($hookPath);
Expand All @@ -287,15 +288,15 @@ private function installHook(string $sourcePath, string $hookPath, bool $replace
$this->filesystem->chmod(files: $hookPath, mode: 0o755);

return true;
} catch (IOExceptionInterface $exception) {
} catch (IOExceptionInterface $ioException) {
$this->logger->error(
'Failed to install {hook_name} hook automatically. Remove or unlock {hook_path} and rerun git-hooks.',
[
'input' => $input,
'hook_name' => $this->filesystem->basename($hookPath),
'hook_path' => $hookPath,
'error' => $exception->getMessage(),
'file' => $exception->getPath() ?? $hookPath,
'error' => $ioException->getMessage(),
'file' => $ioException->getPath() ?? $hookPath,
'line' => null,
],
);
Expand Down
9 changes: 4 additions & 5 deletions src/Console/Command/GitIgnoreCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@
* The command accepts two options: --source and --target to specify the paths
* to the canonical and project .gitignore files respectively.
*/
#[AsCommand(
name: 'gitignore',
description: 'Merges and synchronizes .gitignore files.'
)]
#[AsCommand(name: 'gitignore', description: 'Merges and synchronizes .gitignore files.')]
final class GitIgnoreCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down Expand Up @@ -87,7 +84,9 @@ public function __construct(
*/
protected function configure(): void
{
$this->setHelp("This command merges the canonical .gitignore from dev-tools with the project's existing .gitignore.");
$this->setHelp(
"This command merges the canonical .gitignore from dev-tools with the project's existing .gitignore."
);

$this->addJsonOption()
->addOption(
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/LicenseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@
* This command generates a LICENSE file if one does not exist and a supported
* license is declared in composer.json.
*/
#[AsCommand(
name: 'license',
description: 'Generates a LICENSE file from composer.json license information.'
)]
#[AsCommand(name: 'license', description: 'Generates a LICENSE file from composer.json license information.')]
final class LicenseCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/MetricsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@

use function rtrim;

#[AsCommand(
name: 'metrics',
description: 'Analyzes code metrics with PhpMetrics.'
)]
#[AsCommand(name: 'metrics', description: 'Analyzes code metrics with PhpMetrics.')]
final class MetricsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/PhpDocCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@
* Provides operations to inspect, lint, and repair PHPDoc comments across the project.
* The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector.
*/
#[AsCommand(
name: 'phpdoc',
description: 'Checks and fixes PHPDocs.'
)]
#[AsCommand(name: 'phpdoc', description: 'Checks and fixes PHPDocs.')]
final class PhpDocCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasCacheOption;
Expand Down
6 changes: 1 addition & 5 deletions src/Console/Command/RefactorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@
* Provides functionality to execute automated code refactoring using Rector.
* This class MUST NOT be extended and SHALL encapsulate the logic for Rector invocation.
*/
#[AsCommand(
name: 'refactor',
description: 'Runs Rector for code refactoring.',
aliases: ['rector']
)]
#[AsCommand(name: 'refactor', description: 'Runs Rector for code refactoring.', aliases: ['rector'])]
final class RefactorCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/ReportsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@
* Coordinates the generation of Fast Forward documentation frontpage and related reports.
* This class MUST NOT be overridden and SHALL securely combine docs and testing commands.
*/
#[AsCommand(
name: 'reports',
description: 'Generates the frontpage for Fast Forward documentation.'
)]
#[AsCommand(name: 'reports', description: 'Generates the frontpage for Fast Forward documentation.')]
final class ReportsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasCacheOption;
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/SkillsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@
* target paths, triggers synchronization, and translates the resulting status
* into Symfony Console output and process exit codes.
*/
#[AsCommand(
name: 'skills',
description: 'Synchronizes Fast Forward skills into .agents/skills directory.'
)]
#[AsCommand(name: 'skills', description: 'Synchronizes Fast Forward skills into .agents/skills directory.')]
final class SkillsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasJsonOption;
Expand Down
9 changes: 2 additions & 7 deletions src/Console/Command/StandardsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@
* Executes the full suite of Fast Forward code standard checks.
* This class MUST NOT be modified through inheritance and SHALL streamline code validation workflows.
*/
#[AsCommand(
name: 'standards',
description: 'Runs Fast Forward code standards checks.'
)]
#[AsCommand(name: 'standards', description: 'Runs Fast Forward code standards checks.')]
final class StandardsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasCacheOption;
Expand Down Expand Up @@ -73,9 +70,7 @@ protected function configure(): void
);
$this
->addJsonOption()
->addCacheOption(
'Whether to enable cache writes in nested cache-aware standards commands.'
)
->addCacheOption('Whether to enable cache writes in nested cache-aware standards commands.')
->addCacheDirOption(
description: 'Base cache directory used for nested cache-aware standards commands.',
default: ManagedWorkspace::getCacheDirectory('standards'),
Expand Down
10 changes: 2 additions & 8 deletions src/Console/Command/TestsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@
* Facilitates the execution of the PHPUnit testing framework.
* This class MUST NOT be overridden and SHALL configure testing parameters dynamically.
*/
#[AsCommand(
name: 'tests',
description: 'Runs PHPUnit tests.'
)]
#[AsCommand(name: 'tests', description: 'Runs PHPUnit tests.')]
final class TestsCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasCacheOption;
Expand Down Expand Up @@ -187,10 +184,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($cacheEnabled) {
$processBuilder = $processBuilder->withArgument(
'--cache-result',
)->withArgument(
'--cache-directory',
$this->resolvePath($input, 'cache-dir')
);
)->withArgument('--cache-directory', $this->resolvePath($input, 'cache-dir'));
} else {
$processBuilder = $processBuilder->withArgument('--do-not-cache-result');
}
Expand Down
5 changes: 1 addition & 4 deletions src/Console/Command/WikiCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@
* Handles the generation of API documentation for the project.
* This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task.
*/
#[AsCommand(
name: 'wiki',
description: 'Generates API documentation in Markdown format.'
)]
#[AsCommand(name: 'wiki', description: 'Generates API documentation in Markdown format.')]
final class WikiCommand extends BaseCommand implements LoggerAwareCommandInterface
{
use HasCacheOption;
Expand Down
3 changes: 2 additions & 1 deletion src/Console/Input/HasCacheOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace FastForward\DevTools\Console\Input;

use Throwable;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand Down Expand Up @@ -140,7 +141,7 @@ private function hasExplicitCacheDirArgument(InputInterface $input): bool
{
try {
return $input->hasParameterOption('--cache-dir', true);
} catch (\Throwable) {
} catch (Throwable) {
return false;
}
}
Expand Down
13 changes: 10 additions & 3 deletions src/Filesystem/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@
* converting provided paths to absolute representations when a base path is supplied or
* dynamically inferred from the generic working directory.
*/
final class Filesystem implements FilesystemInterface
final readonly class Filesystem implements FilesystemInterface
{
/**
* @param SymfonyFilesystem $filesystem
*/
public function __construct(
private readonly SymfonyFilesystem $filesystem = new SymfonyFilesystem(),
private SymfonyFilesystem $filesystem = new SymfonyFilesystem(),
) {}

/**
Expand Down Expand Up @@ -84,7 +87,11 @@ public function dumpFile(string $filename, mixed $content, ?string $path = null)
*/
public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false): void
{
$this->filesystem->copy($this->getAbsolutePath($originFile), $this->getAbsolutePath($targetFile), $overwriteNewerFiles);
$this->filesystem->copy(
$this->getAbsolutePath($originFile),
$this->getAbsolutePath($targetFile),
$overwriteNewerFiles
);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/GitAttributes/Merger.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private function keepInExportLookup(array $keepInExportPaths): array
*/
private function isKeptPath(string $pathKey, array $keptExportLookup): bool
{
foreach ($keptExportLookup as $keptPath => $_) {
foreach (array_keys($keptExportLookup) as $keptPath) {
if ($pathKey === $keptPath || str_starts_with($pathKey . '/', $keptPath . '/')) {
return true;
}
Expand Down
Loading
Loading