xref: /plugin/dev/cli.php (revision fe060d0d4c4c42403a614910f1d8cdcd8d360b96)
136c0b2b4SAndreas Gohr#!/usr/bin/env php
236c0b2b4SAndreas Gohr<?php
336c0b2b4SAndreas Gohr
436c0b2b4SAndreas Gohruse dokuwiki\Extension\CLIPlugin;
536c0b2b4SAndreas Gohruse dokuwiki\Extension\PluginController;
65586e97bSAndreas Gohruse dokuwiki\plugin\dev\LangProcessor;
770316b84SAndreas Gohruse dokuwiki\plugin\dev\Skeletor;
81a23d1dbSAndreas Gohruse dokuwiki\plugin\dev\SVGIcon;
936c0b2b4SAndreas Gohruse splitbrain\phpcli\Exception as CliException;
1036c0b2b4SAndreas Gohruse splitbrain\phpcli\Options;
1136c0b2b4SAndreas Gohr
1236c0b2b4SAndreas Gohr/**
1336c0b2b4SAndreas Gohr * @license GPL2
1436c0b2b4SAndreas Gohr * @author  Andreas Gohr <andi@splitbrain.org>
1536c0b2b4SAndreas Gohr */
1636c0b2b4SAndreas Gohrclass cli_plugin_dev extends CLIPlugin
1736c0b2b4SAndreas Gohr{
1836c0b2b4SAndreas Gohr    /**
1936c0b2b4SAndreas Gohr     * Register options and arguments on the given $options object
2036c0b2b4SAndreas Gohr     *
2136c0b2b4SAndreas Gohr     * @param Options $options
2236c0b2b4SAndreas Gohr     * @return void
2336c0b2b4SAndreas Gohr     */
2436c0b2b4SAndreas Gohr    protected function setup(Options $options)
2536c0b2b4SAndreas Gohr    {
26f2576912SAndreas Gohr        $options->useCompactHelp();
2736c0b2b4SAndreas Gohr        $options->setHelp(
28f2576912SAndreas Gohr            "CLI to help with DokuWiki plugin and template development.\n\n" .
2936c0b2b4SAndreas Gohr            "Run this script from within the extension's directory."
3036c0b2b4SAndreas Gohr        );
3136c0b2b4SAndreas Gohr
32fcb8165bSAndreas Gohr        $options->registerCommand('init', 'Initialize a new plugin or template in the current directory.');
33f2576912SAndreas Gohr        $options->registerCommand('addTest', 'Add the testing framework files and a test. (_test/)');
34*fe060d0dSAndreas Gohr        $options->registerArgument(
35*fe060d0dSAndreas Gohr            'test',
36*fe060d0dSAndreas Gohr            'Optional name of the new test. Defaults to the general test.',
37*fe060d0dSAndreas Gohr            false,
38*fe060d0dSAndreas Gohr            'addTest'
39*fe060d0dSAndreas Gohr        );
40f2576912SAndreas Gohr        $options->registerCommand('addConf', 'Add the configuration files. (conf/)');
41f2576912SAndreas Gohr        $options->registerCommand('addLang', 'Add the language files. (lang/)');
42719b4841SAndreas Gohr        $options->registerCommand('addAgents', 'Add an initial AGENTS.md file for guiding LLM coding agents');
43*fe060d0dSAndreas Gohr        $options->registerOption(
44*fe060d0dSAndreas Gohr            'claude',
45*fe060d0dSAndreas Gohr            'Symlink the AGENTS.md to CLAUDE.md for use with claude code',
46*fe060d0dSAndreas Gohr            'c',
47*fe060d0dSAndreas Gohr            false,
48*fe060d0dSAndreas Gohr            'addAgents'
49*fe060d0dSAndreas Gohr        );
5036c0b2b4SAndreas Gohr
51f2576912SAndreas Gohr        $types = PluginController::PLUGIN_TYPES;
52f2576912SAndreas Gohr        array_walk(
53f2576912SAndreas Gohr            $types,
54f2576912SAndreas Gohr            function (&$item) {
55f2576912SAndreas Gohr                $item = $this->colors->wrap($item, $this->colors::C_BROWN);
56f2576912SAndreas Gohr            }
5736c0b2b4SAndreas Gohr        );
5836c0b2b4SAndreas Gohr
59f2576912SAndreas Gohr        $options->registerCommand('addComponent', 'Add a new plugin component.');
60*fe060d0dSAndreas Gohr        $options->registerArgument(
61*fe060d0dSAndreas Gohr            'type',
62*fe060d0dSAndreas Gohr            'Type of the component. Needs to be one of ' . implode(', ', $types),
63*fe060d0dSAndreas Gohr            true,
64*fe060d0dSAndreas Gohr            'addComponent'
65*fe060d0dSAndreas Gohr        );
66*fe060d0dSAndreas Gohr        $options->registerArgument(
67*fe060d0dSAndreas Gohr            'name',
68*fe060d0dSAndreas Gohr            'Optional name of the component. Defaults to a base component.',
69*fe060d0dSAndreas Gohr            false,
70*fe060d0dSAndreas Gohr            'addComponent'
71*fe060d0dSAndreas Gohr        );
72f2576912SAndreas Gohr
73f2576912SAndreas Gohr        $options->registerCommand('deletedFiles', 'Create the list of deleted files based on the git history.');
74f2576912SAndreas Gohr        $options->registerCommand('rmObsolete', 'Delete obsolete files.');
751a23d1dbSAndreas Gohr
761a23d1dbSAndreas Gohr        $prefixes = array_keys(SVGIcon::SOURCES);
771a23d1dbSAndreas Gohr        array_walk(
781a23d1dbSAndreas Gohr            $prefixes,
791a23d1dbSAndreas Gohr            function (&$item) {
801a23d1dbSAndreas Gohr                $item = $this->colors->wrap($item, $this->colors::C_BROWN);
811a23d1dbSAndreas Gohr            }
821a23d1dbSAndreas Gohr        );
831a23d1dbSAndreas Gohr
841a23d1dbSAndreas Gohr        $options->registerCommand('downloadSvg', 'Download an SVG file from a known icon repository.');
85*fe060d0dSAndreas Gohr        $options->registerArgument(
86*fe060d0dSAndreas Gohr            'prefix:name',
87*fe060d0dSAndreas Gohr            'Colon-prefixed name of the icon. Available prefixes: ' . implode(', ', $prefixes),
88*fe060d0dSAndreas Gohr            true,
89*fe060d0dSAndreas Gohr            'downloadSvg'
90*fe060d0dSAndreas Gohr        );
91*fe060d0dSAndreas Gohr        $options->registerArgument(
92*fe060d0dSAndreas Gohr            'output',
93*fe060d0dSAndreas Gohr            'File to save, defaults to <name>.svg in current dir',
94*fe060d0dSAndreas Gohr            false,
95*fe060d0dSAndreas Gohr            'downloadSvg'
96*fe060d0dSAndreas Gohr        );
97*fe060d0dSAndreas Gohr        $options->registerOption(
98*fe060d0dSAndreas Gohr            'keep-ns',
99*fe060d0dSAndreas Gohr            'Keep the SVG namespace. Use when the file is not inlined into HTML.',
100*fe060d0dSAndreas Gohr            'k',
101*fe060d0dSAndreas Gohr            false,
102*fe060d0dSAndreas Gohr            'downloadSvg'
103*fe060d0dSAndreas Gohr        );
1041a23d1dbSAndreas Gohr
1058f82d673SAndreas Gohr        $options->registerCommand('cleanSvg', 'Clean a existing SVG files to reduce their file size.');
1068f82d673SAndreas Gohr        $options->registerArgument('files...', 'The files to clean (will be overwritten)', true, 'cleanSvg');
107*fe060d0dSAndreas Gohr        $options->registerOption(
108*fe060d0dSAndreas Gohr            'keep-ns',
109*fe060d0dSAndreas Gohr            'Keep the SVG namespace. Use when the file is not inlined into HTML.',
110*fe060d0dSAndreas Gohr            'k',
111*fe060d0dSAndreas Gohr            false,
112*fe060d0dSAndreas Gohr            'cleanSvg'
113*fe060d0dSAndreas Gohr        );
1145586e97bSAndreas Gohr
115*fe060d0dSAndreas Gohr        $options->registerCommand(
116*fe060d0dSAndreas Gohr            'cleanLang',
1175586e97bSAndreas Gohr            'Clean language files from unused language strings. Detecting which strings are truly in use may ' .
118*fe060d0dSAndreas Gohr            'not always correctly work. Use with caution.'
119*fe060d0dSAndreas Gohr        );
120ec0a687bSAndreas Gohr
12157732a2dSAndreas Gohr        $options->registerCommand(
12257732a2dSAndreas Gohr            'test',
12357732a2dSAndreas Gohr            'Run the unit tests for this extension. (calls phpunit using the proper config and group)'
12457732a2dSAndreas Gohr        );
12557732a2dSAndreas Gohr        $options->registerOption(
12657732a2dSAndreas Gohr            'filter',
12757732a2dSAndreas Gohr            'Filter tests to run by a given string. (passed to phpunit)',
12857732a2dSAndreas Gohr            null,
12957732a2dSAndreas Gohr            true,
13057732a2dSAndreas Gohr            'test'
13157732a2dSAndreas Gohr        );
13257732a2dSAndreas Gohr        $options->registerArgument('files...', 'The test files to run. Defaults to all.', false, 'test');
133ec0a687bSAndreas Gohr
134ec0a687bSAndreas Gohr        $options->registerCommand('check', 'Check for code style violations.');
135ec0a687bSAndreas Gohr        $options->registerArgument('files...', 'The files to check. Defaults to the whole extension.', false, 'check');
136ec0a687bSAndreas Gohr
137ec0a687bSAndreas Gohr        $options->registerCommand('fix', 'Fix code style violations and refactor outdated code.');
138ec0a687bSAndreas Gohr        $options->registerArgument('files...', 'The files to check. Defaults to the whole extension.', false, 'fix');
13936c0b2b4SAndreas Gohr    }
14036c0b2b4SAndreas Gohr
14136c0b2b4SAndreas Gohr    /** @inheritDoc */
14236c0b2b4SAndreas Gohr    protected function main(Options $options)
14336c0b2b4SAndreas Gohr    {
1441a23d1dbSAndreas Gohr        $args = $options->getArgs();
1451a23d1dbSAndreas Gohr
14636c0b2b4SAndreas Gohr        switch ($options->getCmd()) {
14736c0b2b4SAndreas Gohr            case 'init':
14836c0b2b4SAndreas Gohr                return $this->cmdInit();
14936c0b2b4SAndreas Gohr            case 'addTest':
15036c0b2b4SAndreas Gohr                $test = array_shift($args);
15136c0b2b4SAndreas Gohr                return $this->cmdAddTest($test);
15236c0b2b4SAndreas Gohr            case 'addConf':
15336c0b2b4SAndreas Gohr                return $this->cmdAddConf();
15436c0b2b4SAndreas Gohr            case 'addLang':
15536c0b2b4SAndreas Gohr                return $this->cmdAddLang();
156719b4841SAndreas Gohr            case 'addAgents':
157488499ccSAndreas Gohr                $claude = $options->getOpt('claude');
158488499ccSAndreas Gohr                return $this->cmdAddAgents($claude);
15936c0b2b4SAndreas Gohr            case 'addComponent':
16036c0b2b4SAndreas Gohr                $type = array_shift($args);
16136c0b2b4SAndreas Gohr                $component = array_shift($args);
16236c0b2b4SAndreas Gohr                return $this->cmdAddComponent($type, $component);
16336c0b2b4SAndreas Gohr            case 'deletedFiles':
16436c0b2b4SAndreas Gohr                return $this->cmdDeletedFiles();
165c5c85a97SAndreas Gohr            case 'rmObsolete':
1661a23d1dbSAndreas Gohr                return $this->cmdRmObsolete();
1671a23d1dbSAndreas Gohr            case 'downloadSvg':
1681a23d1dbSAndreas Gohr                $ident = array_shift($args);
1691a23d1dbSAndreas Gohr                $save = array_shift($args);
17070316b84SAndreas Gohr                $keep = $options->getOpt('keep-ns');
17192738407SAndreas Gohr                return $this->cmdDownloadSVG($ident, $save, $keep);
1721a23d1dbSAndreas Gohr            case 'cleanSvg':
17370316b84SAndreas Gohr                $keep = $options->getOpt('keep-ns');
1748f82d673SAndreas Gohr                return $this->cmdCleanSVG($args, $keep);
1755586e97bSAndreas Gohr            case 'cleanLang':
1765586e97bSAndreas Gohr                return $this->cmdCleanLang();
177ec0a687bSAndreas Gohr            case 'test':
17857732a2dSAndreas Gohr                $filter = $options->getOpt('filter');
17957732a2dSAndreas Gohr                return $this->cmdTest($filter, $args);
180ec0a687bSAndreas Gohr            case 'check':
181ec0a687bSAndreas Gohr                return $this->cmdCheck($args);
182ec0a687bSAndreas Gohr            case 'fix':
183ec0a687bSAndreas Gohr                return $this->cmdFix();
18436c0b2b4SAndreas Gohr            default:
1851a23d1dbSAndreas Gohr                $this->error('Unknown command');
18636c0b2b4SAndreas Gohr                echo $options->help();
18736c0b2b4SAndreas Gohr                return 0;
18836c0b2b4SAndreas Gohr        }
18936c0b2b4SAndreas Gohr    }
19036c0b2b4SAndreas Gohr
19136c0b2b4SAndreas Gohr    /**
19236c0b2b4SAndreas Gohr     * Get the extension name from the current working directory
19336c0b2b4SAndreas Gohr     *
19436c0b2b4SAndreas Gohr     * @throws CliException if something's wrong
19536c0b2b4SAndreas Gohr     * @param string $dir
19636c0b2b4SAndreas Gohr     * @return string[] name, type
19736c0b2b4SAndreas Gohr     */
19836c0b2b4SAndreas Gohr    protected function getTypedNameFromDir($dir)
19936c0b2b4SAndreas Gohr    {
20036c0b2b4SAndreas Gohr        $pdir = fullpath(DOKU_PLUGIN);
20136c0b2b4SAndreas Gohr        $tdir = fullpath(tpl_incdir() . '../');
20236c0b2b4SAndreas Gohr
203*fe060d0dSAndreas Gohr        if (str_starts_with($dir, $pdir)) {
20436c0b2b4SAndreas Gohr            $ldir = substr($dir, strlen($pdir));
20536c0b2b4SAndreas Gohr            $type = 'plugin';
206*fe060d0dSAndreas Gohr        } elseif (str_starts_with($dir, $tdir)) {
20736c0b2b4SAndreas Gohr            $ldir = substr($dir, strlen($tdir));
20836c0b2b4SAndreas Gohr            $type = 'template';
20936c0b2b4SAndreas Gohr        } else {
21036c0b2b4SAndreas Gohr            throw new CliException('Current directory needs to be in plugin or template directory');
21136c0b2b4SAndreas Gohr        }
21236c0b2b4SAndreas Gohr
21336c0b2b4SAndreas Gohr        $ldir = trim($ldir, '/');
21436c0b2b4SAndreas Gohr
215*fe060d0dSAndreas Gohr        if (str_contains($ldir, '/')) {
21636c0b2b4SAndreas Gohr            throw new CliException('Current directory has to be main extension directory');
21736c0b2b4SAndreas Gohr        }
21836c0b2b4SAndreas Gohr
21936c0b2b4SAndreas Gohr        return [$ldir, $type];
22036c0b2b4SAndreas Gohr    }
22136c0b2b4SAndreas Gohr
22236c0b2b4SAndreas Gohr    /**
22336c0b2b4SAndreas Gohr     * Interactively ask for a value from the user
22436c0b2b4SAndreas Gohr     *
22536c0b2b4SAndreas Gohr     * @param string $prompt
22636c0b2b4SAndreas Gohr     * @param bool $cache cache given value for next time?
22736c0b2b4SAndreas Gohr     * @return string
22836c0b2b4SAndreas Gohr     */
22936c0b2b4SAndreas Gohr    protected function readLine($prompt, $cache = false)
23036c0b2b4SAndreas Gohr    {
23136c0b2b4SAndreas Gohr        $value = '';
23236c0b2b4SAndreas Gohr        $default = '';
23336c0b2b4SAndreas Gohr        $cachename = getCacheName($prompt, '.readline');
23436c0b2b4SAndreas Gohr        if ($cache && file_exists($cachename)) {
23536c0b2b4SAndreas Gohr            $default = file_get_contents($cachename);
23636c0b2b4SAndreas Gohr        }
23736c0b2b4SAndreas Gohr
23836c0b2b4SAndreas Gohr        while ($value === '') {
23936c0b2b4SAndreas Gohr            echo $prompt;
24036c0b2b4SAndreas Gohr            if ($default) echo ' [' . $default . ']';
24136c0b2b4SAndreas Gohr            echo ': ';
24236c0b2b4SAndreas Gohr
24336c0b2b4SAndreas Gohr            $fh = fopen('php://stdin', 'r');
24436c0b2b4SAndreas Gohr            $value = trim(fgets($fh));
24536c0b2b4SAndreas Gohr            fclose($fh);
24636c0b2b4SAndreas Gohr
24736c0b2b4SAndreas Gohr            if ($value === '') $value = $default;
24836c0b2b4SAndreas Gohr        }
24936c0b2b4SAndreas Gohr
25036c0b2b4SAndreas Gohr        if ($cache) {
25136c0b2b4SAndreas Gohr            file_put_contents($cachename, $value);
25236c0b2b4SAndreas Gohr        }
25336c0b2b4SAndreas Gohr
25436c0b2b4SAndreas Gohr        return $value;
25536c0b2b4SAndreas Gohr    }
25636c0b2b4SAndreas Gohr
25736c0b2b4SAndreas Gohr    /**
25870316b84SAndreas Gohr     * Create the given files with their given content
25936c0b2b4SAndreas Gohr     *
26070316b84SAndreas Gohr     * Ignores all files that already exist
26170316b84SAndreas Gohr     *
26270316b84SAndreas Gohr     * @param array $files A File array as created by Skeletor::getFiles()
26336c0b2b4SAndreas Gohr     */
264fcb8165bSAndreas Gohr    protected function createFiles($files)
265fcb8165bSAndreas Gohr    {
26670316b84SAndreas Gohr        foreach ($files as $path => $content) {
26770316b84SAndreas Gohr            if (file_exists($path)) {
26870316b84SAndreas Gohr                $this->error($path . ' already exists');
26970316b84SAndreas Gohr                continue;
27036c0b2b4SAndreas Gohr            }
27136c0b2b4SAndreas Gohr
27270316b84SAndreas Gohr            io_makeFileDir($path);
27370316b84SAndreas Gohr            file_put_contents($path, $content);
27470316b84SAndreas Gohr            $this->success($path . ' created');
27536c0b2b4SAndreas Gohr        }
27636c0b2b4SAndreas Gohr    }
27736c0b2b4SAndreas Gohr
27836c0b2b4SAndreas Gohr    /**
279c5c85a97SAndreas Gohr     * Delete the given file if it exists
280c5c85a97SAndreas Gohr     *
281c5c85a97SAndreas Gohr     * @param string $file
282c5c85a97SAndreas Gohr     */
283c5c85a97SAndreas Gohr    protected function deleteFile($file)
284c5c85a97SAndreas Gohr    {
285c5c85a97SAndreas Gohr        if (!file_exists($file)) return;
286c5c85a97SAndreas Gohr        if (@unlink($file)) {
287c5c85a97SAndreas Gohr            $this->success('Delete ' . $file);
288c5c85a97SAndreas Gohr        }
289c5c85a97SAndreas Gohr    }
290c5c85a97SAndreas Gohr
291c5c85a97SAndreas Gohr    /**
292c5c85a97SAndreas Gohr     * Run git with the given arguments and return the output
293c5c85a97SAndreas Gohr     *
294c5c85a97SAndreas Gohr     * @throws CliException when the command can't be run
295c5c85a97SAndreas Gohr     * @param string ...$args
296c5c85a97SAndreas Gohr     * @return string[]
297c5c85a97SAndreas Gohr     */
298c5c85a97SAndreas Gohr    protected function git(...$args)
299c5c85a97SAndreas Gohr    {
300*fe060d0dSAndreas Gohr        $args = array_map(escapeshellarg(...), $args);
301*fe060d0dSAndreas Gohr        $cmd = 'git ' . implode(' ', $args);
302c5c85a97SAndreas Gohr        $output = [];
303c5c85a97SAndreas Gohr        $result = 0;
304c5c85a97SAndreas Gohr
305c5c85a97SAndreas Gohr        $this->info($cmd);
306c5c85a97SAndreas Gohr        $last = exec($cmd, $output, $result);
307c5c85a97SAndreas Gohr        if ($last === false || $result !== 0) {
308c5c85a97SAndreas Gohr            throw new CliException('Running git failed');
309c5c85a97SAndreas Gohr        }
310c5c85a97SAndreas Gohr
311c5c85a97SAndreas Gohr        return $output;
312c5c85a97SAndreas Gohr    }
313c5c85a97SAndreas Gohr
314c5c85a97SAndreas Gohr    // region Commands
315c5c85a97SAndreas Gohr
316c5c85a97SAndreas Gohr    /**
31736c0b2b4SAndreas Gohr     * Intialize the current directory as a plugin or template
31836c0b2b4SAndreas Gohr     *
31936c0b2b4SAndreas Gohr     * @return int
32036c0b2b4SAndreas Gohr     */
32136c0b2b4SAndreas Gohr    protected function cmdInit()
32236c0b2b4SAndreas Gohr    {
32336c0b2b4SAndreas Gohr        $dir = fullpath(getcwd());
32436c0b2b4SAndreas Gohr        if ((new FilesystemIterator($dir))->valid()) {
325fcb8165bSAndreas Gohr            // existing directory, initialize from info file
326fcb8165bSAndreas Gohr            $skeletor = Skeletor::fromDir($dir);
327fcb8165bSAndreas Gohr        } else {
328fcb8165bSAndreas Gohr            // new directory, ask for info
32970316b84SAndreas Gohr            [$base, $type] = $this->getTypedNameFromDir($dir);
33036c0b2b4SAndreas Gohr            $user = $this->readLine('Your Name', true);
33136c0b2b4SAndreas Gohr            $mail = $this->readLine('Your E-Mail', true);
33236c0b2b4SAndreas Gohr            $desc = $this->readLine('Short description');
33370316b84SAndreas Gohr            $skeletor = new Skeletor($type, $base, $desc, $user, $mail);
334fcb8165bSAndreas Gohr        }
33570316b84SAndreas Gohr        $skeletor->addBasics();
33670316b84SAndreas Gohr        $this->createFiles($skeletor->getFiles());
33736c0b2b4SAndreas Gohr
338fcb8165bSAndreas Gohr        if (!is_dir("$dir/.git")) {
3398b06c9ddSAndreas Gohr            try {
3408b06c9ddSAndreas Gohr                $this->git('init');
3418b06c9ddSAndreas Gohr            } catch (CliException $e) {
3428b06c9ddSAndreas Gohr                $this->error($e->getMessage());
3438b06c9ddSAndreas Gohr            }
344fcb8165bSAndreas Gohr        }
3458b06c9ddSAndreas Gohr
34636c0b2b4SAndreas Gohr        return 0;
34736c0b2b4SAndreas Gohr    }
34836c0b2b4SAndreas Gohr
34936c0b2b4SAndreas Gohr    /**
35036c0b2b4SAndreas Gohr     * Add test framework
35136c0b2b4SAndreas Gohr     *
35236c0b2b4SAndreas Gohr     * @param string $test Name of the Test to add
35336c0b2b4SAndreas Gohr     * @return int
35436c0b2b4SAndreas Gohr     */
35536c0b2b4SAndreas Gohr    protected function cmdAddTest($test = '')
35636c0b2b4SAndreas Gohr    {
35770316b84SAndreas Gohr        $skeletor = Skeletor::fromDir(getcwd());
35870316b84SAndreas Gohr        $skeletor->addTest($test);
35970316b84SAndreas Gohr        $this->createFiles($skeletor->getFiles());
36036c0b2b4SAndreas Gohr        return 0;
36136c0b2b4SAndreas Gohr    }
36236c0b2b4SAndreas Gohr
36336c0b2b4SAndreas Gohr    /**
36436c0b2b4SAndreas Gohr     * Add configuration
36536c0b2b4SAndreas Gohr     *
36636c0b2b4SAndreas Gohr     * @return int
36736c0b2b4SAndreas Gohr     */
36836c0b2b4SAndreas Gohr    protected function cmdAddConf()
36936c0b2b4SAndreas Gohr    {
37070316b84SAndreas Gohr        $skeletor = Skeletor::fromDir(getcwd());
37170316b84SAndreas Gohr        $skeletor->addConf(is_dir('lang'));
37270316b84SAndreas Gohr        $this->createFiles($skeletor->getFiles());
37336c0b2b4SAndreas Gohr        return 0;
37436c0b2b4SAndreas Gohr    }
37536c0b2b4SAndreas Gohr
37636c0b2b4SAndreas Gohr    /**
37736c0b2b4SAndreas Gohr     * Add language
37836c0b2b4SAndreas Gohr     *
37936c0b2b4SAndreas Gohr     * @return int
38036c0b2b4SAndreas Gohr     */
38136c0b2b4SAndreas Gohr    protected function cmdAddLang()
38236c0b2b4SAndreas Gohr    {
38370316b84SAndreas Gohr        $skeletor = Skeletor::fromDir(getcwd());
38470316b84SAndreas Gohr        $skeletor->addLang(is_dir('conf'));
38570316b84SAndreas Gohr        $this->createFiles($skeletor->getFiles());
38636c0b2b4SAndreas Gohr        return 0;
38736c0b2b4SAndreas Gohr    }
38836c0b2b4SAndreas Gohr
38936c0b2b4SAndreas Gohr    /**
390719b4841SAndreas Gohr     * Add AGENTS.md
391719b4841SAndreas Gohr     *
392719b4841SAndreas Gohr     * @return int
393719b4841SAndreas Gohr     */
394488499ccSAndreas Gohr    protected function cmdAddAgents($claude)
395719b4841SAndreas Gohr    {
396719b4841SAndreas Gohr        $skeletor = Skeletor::fromDir(getcwd());
397719b4841SAndreas Gohr        $skeletor->addAgents();
398719b4841SAndreas Gohr        $this->createFiles($skeletor->getFiles());
399488499ccSAndreas Gohr        if ($claude && !file_exists('CLAUDE.md')) {
400*fe060d0dSAndreas Gohr            if (symlink('AGENTS.md', 'CLAUDE.md')) {
401*fe060d0dSAndreas Gohr                $this->success('Created symlink CLAUDE.md -> AGENTS.md');
402*fe060d0dSAndreas Gohr            }
403488499ccSAndreas Gohr        }
404719b4841SAndreas Gohr        return 0;
405719b4841SAndreas Gohr    }
406719b4841SAndreas Gohr
407719b4841SAndreas Gohr    /**
40836c0b2b4SAndreas Gohr     * Add another component to the plugin
40936c0b2b4SAndreas Gohr     *
41036c0b2b4SAndreas Gohr     * @param string $type
41136c0b2b4SAndreas Gohr     * @param string $component
41236c0b2b4SAndreas Gohr     */
41336c0b2b4SAndreas Gohr    protected function cmdAddComponent($type, $component = '')
41436c0b2b4SAndreas Gohr    {
41570316b84SAndreas Gohr        $skeletor = Skeletor::fromDir(getcwd());
41670316b84SAndreas Gohr        $skeletor->addComponent($type, $component);
41770316b84SAndreas Gohr        $this->createFiles($skeletor->getFiles());
41836c0b2b4SAndreas Gohr        return 0;
41936c0b2b4SAndreas Gohr    }
42036c0b2b4SAndreas Gohr
42136c0b2b4SAndreas Gohr    /**
42236c0b2b4SAndreas Gohr     * Generate a list of deleted files from git
42336c0b2b4SAndreas Gohr     *
42436c0b2b4SAndreas Gohr     * @link https://stackoverflow.com/a/6018049/172068
42536c0b2b4SAndreas Gohr     */
42636c0b2b4SAndreas Gohr    protected function cmdDeletedFiles()
42736c0b2b4SAndreas Gohr    {
4288b06c9ddSAndreas Gohr        if (!is_dir('.git')) throw new CliException('This extension seems not to be managed by git');
42936c0b2b4SAndreas Gohr
4308b06c9ddSAndreas Gohr        $output = $this->git('log', '--no-renames', '--pretty=format:', '--name-only', '--diff-filter=D');
431*fe060d0dSAndreas Gohr        $output = array_map(trim(...), $output);
43236c0b2b4SAndreas Gohr        $output = array_filter($output);
43336c0b2b4SAndreas Gohr        $output = array_unique($output);
434*fe060d0dSAndreas Gohr        $output = array_filter($output, fn($item) => !file_exists($item));
43536c0b2b4SAndreas Gohr        sort($output);
43636c0b2b4SAndreas Gohr
437*fe060d0dSAndreas Gohr        if ($output === []) {
43836c0b2b4SAndreas Gohr            $this->info('No deleted files found');
43936c0b2b4SAndreas Gohr            return 0;
44036c0b2b4SAndreas Gohr        }
44136c0b2b4SAndreas Gohr
44236c0b2b4SAndreas Gohr        $content = "# This is a list of files that were present in previous releases\n" .
44336c0b2b4SAndreas Gohr            "# but were removed later. They should not exist in your installation.\n" .
444*fe060d0dSAndreas Gohr            implode("\n", $output) . "\n";
44536c0b2b4SAndreas Gohr
44636c0b2b4SAndreas Gohr        file_put_contents('deleted.files', $content);
44736c0b2b4SAndreas Gohr        $this->success('written deleted.files');
44836c0b2b4SAndreas Gohr        return 0;
44936c0b2b4SAndreas Gohr    }
4508b06c9ddSAndreas Gohr
4518b06c9ddSAndreas Gohr    /**
452c5c85a97SAndreas Gohr     * Remove files that shouldn't be here anymore
4538b06c9ddSAndreas Gohr     */
4541a23d1dbSAndreas Gohr    protected function cmdRmObsolete()
4558b06c9ddSAndreas Gohr    {
456c5c85a97SAndreas Gohr        $this->deleteFile('_test/general.test.php');
457c5c85a97SAndreas Gohr        $this->deleteFile('.travis.yml');
45853bec4caSAndreas Gohr        $this->deleteFile('.github/workflows/phpTestLinux.yml');
4598b06c9ddSAndreas Gohr
460c5c85a97SAndreas Gohr        return 0;
4618b06c9ddSAndreas Gohr    }
4628b06c9ddSAndreas Gohr
4631a23d1dbSAndreas Gohr    /**
4641a23d1dbSAndreas Gohr     * Download a remote icon
4651a23d1dbSAndreas Gohr     *
4661a23d1dbSAndreas Gohr     * @param string $ident
4671a23d1dbSAndreas Gohr     * @param string $save
46892738407SAndreas Gohr     * @param bool $keep
4691a23d1dbSAndreas Gohr     * @return int
4701a23d1dbSAndreas Gohr     * @throws Exception
4711a23d1dbSAndreas Gohr     */
47292738407SAndreas Gohr    protected function cmdDownloadSVG($ident, $save = '', $keep = false)
4731a23d1dbSAndreas Gohr    {
4741a23d1dbSAndreas Gohr        $svg = new SVGIcon($this);
47592738407SAndreas Gohr        $svg->keepNamespace($keep);
4761a23d1dbSAndreas Gohr        return (int)$svg->downloadRemoteIcon($ident, $save);
4771a23d1dbSAndreas Gohr    }
4781a23d1dbSAndreas Gohr
4791a23d1dbSAndreas Gohr    /**
4808f82d673SAndreas Gohr     * @param string[] $files
48192738407SAndreas Gohr     * @param bool $keep
4821a23d1dbSAndreas Gohr     * @return int
4831a23d1dbSAndreas Gohr     * @throws Exception
4841a23d1dbSAndreas Gohr     */
4858f82d673SAndreas Gohr    protected function cmdCleanSVG($files, $keep = false)
4861a23d1dbSAndreas Gohr    {
4871a23d1dbSAndreas Gohr        $svg = new SVGIcon($this);
48892738407SAndreas Gohr        $svg->keepNamespace($keep);
4898f82d673SAndreas Gohr
4908f82d673SAndreas Gohr        $ok = true;
4918f82d673SAndreas Gohr        foreach ($files as $file) {
4928f82d673SAndreas Gohr            $ok = $ok && $svg->cleanSVGFile($file);
4938f82d673SAndreas Gohr        }
4948f82d673SAndreas Gohr        return (int)$ok;
4951a23d1dbSAndreas Gohr    }
4961a23d1dbSAndreas Gohr
4975586e97bSAndreas Gohr    /**
4985586e97bSAndreas Gohr     * @return int
4995586e97bSAndreas Gohr     */
5005586e97bSAndreas Gohr    protected function cmdCleanLang()
5015586e97bSAndreas Gohr    {
5025586e97bSAndreas Gohr        $lp = new LangProcessor($this);
5035586e97bSAndreas Gohr
5045586e97bSAndreas Gohr        $files = glob('./lang/*/lang.php');
5055b2e8f12SAndreas Gohr        foreach ($files as $file) {
5065b2e8f12SAndreas Gohr            $lp->processLangFile($file);
5075b2e8f12SAndreas Gohr        }
5085b2e8f12SAndreas Gohr
5095b2e8f12SAndreas Gohr        $files = glob('./lang/*/settings.php');
5105586e97bSAndreas Gohr        foreach ($files as $file) {
511f4f76afdSAndreas Gohr            $lp->processSettingsFile($file);
5125586e97bSAndreas Gohr        }
5135586e97bSAndreas Gohr
5145586e97bSAndreas Gohr        return 0;
5155586e97bSAndreas Gohr    }
5165586e97bSAndreas Gohr
517ec0a687bSAndreas Gohr    /**
51857732a2dSAndreas Gohr     * Run the unit tests for this extension
51957732a2dSAndreas Gohr     *
52057732a2dSAndreas Gohr     * @param string $filter Optional filter string for phpunit
52157732a2dSAndreas Gohr     * @param string[] $args Additional arguments to pass to phpunit (files)
522ec0a687bSAndreas Gohr     * @return int
523ec0a687bSAndreas Gohr     */
52457732a2dSAndreas Gohr    protected function cmdTest($filter = '', $args = [])
525ec0a687bSAndreas Gohr    {
526ec0a687bSAndreas Gohr        $dir = fullpath(getcwd());
527ec0a687bSAndreas Gohr        [$base, $type] = $this->getTypedNameFromDir($dir);
528ec0a687bSAndreas Gohr
529ec0a687bSAndreas Gohr        if ($this->colors->isEnabled()) {
530ec0a687bSAndreas Gohr            $colors = 'always';
531ec0a687bSAndreas Gohr        } else {
532ec0a687bSAndreas Gohr            $colors = 'never';
533ec0a687bSAndreas Gohr        }
534ec0a687bSAndreas Gohr
535*fe060d0dSAndreas Gohr        $bin = fullpath(__DIR__ . '/../../../_test/vendor/bin/phpunit');
536*fe060d0dSAndreas Gohr        ;
53757732a2dSAndreas Gohr        if (!file_exists($bin)) {
53857732a2dSAndreas Gohr            $this->error('Testing framework not found. Please run "composer install" in the _test/ directory first.');
53957732a2dSAndreas Gohr            return 1;
54057732a2dSAndreas Gohr        }
54157732a2dSAndreas Gohr
54257732a2dSAndreas Gohr        $runArgs = [
54357732a2dSAndreas Gohr            $bin,
544ec0a687bSAndreas Gohr            '--verbose',
545ec0a687bSAndreas Gohr            "--colors=$colors",
546ec0a687bSAndreas Gohr            '--configuration', fullpath(__DIR__ . '/../../../_test/phpunit.xml'),
547ec0a687bSAndreas Gohr            '--group', $type . '_' . $base,
548ec0a687bSAndreas Gohr        ];
54957732a2dSAndreas Gohr        if ($filter) {
55057732a2dSAndreas Gohr            $runArgs[] = '--filter';
55157732a2dSAndreas Gohr            $runArgs[] = $filter;
55257732a2dSAndreas Gohr        }
55357732a2dSAndreas Gohr
55457732a2dSAndreas Gohr        $runArgs = array_merge($runArgs, $args);
555*fe060d0dSAndreas Gohr        $cmd = implode(' ', array_map(escapeshellarg(...), $runArgs));
556ec0a687bSAndreas Gohr        $this->info("Running $cmd");
557ec0a687bSAndreas Gohr
558ec0a687bSAndreas Gohr        $result = 0;
559ec0a687bSAndreas Gohr        passthru($cmd, $result);
560ec0a687bSAndreas Gohr        return $result;
561ec0a687bSAndreas Gohr    }
562ec0a687bSAndreas Gohr
563ec0a687bSAndreas Gohr    /**
564ec0a687bSAndreas Gohr     * @return int
565ec0a687bSAndreas Gohr     */
566ec0a687bSAndreas Gohr    protected function cmdCheck($files = [])
567ec0a687bSAndreas Gohr    {
568ec0a687bSAndreas Gohr        $dir = fullpath(getcwd());
569ec0a687bSAndreas Gohr
570ec0a687bSAndreas Gohr        $args = [
571ec0a687bSAndreas Gohr            fullpath(__DIR__ . '/../../../_test/vendor/bin/phpcs'),
572ec0a687bSAndreas Gohr            '--standard=' . fullpath(__DIR__ . '/../../../_test/phpcs.xml'),
573ec0a687bSAndreas Gohr            ($this->colors->isEnabled()) ? '--colors' : '--no-colors',
574ec0a687bSAndreas Gohr            '--',
575ec0a687bSAndreas Gohr        ];
576ec0a687bSAndreas Gohr
577ec0a687bSAndreas Gohr        if ($files) {
578ec0a687bSAndreas Gohr            $args = array_merge($args, $files);
579ec0a687bSAndreas Gohr        } else {
580ec0a687bSAndreas Gohr            $args[] = fullpath($dir);
581ec0a687bSAndreas Gohr        }
582ec0a687bSAndreas Gohr
583*fe060d0dSAndreas Gohr        $cmd = implode(' ', array_map(escapeshellarg(...), $args));
584ec0a687bSAndreas Gohr        $this->info("Running $cmd");
585ec0a687bSAndreas Gohr
586ec0a687bSAndreas Gohr        $result = 0;
587ec0a687bSAndreas Gohr        passthru($cmd, $result);
588ec0a687bSAndreas Gohr        return $result;
589ec0a687bSAndreas Gohr    }
590ec0a687bSAndreas Gohr
591ec0a687bSAndreas Gohr    /**
592ec0a687bSAndreas Gohr     * @return int
593ec0a687bSAndreas Gohr     */
594ec0a687bSAndreas Gohr    protected function cmdFix($files = [])
595ec0a687bSAndreas Gohr    {
596ec0a687bSAndreas Gohr        $dir = fullpath(getcwd());
597ec0a687bSAndreas Gohr
598ec0a687bSAndreas Gohr        // first run rector to refactor outdated code
599ec0a687bSAndreas Gohr        $args = [
600ec0a687bSAndreas Gohr            fullpath(__DIR__ . '/../../../_test/vendor/bin/rector'),
601ec0a687bSAndreas Gohr            ($this->colors->isEnabled()) ? '--ansi' : '--no-ansi',
602ec0a687bSAndreas Gohr            '--config=' . fullpath(__DIR__ . '/../../../_test/rector.php'),
603ec0a687bSAndreas Gohr            '--no-diffs',
604ec0a687bSAndreas Gohr            'process',
605ec0a687bSAndreas Gohr        ];
606ec0a687bSAndreas Gohr
607ec0a687bSAndreas Gohr        if ($files) {
608ec0a687bSAndreas Gohr            $args = array_merge($args, $files);
609ec0a687bSAndreas Gohr        } else {
610ec0a687bSAndreas Gohr            $args[] = fullpath($dir);
611ec0a687bSAndreas Gohr        }
612ec0a687bSAndreas Gohr
613*fe060d0dSAndreas Gohr        $cmd = implode(' ', array_map(escapeshellarg(...), $args));
614ec0a687bSAndreas Gohr        $this->info("Running $cmd");
615ec0a687bSAndreas Gohr
616ec0a687bSAndreas Gohr        $result = 0;
617ec0a687bSAndreas Gohr        passthru($cmd, $result);
618ec0a687bSAndreas Gohr        if ($result !== 0) return $result;
619ec0a687bSAndreas Gohr
620ec0a687bSAndreas Gohr        // now run phpcbf to clean up code style
621ec0a687bSAndreas Gohr        $args = [
622ec0a687bSAndreas Gohr            fullpath(__DIR__ . '/../../../_test/vendor/bin/phpcbf'),
623ec0a687bSAndreas Gohr            '--standard=' . fullpath(__DIR__ . '/../../../_test/phpcs.xml'),
624ec0a687bSAndreas Gohr            ($this->colors->isEnabled()) ? '--colors' : '--no-colors',
625ec0a687bSAndreas Gohr            '--',
626ec0a687bSAndreas Gohr        ];
627ec0a687bSAndreas Gohr
628ec0a687bSAndreas Gohr        if ($files) {
629ec0a687bSAndreas Gohr            $args = array_merge($args, $files);
630ec0a687bSAndreas Gohr        } else {
631ec0a687bSAndreas Gohr            $args[] = fullpath($dir);
632ec0a687bSAndreas Gohr        }
633ec0a687bSAndreas Gohr
634*fe060d0dSAndreas Gohr        $cmd = implode(' ', array_map(escapeshellarg(...), $args));
635ec0a687bSAndreas Gohr        $this->info("Running $cmd");
636ec0a687bSAndreas Gohr
637ec0a687bSAndreas Gohr        $result = 0;
638ec0a687bSAndreas Gohr        passthru($cmd, $result);
639ec0a687bSAndreas Gohr        return $result;
640ec0a687bSAndreas Gohr    }
641ec0a687bSAndreas Gohr
642c5c85a97SAndreas Gohr    //endregion
64336c0b2b4SAndreas Gohr}
644