Public Access
558cd6043d
Platform: mokoplatform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: mokoplatform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: mokoplatform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Platform: mokoplatform CI / Gate 1: Code Quality (pull_request) Failing after 1m2s
- Fix #1: replace nonexistent menu() with choose() using select() - Fix #2: constructor — pass name only, not description as version - Fix #3: respect --non-interactive flag (skip prompts, use defaults) - Fix #4: use json_encode for composer/package.json (prevent injection) - Fix #5: remove pointless count() wrapper - Fix #6: validate --path exists or can be created before proceeding - Fix TOML description escaping
430 lines
14 KiB
PHP
430 lines
14 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
* 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 <<<MD
|
|
# {$name}
|
|
|
|
{$desc}
|
|
|
|
## License
|
|
|
|
{$license}
|
|
MD;
|
|
}
|
|
|
|
private function changelog(): string
|
|
{
|
|
return <<<MD
|
|
# Changelog
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
- Initial project scaffold
|
|
MD;
|
|
}
|
|
|
|
private function composerJson(): string
|
|
{
|
|
$data = [
|
|
'name' => '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'
|
|
<?xml version="1.0"?>
|
|
<ruleset name="MokoCli">
|
|
<rule ref="PSR12"/>
|
|
<file>source/</file>
|
|
<exclude-pattern>vendor/*</exclude-pattern>
|
|
</ruleset>
|
|
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 <<<TOML
|
|
[project]
|
|
name = "{$name}"
|
|
version = "0.1.0"
|
|
description = "{$desc}"
|
|
requires-python = ">=3.10"
|
|
|
|
[build-system]
|
|
requires = ["setuptools>=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());
|