From 1cf076f088f5b08ee55e08e199a35095e5a35f52 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 20 Jun 2026 23:33:19 -0500 Subject: [PATCH] feat: interactive repo configuration wizard (#145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `repo:wizard` command — walks through platform selection, generates config files (phpcs, phpstan, eslint, tsconfig, composer/package.json, .editorconfig, README, CHANGELOG, .gitignore, workflows), and optionally creates the repo on Gitea via API. Supports --dry-run, --non-interactive, and --create-remote flags. --- bin/moko | 1 + cli/repo_wizard.php | 411 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 cli/repo_wizard.php diff --git a/bin/moko b/bin/moko index dd449b8..d484dff 100644 --- a/bin/moko +++ b/bin/moko @@ -178,6 +178,7 @@ const COMMAND_MAP = [ 'repo:archive' => 'cli/archive_repo.php', 'repo:scaffold-client' => 'cli/scaffold_client.php', 'repo:provision' => 'cli/client_provision.php', + 'repo:wizard' => 'cli/repo_wizard.php', 'repo:rename-branch' => 'cli/branch_rename.php', 'repo:reset-dev' => 'cli/dev_branch_reset.php', diff --git a/cli/repo_wizard.php b/cli/repo_wizard.php new file mode 100644 index 0000000..5dc612d --- /dev/null +++ b/cli/repo_wizard.php @@ -0,0 +1,411 @@ +#!/usr/bin/env php + + * SPDX-License-Identifier: GPL-3.0-or-later + * FILE INFORMATION + * DEFGROUP: mokocli.CLI + * INGROUP: mokocli + * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli + * PATH: /cli/repo_wizard.php + * BRIEF: Interactive configuration wizard for new repositories + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoCli\{CliFramework, Config, PlatformAdapterFactory}; + +/** + * Interactive repo setup wizard. + * + * Walks through platform selection, generates config files, workflows, + * and optionally creates the repo on Gitea via API. + * + * @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/145 + */ +class RepoWizard extends CliFramework +{ + private const PLATFORMS = [ + 'joomla' => 'Joomla extension (component, module, plugin, package)', + 'dolibarr' => 'Dolibarr ERP module', + 'nodejs' => 'Node.js / TypeScript project', + 'python' => 'Python project', + 'mcp-server' => 'MCP server (Model Context Protocol)', + 'generic' => 'Generic PHP or multi-language project', + ]; + + private const LICENSES = [ + 'GPL-3.0-or-later' => 'GNU General Public License v3', + 'MIT' => 'MIT License', + 'Apache-2.0' => 'Apache License 2.0', + 'proprietary' => 'Proprietary / All rights reserved', + ]; + + /** Collected wizard answers. */ + private array $answers = []; + + protected function configure(): void + { + $this->setDescription('Interactive configuration wizard for new repositories'); + $this->addArgument('--path', 'Directory to generate files in', '.'); + $this->addArgument('--create-remote', 'Create repo on Gitea via API', false); + $this->addArgument('--non-interactive', 'Use defaults (no prompts)', false); + } + + protected function run(): int + { + $targetPath = realpath($this->getArgument('--path', '.')) ?: $this->getArgument('--path', '.'); + + $this->section('MokoCli Repository Wizard'); + + // ── Gather info ────────────────────────────────────────────── + $this->answers['name'] = $this->input('Repository name', basename($targetPath)); + $this->answers['platform'] = $this->menu('Platform type', self::PLATFORMS, 'generic'); + $this->answers['org'] = $this->input('Organization', 'MokoConsulting'); + $this->answers['description'] = $this->input('Description', ''); + $this->answers['license'] = $this->menu('License', self::LICENSES, 'GPL-3.0-or-later'); + + // ── Confirm ────────────────────────────────────────────────── + $this->section('Configuration Summary'); + foreach ($this->answers as $key => $value) { + $this->log('INFO', sprintf(' %-12s %s', $key . ':', $value)); + } + + if (!$this->confirm('Proceed with these settings?', true)) { + $this->log('INFO', 'Wizard cancelled'); + return 0; + } + + // ── Generate files ─────────────────────────────────────────── + $this->section('Generating files'); + $generated = $this->generateFiles($targetPath); + + foreach ($generated as $file) { + $this->status(true, $file); + } + + // ── Create remote repo ─────────────────────────────────────── + if ($this->getArgument('--create-remote', false)) { + $this->section('Creating remote repository'); + $this->createRemoteRepo(); + } + + $this->log('INFO', ''); + $this->log('INFO', "Generated {$this->count($generated)} files in {$targetPath}"); + $this->log('INFO', 'Next: git init && git add -A && git commit -m "chore: initial scaffold"'); + + return 0; + } + + // ── File generation ────────────────────────────────────────────── + + private function generateFiles(string $path): array + { + $platform = $this->answers['platform']; + $name = $this->answers['name']; + $generated = []; + + // .editorconfig + $generated[] = $this->writeFile($path, '.editorconfig', $this->editorconfig()); + + // README.md + $generated[] = $this->writeFile($path, 'README.md', $this->readme()); + + // CHANGELOG.md + $generated[] = $this->writeFile($path, 'CHANGELOG.md', $this->changelog()); + + // LICENSE + if ($this->answers['license'] !== 'proprietary') { + $generated[] = $this->writeFile($path, 'LICENSE', "See SPDX: {$this->answers['license']}"); + } + + // Platform-specific configs + switch ($platform) { + case 'joomla': + case 'dolibarr': + case 'generic': + $generated[] = $this->writeFile($path, 'phpcs.xml', $this->phpcsXml()); + $generated[] = $this->writeFile($path, 'phpstan.neon', $this->phpstanNeon()); + $generated[] = $this->writeFile($path, 'composer.json', $this->composerJson()); + break; + case 'nodejs': + case 'mcp-server': + $generated[] = $this->writeFile($path, 'package.json', $this->packageJson()); + $generated[] = $this->writeFile($path, 'tsconfig.json', $this->tsconfigJson()); + $generated[] = $this->writeFile($path, '.eslintrc.json', $this->eslintrc()); + break; + case 'python': + $generated[] = $this->writeFile($path, 'pyproject.toml', $this->pyprojectToml()); + $generated[] = $this->writeFile($path, 'requirements.txt', ''); + break; + } + + // .mokogitea/workflows + $generated[] = $this->writeFile($path, '.mokogitea/workflows/pr-check.yml', + "# PR check workflow — synced from mokocli templates\n# Run: moko sync to update\n"); + + // .gitignore + $generated[] = $this->writeFile($path, '.gitignore', $this->gitignore($platform)); + + // Source directory + $srcDir = in_array($platform, ['joomla', 'dolibarr', 'generic']) ? 'source' : 'src'; + if (!is_dir("{$path}/{$srcDir}")) { + @mkdir("{$path}/{$srcDir}", 0755, true); + $generated[] = "{$srcDir}/"; + } + + return array_filter($generated); + } + + private function writeFile(string $basePath, string $relativePath, string $content): ?string + { + $fullPath = $basePath . '/' . $relativePath; + $dir = dirname($fullPath); + + if (file_exists($fullPath)) { + $this->log('DEBUG', " SKIP {$relativePath} (already exists)"); + return null; + } + + if ($this->dryRun) { + $this->log('INFO', "[dry-run] Would create {$relativePath}"); + return $relativePath; + } + + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + } + + file_put_contents($fullPath, $content); + return $relativePath; + } + + // ── Remote repo creation ───────────────────────────────────────── + + private function createRemoteRepo(): void + { + try { + $config = Config::load(); + $adapter = PlatformAdapterFactory::create($config); + $org = $this->answers['org']; + + if ($this->dryRun) { + $this->log('INFO', "[dry-run] Would create {$org}/{$this->answers['name']} on Gitea"); + return; + } + + $result = $adapter->createRepository($org, $this->answers['name'], [ + 'description' => $this->answers['description'], + 'private' => false, + ]); + + $url = $result['html_url'] ?? "{$org}/{$this->answers['name']}"; + $this->log('INFO', "Created: {$url}"); + } catch (\Exception $e) { + $this->log('ERROR', "Failed to create remote repo: {$e->getMessage()}"); + } + } + + private function count(array $arr): int { return \count($arr); } + + // ── Template content ───────────────────────────────────────────── + + private function editorconfig(): string + { + return <<<'CONF' +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false +CONF; + } + + private function readme(): string + { + $name = $this->answers['name']; + $desc = $this->answers['description'] ?: 'A Moko Consulting project.'; + $license = $this->answers['license']; + + return <<answers['name']); + $desc = $this->answers['description'] ?: $this->answers['name']; + $license = $this->answers['license']; + + return <<=8.1" + }, + "autoload": { + "psr-4": {} + } +} +JSON; + } + + private function phpcsXml(): string + { + return <<<'XML' + + + + source/ + vendor/* + +XML; + } + + private function phpstanNeon(): string + { + return <<<'NEON' +parameters: + level: 6 + paths: + - source/ +NEON; + } + + private function packageJson(): string + { + $name = strtolower($this->answers['name']); + $desc = $this->answers['description'] ?: $this->answers['name']; + + return <<answers['name']); + $desc = $this->answers['description'] ?: $this->answers['name']; + + return <<=68.0"] +build-backend = "setuptools.build_meta" +TOML; + } + + private function gitignore(string $platform): string + { + $common = <<<'GI' +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db +desktop.ini + +# Logs +*.log +GI; + + $extra = match ($platform) { + 'joomla', 'dolibarr', 'generic' => "\n# PHP\nvendor/\n.phpunit.result.cache\n", + 'nodejs', 'mcp-server' => "\n# Node\nnode_modules/\ndist/\n*.tsbuildinfo\n", + 'python' => "\n# Python\n__pycache__/\n*.pyc\n.venv/\n*.egg-info/\n", + default => '', + }; + + return $common . $extra; + } +} + +$app = new RepoWizard('repo_wizard', 'Interactive configuration wizard for new repositories'); +exit($app->execute());