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