Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.52% covered (warning)
88.52%
54 / 61
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
DevTools
88.52% covered (warning)
88.52%
54 / 61
77.78% covered (warning)
77.78%
7 / 9
24.87
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultInputDefinition
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 doRun
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 getWorkingDirectoryOption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 configureWorkspaceDirectory
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 runAutoUpdateWhenRequested
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 shouldRenderLogo
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 isSelfUpdateCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTruthyAutoUpdateMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console;
21
22use FastForward\DevTools\Console\Command\SelfUpdateCommand;
23use FastForward\DevTools\Environment\EnvironmentInterface;
24use FastForward\DevTools\Environment\RuntimeEnvironmentInterface;
25use FastForward\DevTools\Path\ManagedWorkspace;
26use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
27use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface;
28use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface;
29use FastForward\DevTools\SelfUpdate\VersionCheckerInterface;
30use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface;
31use Override;
32use Symfony\Component\Console\Application;
33use Symfony\Component\Console\Command\Command;
34use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
35use Symfony\Component\Console\Input\InputDefinition;
36use Symfony\Component\Console\Input\InputInterface;
37use Symfony\Component\Console\Input\InputOption;
38use Symfony\Component\Console\Output\OutputInterface;
39use Throwable;
40
41use function Safe\putenv;
42
43/**
44 * Wraps the fast-forward console tooling suite conceptually as an isolated application instance.
45 * Extending the base application, it MUST provide default command injections safely.
46 */
47 class DevTools extends Application
48{
49    public const string ENV_AUTO_UPDATE = 'FAST_FORWARD_AUTO_UPDATE';
50
51    private const string LOGO = <<<'LOGO'
52         ____             _____           _
53        |  _ \  _____   _|_   _|__   ___ | |___
54        | | | |/ _ \ \ / / | |/ _ \ / _ \| / __|
55        | |_| |  __/\ V /  | | (_) | (_) | \__ \
56        |____/ \___| \_/   |_|\___/ \___/|_|___/
57        ========================================
58
59        LOGO;
60
61    /**
62     * Initializes the DevTools global context and dependency graph.
63     *
64     * The method MUST define default configurations and MAY accept an explicit command provider.
65     * It SHALL instruct the runner to treat the `standards` command generically as its default endpoint.
66     *
67     * @param CommandLoaderInterface $commandLoader the command loader responsible for providing command instances
68     * @param WorkingDirectorySwitcherInterface $workingDirectorySwitcher switches the process working directory
69     * @param VersionCheckNotifierInterface $versionCheckNotifier emits non-blocking version freshness warnings
70     * @param SelfUpdateRunnerInterface $selfUpdateRunner runs explicit or automatic self-update flows
71     * @param SelfUpdateScopeResolverInterface $selfUpdateScopeResolver resolves whether the active binary is global
72     * @param VersionCheckerInterface $versionChecker resolves the installed DevTools version for metadata output
73     * @param EnvironmentInterface $environment reads environment flags for optional auto-update behavior
74     * @param RuntimeEnvironmentInterface $runtimeEnvironment resolves runtime environment capabilities
75     */
76    public function __construct(
77        CommandLoaderInterface $commandLoader,
78        private  WorkingDirectorySwitcherInterface $workingDirectorySwitcher,
79        private  VersionCheckNotifierInterface $versionCheckNotifier,
80        private  SelfUpdateRunnerInterface $selfUpdateRunner,
81        private  SelfUpdateScopeResolverInterface $selfUpdateScopeResolver,
82        private  VersionCheckerInterface $versionChecker,
83        private  EnvironmentInterface $environment,
84        private  RuntimeEnvironmentInterface $runtimeEnvironment,
85    ) {
86        parent::__construct('Fast Forward Dev Tools', $this->versionChecker->getCurrentVersion());
87
88        $this->setDefaultCommand('dev-tools:standards');
89        $this->setCommandLoader($commandLoader);
90    }
91
92    /**
93     * Returns the application-level input definition with DevTools runtime options.
94     *
95     * @return InputDefinition the global application input definition
96     */
97    #[Override]
98    protected function getDefaultInputDefinition(): InputDefinition
99    {
100        $definition = parent::getDefaultInputDefinition();
101
102        $definition->addOption(new InputOption(
103            name: 'working-dir',
104            shortcut: 'd',
105            mode: InputOption::VALUE_REQUIRED,
106            description: 'Run DevTools as if it was started in the given directory.',
107        ));
108
109        $definition->addOption(new InputOption(
110            name: 'auto-update',
111            mode: InputOption::VALUE_NONE,
112            description: 'Update fast-forward/dev-tools before running the requested command.',
113        ));
114
115        $definition->addOption(new InputOption(
116            name: 'workspace-dir',
117            shortcut: 'w',
118            mode: InputOption::VALUE_REQUIRED,
119            description: 'Store generated DevTools artifacts in the given directory.',
120        ));
121
122        $definition->addOption(new InputOption(
123            name: 'no-logo',
124            mode: InputOption::VALUE_NONE,
125            description: 'Hide the startup ASCII logo.',
126        ));
127
128        return $definition;
129    }
130
131    /**
132     * Runs the application after applying global runtime options.
133     *
134     * @param InputInterface $input the application input
135     * @param OutputInterface $output the application output
136     *
137     * @return int the application status code
138     */
139    #[Override]
140    public function doRun(InputInterface $input, OutputInterface $output): int
141    {
142        if ($this->shouldRenderLogo($input)) {
143            $output->writeln(self::LOGO);
144        }
145
146        try {
147            $this->workingDirectorySwitcher->switchTo($this->getWorkingDirectoryOption($input));
148            $this->configureWorkspaceDirectory($input);
149        } catch (Throwable $throwable) {
150            $output->writeln(\sprintf('<error>%s</error>', $throwable->getMessage()));
151
152            return Command::FAILURE;
153        }
154
155        if ($this->shouldRenderLogo($input) && ! $this->isSelfUpdateCommand($input)) {
156            $this->runAutoUpdateWhenRequested($input, $output);
157            $this->versionCheckNotifier->notify($output);
158        }
159
160        return parent::doRun($input, $output);
161    }
162
163    /**
164     * Resolves the raw working-directory option before command parsing.
165     *
166     * @param InputInterface $input the application input
167     */
168    private function getWorkingDirectoryOption(InputInterface $input): ?string
169    {
170        $workingDirectory = $input->getParameterOption(['--working-dir', '-d'], null, true);
171
172        return \is_string($workingDirectory) ? $workingDirectory : null;
173    }
174
175    /**
176     * Applies the configured workspace directory before resolving command defaults.
177     *
178     * @param InputInterface $input the application input
179     */
180    private function configureWorkspaceDirectory(InputInterface $input): void
181    {
182        $workspaceDirectory = $input->getParameterOption('--workspace-dir', null, true);
183
184        if (! \is_string($workspaceDirectory) || '' === $workspaceDirectory) {
185            return;
186        }
187
188        putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=' . $workspaceDirectory);
189    }
190
191    /**
192     * Runs an explicit automatic update without letting failures block the requested command.
193     *
194     * @param InputInterface $input the application input
195     * @param OutputInterface $output the application output
196     */
197    private function runAutoUpdateWhenRequested(InputInterface $input, OutputInterface $output): void
198    {
199        $autoUpdateMode = $this->environment->get(self::ENV_AUTO_UPDATE, '');
200
201        if (! $input->hasParameterOption('--auto-update', true) && ! $this->isTruthyAutoUpdateMode($autoUpdateMode)) {
202            return;
203        }
204
205        try {
206            $global = $this->selfUpdateScopeResolver->isGlobalInstallation();
207            $statusCode = $this->selfUpdateRunner->update($global, $output);
208        } catch (Throwable) {
209            $output->writeln('<comment>DevTools auto-update failed; continuing with the requested command.</comment>');
210
211            return;
212        }
213
214        if (Command::SUCCESS !== $statusCode) {
215            $output->writeln('<comment>DevTools auto-update failed; continuing with the requested command.</comment>');
216        }
217    }
218
219    /**
220     * Determines whether the startup logo should be rendered for this invocation.
221     *
222     * @param InputInterface $input the application input
223     */
224    private function shouldRenderLogo(InputInterface $input): bool
225    {
226        if ($this->runtimeEnvironment->isAgentPresent()) {
227            return false;
228        }
229
230        if ((bool) $input->getParameterOption('--no-logo', null, true)) {
231            return false;
232        }
233
234        return ! $input->hasParameterOption('--json', true) && ! $input->hasParameterOption('--pretty-json', true);
235    }
236
237    /**
238     * Detects whether the current invocation targets the self-update command.
239     *
240     * @param InputInterface $input the application input
241     */
242    private function isSelfUpdateCommand(InputInterface $input): bool
243    {
244        return \in_array($input->getFirstArgument(), SelfUpdateCommand::getCommandNames(), true);
245    }
246
247    /**
248     * Interprets environment values that enable auto-update.
249     *
250     * @param string|null $mode the FAST_FORWARD_AUTO_UPDATE value
251     */
252    private function isTruthyAutoUpdateMode(?string $mode): bool
253    {
254        return null !== $mode && \in_array(strtolower($mode), ['1', 'true', 'yes', 'on'], true);
255    }
256}