#!/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 = []; /** When true, skip all interactive prompts and use defaults. */ private bool $nonInteractive = false; 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 { $rawPath = $this->getArgument('--path', '.'); $targetPath = realpath($rawPath) ?: $rawPath; $this->nonInteractive = (bool) $this->getArgument('--non-interactive', false); // Validate target path if (!is_dir($targetPath) && !@mkdir($targetPath, 0755, true)) { $this->log('ERROR', "Target path does not exist and cannot be created: {$targetPath}"); return self::EXIT_USAGE; } $targetPath = realpath($targetPath) ?: $targetPath; $this->section('MokoCli Repository Wizard'); // ── Gather info ────────────────────────────────────────────── $this->answers['name'] = $this->ask('Repository name', basename($targetPath)); $this->answers['platform'] = $this->choose('Platform type', self::PLATFORMS, 'generic'); $this->answers['org'] = $this->ask('Organization', 'MokoConsulting'); $this->answers['description'] = $this->ask('Description', ''); $this->answers['license'] = $this->choose('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 ' . 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()}"); } } // ── Interactive helpers (respect --non-interactive) ───────────── private function ask(string $prompt, string $default): string { if ($this->nonInteractive) { return $default; } return $this->input($prompt, $default); } private function choose(string $prompt, array $options, string $default): string { if ($this->nonInteractive) { return $default; } $keys = array_keys($options); $labels = []; foreach ($options as $key => $desc) { $labels[] = "{$key} — {$desc}"; } $chosen = $this->select($prompt, $labels); // Extract the key from "key — description" $chosenKey = explode(' — ', $chosen, 2)[0] ?? $default; return in_array($chosenKey, $keys, true) ? $chosenKey : $default; } // ── 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 << 'mokoconsulting/' . strtolower($this->answers['name']), 'description' => $this->answers['description'] ?: $this->answers['name'], 'type' => 'library', 'license' => $this->answers['license'], 'require' => ['php' => '>=8.1'], 'autoload' => ['psr-4' => new \stdClass()], ]; return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; } 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 { $data = [ 'name' => '@mokoconsulting/' . strtolower($this->answers['name']), 'version' => '0.1.0', 'description' => $this->answers['description'] ?: $this->answers['name'], 'type' => 'module', 'scripts' => ['build' => 'tsc', 'start' => 'node dist/index.js'], 'devDependencies' => ['typescript' => '^5.0'], ]; return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; } private function tsconfigJson(): string { return <<<'JSON' { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "declaration": true }, "include": ["src/**/*"] } JSON; } private function eslintrc(): string { return <<<'JSON' { "root": true, "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"] } JSON; } private function pyprojectToml(): string { $name = strtolower($this->answers['name']); $desc = str_replace(['\\', '"'], ['\\\\', '\\"'], $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'); exit($app->execute());