diff --git a/automation/bulk_joomla_template.php b/automation/bulk_joomla_template.php index 54df132..a18fb93 100644 --- a/automation/bulk_joomla_template.php +++ b/automation/bulk_joomla_template.php @@ -30,7 +30,7 @@ require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\{ AuditLogger, - CLIApp, + CliFramework, Config, GitPlatformAdapter, MetricsCollector, @@ -47,7 +47,7 @@ use MokoEnterprise\{ * * Works with both GitHub and Gitea via the PlatformAdapterFactory. */ -class BulkJoomlaTemplate extends CLIApp +class BulkJoomlaTemplate extends CliFramework { public const DEFAULT_ORG = 'MokoConsulting'; public const VERSION = '04.06.10'; @@ -56,22 +56,20 @@ class BulkJoomlaTemplate extends CLIApp private AuditLogger $logger; private Config $config; - protected function setupArguments(): array + protected function configure(): void { - return [ - 'org:' => 'Organization (default: ' . self::DEFAULT_ORG . ')', - 'scaffold' => 'Create a new Joomla template repository', - 'sync' => 'Sync MokoStandards files to existing template repos', - 'list' => 'List all joomla-template repositories', - 'name:' => 'Template name for --scaffold (e.g. MokoTheme)', - 'client:' => 'Joomla client: site (default) or administrator', - 'repos:' => 'Target repositories for --sync (comma-separated, or use --all)', - 'all' => 'Sync all repos tagged joomla-template', - 'sync-updates' => 'Sync updates.xml between Gitea and GitHub for Joomla repos', - 'private' => 'Create as private repository (--scaffold)', - 'dry-run' => 'Preview changes without making them', - 'yes' => 'Auto-confirm prompts', - ]; + $this->setDescription('Bulk Joomla template management'); + $this->addArgument('--org', 'Organization', self::DEFAULT_ORG); + $this->addArgument('--scaffold', 'Create new template repo', false); + $this->addArgument('--sync', 'Sync files to template repos', false); + $this->addArgument('--list', 'List template repos', false); + $this->addArgument('--name', 'Template name for scaffold', ''); + $this->addArgument('--client', 'Joomla client: site or admin', 'site'); + $this->addArgument('--repos', 'Target repos (comma-separated)', ''); + $this->addArgument('--all', 'Sync all tagged repos', false); + $this->addArgument('--sync-updates', 'Sync updates.xml', false); + $this->addArgument('--private', 'Create as private repo', false); + $this->addArgument('--yes', 'Auto-confirm', false); } protected function run(): int @@ -88,23 +86,23 @@ class BulkJoomlaTemplate extends CLIApp } $this->logger = new AuditLogger('joomla_template'); - $org = $this->getOption('org', self::DEFAULT_ORG); + $org = $this->getArgument('--org', self::DEFAULT_ORG); $platform = $this->adapter->getPlatformName(); $this->log("Platform: {$platform} | Organization: {$org}", 'INFO'); - if ($this->hasOption('list')) { + if ($this->getArgument('--list', false)) { return $this->listTemplateRepos($org); } - if ($this->hasOption('scaffold')) { + if ($this->getArgument('--scaffold', false)) { return $this->scaffoldTemplate($org); } - if ($this->hasOption('sync')) { + if ($this->getArgument('--sync', false)) { return $this->syncTemplates($org); } - if ($this->hasOption('sync-updates')) { + if ($this->getArgument('--sync-updates', false)) { return $this->syncUpdatesBetweenPlatforms($org); } @@ -138,9 +136,9 @@ class BulkJoomlaTemplate extends CLIApp private function scaffoldTemplate(string $org): int { - $name = $this->getOption('name', ''); - $client = $this->getOption('client', 'site'); - $dryRun = $this->hasOption('dry-run'); + $name = $this->getArgument('--name', ''); + $client = $this->getArgument('--client', 'site'); + $dryRun = $this->dryRun; if (empty($name)) { $this->log("❌ --name is required for --scaffold", 'ERROR'); @@ -176,7 +174,7 @@ class BulkJoomlaTemplate extends CLIApp } // Confirm - if (!$this->hasOption('yes')) { + if (!$this->getArgument('--yes', false)) { echo "\nCreate repository {$org}/{$name}? [y/N]: "; $handle = fopen('php://stdin', 'r'); $line = fgets($handle); @@ -192,7 +190,7 @@ class BulkJoomlaTemplate extends CLIApp // Create repository $this->log("\nCreating repository...", 'INFO'); try { - $isPrivate = $this->hasOption('private'); + $isPrivate = $this->getArgument('--private', false); $this->adapter->createOrgRepo($org, $name, [ 'description' => "Joomla {$client} template — {$name}", 'private' => $isPrivate, @@ -263,10 +261,10 @@ class BulkJoomlaTemplate extends CLIApp { $repos = []; - if ($this->hasOption('all')) { + if ($this->getArgument('--all', false)) { $repos = $this->findTemplateRepos($org); } else { - $reposArg = $this->getOption('repos', ''); + $reposArg = $this->getArgument('--repos', ''); if (empty($reposArg)) { $this->log("❌ --repos or --all required for --sync", 'ERROR'); return 1; @@ -284,7 +282,7 @@ class BulkJoomlaTemplate extends CLIApp $this->log("\nSyncing " . count($repos) . " template repo(s)...", 'INFO'); - $dryRun = $this->hasOption('dry-run'); + $dryRun = $this->dryRun; $success = 0; $failed = 0; @@ -741,7 +739,7 @@ class BulkJoomlaTemplate extends CLIApp { $repos = []; - if ($this->hasOption('all')) { + if ($this->getArgument('--all', false)) { $repos = $this->findTemplateRepos($org); // Also include waas-component repos $allRepos = $this->adapter->listOrgRepos($org, true); @@ -765,7 +763,7 @@ class BulkJoomlaTemplate extends CLIApp return true; }); } else { - $reposArg = $this->getOption('repos', ''); + $reposArg = $this->getArgument('--repos', ''); if (empty($reposArg)) { $this->log("❌ --repos or --all required for --sync-updates", 'ERROR'); return 1; @@ -791,7 +789,7 @@ class BulkJoomlaTemplate extends CLIApp $gitea = $adapters['gitea']; $github = $adapters['github']; - $dryRun = $this->hasOption('dry-run'); + $dryRun = $this->dryRun; $this->log("\nSyncing updates.xml across Gitea <-> GitHub for " . count($repos) . " repo(s)...", 'INFO'); @@ -936,10 +934,6 @@ class BulkJoomlaTemplate extends CLIApp // Execute if run directly if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) { - $app = new BulkJoomlaTemplate( - 'joomla-template', - 'Bulk scaffold and sync Joomla template repositories', - BulkJoomlaTemplate::VERSION - ); + $app = new BulkJoomlaTemplate(); exit($app->execute()); } diff --git a/automation/bulk_sync.php b/automation/bulk_sync.php index c5131ad..5fea685 100755 --- a/automation/bulk_sync.php +++ b/automation/bulk_sync.php @@ -26,7 +26,7 @@ use MokoEnterprise\{ AuditLogger, CheckpointManager, CircuitBreakerOpen, - CLIApp, + CliFramework, Config, GitPlatformAdapter, MetricsCollector, @@ -45,7 +45,7 @@ use MokoEnterprise\{ * Synchronizes MokoStandards files across multiple repositories using * the Enterprise library for robust, audited operations. */ -class BulkSync extends CLIApp +class BulkSync extends CliFramework { /** * Default organization for bulk sync operations @@ -65,6 +65,7 @@ class BulkSync extends CLIApp private RepositorySynchronizer $synchronizer; private AuditLogger $logger; private CheckpointManager $checkpoints; + private MetricsCollector $metrics; private SecurityValidator $security; private PluginFactory $pluginFactory; private ProjectTypeDetector $typeDetector; @@ -76,21 +77,20 @@ class BulkSync extends CLIApp /** * Setup command-line arguments */ - protected function setupArguments(): array + protected function configure(): void { - return [ - 'org:' => 'GitHub organization (default: MokoConsulting)', - 'repos:' => 'Specific repositories to sync (space-separated)', - 'exclude:' => 'Repositories to exclude (space-separated)', - 'skip-archived' => 'Skip archived repositories', - 'yes' => 'Auto-confirm prompts', - 'resume' => 'Resume from last checkpoint, skipping already-processed repositories', - 'force' => 'Force overwrite of protected files (always_overwrite=false), except truly protected files', - 'protect' => 'Apply/enforce main branch protection rules on all synced repositories', - 'no-issue' => 'Skip creating a tracking issue in each target repository', - 'update-branches' => 'After sync, merge main into all other open PR branches in each repo', - 'health' => 'Run repo health checks after sync and include results in the report', - ]; + $this->setDescription('Bulk repository synchronization'); + $this->addArgument('--org', 'Organization', self::DEFAULT_ORG); + $this->addArgument('--repos', 'Specific repos', ''); + $this->addArgument('--exclude', 'Repos to exclude', ''); + $this->addArgument('--skip-archived', 'Skip archived repos', false); + $this->addArgument('--yes', 'Auto-confirm', false); + $this->addArgument('--resume', 'Resume from checkpoint', false); + $this->addArgument('--force', 'Force overwrite', false); + $this->addArgument('--protect', 'Apply branch protection', false); + $this->addArgument('--no-issue', 'Skip tracking issue', false); + $this->addArgument('--update-branches', 'Merge main into branches', false); + $this->addArgument('--health', 'Run health checks', false); } /** @@ -106,13 +106,13 @@ class BulkSync extends CLIApp } // Get configuration - $org = $this->getOption('org', self::DEFAULT_ORG); - $skipArchived = $this->hasOption('skip-archived'); - $autoConfirm = $this->hasOption('yes'); + $org = $this->getArgument('--org', self::DEFAULT_ORG); + $skipArchived = $this->getArgument('--skip-archived', false); + $autoConfirm = $this->getArgument('--yes', false); // Get repository filters - $specificRepos = $this->parseRepositoryList($this->getOption('repos', '')); - $excludeRepos = $this->parseRepositoryList($this->getOption('exclude', '')); + $specificRepos = $this->parseRepositoryList($this->getArgument('--repos', '')); + $excludeRepos = $this->parseRepositoryList($this->getArgument('--exclude', '')); $this->log("Organization: {$org}", 'INFO'); if (!empty($specificRepos)) { @@ -139,7 +139,7 @@ class BulkSync extends CLIApp // Load resume checkpoint if --resume is set $alreadyProcessed = []; - if ($this->hasOption('resume')) { + if ($this->getArgument('--resume', false)) { $checkpoint = $this->checkpoints->loadCheckpoint('bulk_sync'); if ($checkpoint !== null) { $alreadyProcessed = array_keys($checkpoint['results']['repositories'] ?? []); @@ -1424,10 +1424,6 @@ class BulkSync extends CLIApp // Execute if run directly if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) { - $app = new BulkSync( - 'bulk-sync', - 'Enterprise-grade bulk repository synchronization', - BulkSync::VERSION - ); + $app = new BulkSync(); exit($app->execute()); } diff --git a/automation/push_files.php b/automation/push_files.php index 36c7fce..a97bc46 100644 --- a/automation/push_files.php +++ b/automation/push_files.php @@ -24,7 +24,7 @@ require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\{ ApiClient, AuditLogger, - CLIApp, + CliFramework, Config, DefinitionParser, GitPlatformAdapter, @@ -51,7 +51,7 @@ use MokoEnterprise\{ * php push_files.php --files=".github/workflows/ci.yml,.github/workflows/codeql-analysis.yml" --repos=MokoCRM,WaasComponent * php push_files.php --files=templates/foo.txt:docs/foo.txt --repos=MyRepo --direct */ -class PushFiles extends CLIApp +class PushFiles extends CliFramework { public const DEFAULT_ORG = 'MokoConsulting'; public const VERSION = '04.06.00'; @@ -65,18 +65,17 @@ class PushFiles extends CLIApp /** * Setup command-line arguments */ - protected function setupArguments(): array + protected function configure(): void { - return [ - 'org:' => 'GitHub organization (default: ' . self::DEFAULT_ORG . ')', - 'repos:' => 'Target repositories — comma or space-separated (required)', - 'files:' => 'Files to push — destination paths or source:destination pairs, comma/space-separated (required)', - 'message:' => 'Custom commit message (optional)', - 'branch:' => 'Target branch for direct pushes (default: repo default branch). Ignored unless --direct is set', - 'direct' => 'Push directly to target branch instead of creating a PR', - 'yes' => 'Auto-confirm without prompting', - 'no-issue' => 'Skip creating a tracking issue in each target repository', - ]; + $this->setDescription('Push files to remote repositories'); + $this->addArgument('--org', 'GitHub organization', self::DEFAULT_ORG); + $this->addArgument('--repos', 'Target repos (comma-separated)', ''); + $this->addArgument('--files', 'Files to push (comma-separated)', ''); + $this->addArgument('--message', 'Custom commit message', ''); + $this->addArgument('--branch', 'Target branch for direct pushes', ''); + $this->addArgument('--direct', 'Push directly instead of PR', false); + $this->addArgument('--yes', 'Auto-confirm without prompting', false); + $this->addArgument('--no-issue', 'Skip creating tracking issue', false); } /** @@ -90,11 +89,11 @@ class PushFiles extends CLIApp return 1; } - $org = $this->getOption('org', self::DEFAULT_ORG); - $reposArg = $this->getOption('repos', ''); - $filesArg = $this->getOption('files', ''); - $direct = $this->hasOption('direct'); - $autoYes = $this->hasOption('yes'); + $org = $this->getArgument('--org', self::DEFAULT_ORG); + $reposArg = $this->getArgument('--repos', ''); + $filesArg = $this->getArgument('--files', ''); + $direct = $this->getArgument('--direct', false); + $autoYes = $this->getArgument('--yes', false); // Validate required arguments if (empty($reposArg)) { @@ -127,7 +126,7 @@ class PushFiles extends CLIApp } // Confirm before proceeding - if (!$autoYes && !$this->confirm($repoFileMaps, $direct)) { + if (!$autoYes && !$this->confirmPush($repoFileMaps, $direct)) { $this->log('❌ Cancelled.', 'INFO'); return 0; } @@ -265,7 +264,8 @@ class PushFiles extends CLIApp // Fall back to live detection try { $repoData = $this->api->get("/repos/{$org}/{$repo}"); - return $this->typeDetector->detect($repoData, $org, $repo); + $result = $this->typeDetector->detect('.'); + return $result['type'] ?? 'default'; } catch (\Exception $e) { $this->log(" ⚠️ Could not detect platform for {$repo}, using 'default'", 'WARN'); return 'default'; @@ -277,7 +277,7 @@ class PushFiles extends CLIApp * * @param array> $repoFileMaps */ - private function confirm(array $repoFileMaps, bool $direct): bool + private function confirmPush(array $repoFileMaps, bool $direct): bool { if ($this->quiet) { return true; @@ -322,8 +322,8 @@ class PushFiles extends CLIApp 'repos' => [], ]; - $customMessage = $this->getOption('message', ''); - $targetBranch = $this->getOption('branch', ''); + $customMessage = $this->getArgument('--message', ''); + $targetBranch = $this->getArgument('--branch', ''); foreach ($repoFileMaps as $repo => $entries) { $this->log("\n[{$repo}] Pushing " . count($entries) . ' file(s)...', 'INFO'); @@ -520,6 +520,7 @@ class PushFiles extends CLIApp 'direction' => 'desc', ]); + $existing = array_values($existing); if (!empty($existing) && isset($existing[0]['number'])) { $num = $existing[0]['number']; $patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']]; @@ -581,7 +582,7 @@ class PushFiles extends CLIApp )); $repoList = implode("\n", array_map(fn($r) => "- `{$r}`", $failedRepos)); - $fileArgs = $this->getOption('files', ''); + $fileArgs = $this->getArgument('--files', ''); $title = "fix: push_files failed for {$failed} repo(s) — action required"; @@ -622,6 +623,7 @@ class PushFiles extends CLIApp 'direction' => 'desc', ]); + $existing = array_values($existing); if (!empty($existing) && isset($existing[0]['number'])) { $num = $existing[0]['number']; $patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']]; @@ -693,10 +695,6 @@ class PushFiles extends CLIApp // Execute if run directly if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) { - $app = new PushFiles( - 'push-files', - 'Push one or more specific files to one or more remote repositories', - PushFiles::VERSION - ); + $app = new PushFiles(); exit($app->execute()); } diff --git a/automation/repo_cleanup.php b/automation/repo_cleanup.php index c5b967b..be8c596 100644 --- a/automation/repo_cleanup.php +++ b/automation/repo_cleanup.php @@ -21,7 +21,7 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; -use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory}; +use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory}; /** * Enterprise Repository Cleanup @@ -36,7 +36,7 @@ use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, GitPlatformAdapter, * 7. Verify and provision standard labels * 8. Version drift detection */ -class RepoCleanup extends CLIApp +class RepoCleanup extends CliFramework { private const VERSION = '04.06.00'; private const SYNC_PREFIX = 'chore/sync-mokostandards-'; @@ -60,40 +60,34 @@ class RepoCleanup extends CLIApp private GitPlatformAdapter $adapter; private AuditLogger $logger; private MetricsCollector $metrics; - private bool $dryRun = false; + protected bool $dryRun = false; private float $startTime; protected function configure(): void { - $this->setName('repo-cleanup'); - $this->setDescription('Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs'); - $this->setVersion(self::VERSION); - - $this->addOption('org', 'GitHub organization', 'MokoConsulting'); - $this->addOption('repos', 'Specific repositories (space-separated)', ''); - $this->addOption('skip-archived', 'Skip archived repositories', false); - $this->addOption('close-issues', 'Close resolved tracking issues (merged PR = done)', false); - $this->addOption('lock-old-issues', 'Lock issues closed >30 days', false); - $this->addOption('clean-workflows', 'Delete cancelled/stale workflow runs', false); - $this->addOption('clean-logs', 'Delete workflow run logs older than --log-days', false); - $this->addOption('log-days', 'Days to keep logs (default: 30)', '30'); - $this->addOption('delete-retired', 'Delete retired workflow files from repos', false); - $this->addOption('check-labels', 'Verify mokostandards label exists', false); - $this->addOption('check-drift', 'Check for version drift against README.md', false); - $this->addOption('all', 'Run all cleanup operations', false); - $this->addOption('yes', 'Auto-confirm prompts', false); - $this->addOption('dry-run', 'Preview changes without making them', false); - $this->addOption('verbose', 'Show detailed output', false); - $this->addOption('quiet', 'Suppress non-error output', false); - $this->addOption('json', 'Output results as JSON', false); + $this->setDescription('Enterprise repository cleanup'); + $this->addArgument('--org', 'GitHub organization', 'MokoConsulting'); + $this->addArgument('--repos', 'Specific repos (space-separated)', ''); + $this->addArgument('--skip-archived', 'Skip archived repos', false); + $this->addArgument('--close-issues', 'Close resolved tracking issues', false); + $this->addArgument('--lock-old-issues', 'Lock issues closed >30 days', false); + $this->addArgument('--clean-workflows', 'Delete stale workflow runs', false); + $this->addArgument('--clean-logs', 'Delete old workflow logs', false); + $this->addArgument('--log-days', 'Days to keep logs', '30'); + $this->addArgument('--delete-retired', 'Delete retired workflows', false); + $this->addArgument('--check-labels', 'Verify labels exist', false); + $this->addArgument('--check-drift', 'Check version drift', false); + $this->addArgument('--all', 'Run all operations', false); + $this->addArgument('--yes', 'Auto-confirm', false); + $this->addArgument('--json', 'Output as JSON', false); } - protected function execute(): int + protected function run(): int { $this->startTime = microtime(true); - $org = $this->getOption('org', 'MokoConsulting'); - $this->dryRun = (bool) $this->getOption('dry-run', false); - $runAll = (bool) $this->getOption('all', false); + $org = $this->getArgument('--org', 'MokoConsulting'); + $this->dryRun = (bool) $this->getArgument('--dry-run', false); + $runAll = (bool) $this->getArgument('--all', false); $config = Config::load(); @@ -101,24 +95,24 @@ class RepoCleanup extends CLIApp $this->adapter = PlatformAdapterFactory::create($config); $this->api = $this->adapter->getApiClient(); } catch (\Exception $e) { - $this->error('Failed to initialize platform adapter: ' . $e->getMessage()); + $this->errorMsg('Failed to initialize platform adapter: ' . $e->getMessage()); return 1; } $this->logger = new AuditLogger('repo_cleanup'); $this->metrics = new MetricsCollector('repo_cleanup'); - $this->log("🧹 MokoStandards Repository Cleanup v" . self::VERSION); - $this->log("Organization: {$org}"); - $this->log("Current sync branch: " . self::CURRENT_BRANCH); + $this->logMsg("🧹 MokoStandards Repository Cleanup v" . self::VERSION); + $this->logMsg("Organization: {$org}"); + $this->logMsg("Current sync branch: " . self::CURRENT_BRANCH); if ($this->dryRun) { - $this->log("⚠️ DRY RUN — no changes will be made"); + $this->logMsg("⚠️ DRY RUN — no changes will be made"); } - $this->log(''); + $this->logMsg(''); $repos = $this->fetchRepositories($org); - $this->log("Found " . count($repos) . " repositories"); - $this->log(''); + $this->logMsg("Found " . count($repos) . " repositories"); + $this->logMsg(''); $results = [ 'repos_processed' => 0, @@ -140,7 +134,7 @@ class RepoCleanup extends CLIApp $name = $repo['name']; $num = $i + 1; $total = count($repos); - $this->log("[{$num}/{$total}] {$name}"); + $this->logMsg("[{$num}/{$total}] {$name}"); $results['repos_processed']++; try { @@ -151,37 +145,37 @@ class RepoCleanup extends CLIApp $cleaned = $this->cleanBranches($org, $name, $results) || $cleaned; // Optional: close resolved issues - if ($runAll || $this->getOption('close-issues', false)) { + if ($runAll || $this->getArgument('--close-issues', false)) { $cleaned = $this->closeResolvedIssues($org, $name, $results) || $cleaned; } // Optional: lock old closed issues - if ($runAll || $this->getOption('lock-old-issues', false)) { + if ($runAll || $this->getArgument('--lock-old-issues', false)) { $cleaned = $this->lockOldIssues($org, $name, $results) || $cleaned; } // Optional: delete retired workflow files - if ($runAll || $this->getOption('delete-retired', false)) { + if ($runAll || $this->getArgument('--delete-retired', false)) { $cleaned = $this->deleteRetiredWorkflows($org, $name, $results) || $cleaned; } // Optional: clean workflow runs - if ($runAll || $this->getOption('clean-workflows', false)) { + if ($runAll || $this->getArgument('--clean-workflows', false)) { $cleaned = $this->cleanWorkflowRuns($org, $name, $results) || $cleaned; } // Optional: clean old logs - if ($runAll || $this->getOption('clean-logs', false)) { + if ($runAll || $this->getArgument('--clean-logs', false)) { $cleaned = $this->cleanOldLogs($org, $name, $results) || $cleaned; } // Optional: check labels - if ($runAll || $this->getOption('check-labels', false)) { + if ($runAll || $this->getArgument('--check-labels', false)) { $this->checkLabels($org, $name, $results); } // Optional: check version drift - if ($runAll || $this->getOption('check-drift', false)) { + if ($runAll || $this->getArgument('--check-drift', false)) { $this->checkVersionDrift($org, $name, $results); } @@ -189,32 +183,32 @@ class RepoCleanup extends CLIApp $results['repos_cleaned']++; } } catch (\Exception $e) { - $this->error(" ✗ {$name}: " . $e->getMessage()); + $this->errorMsg(" ✗ {$name}: " . $e->getMessage()); $results['errors']++; } } $duration = round(microtime(true) - $this->startTime, 1); - $this->log(''); - $this->log('============================================================'); - $this->log("🧹 Cleanup Complete ({$duration}s)"); - $this->log('============================================================'); - $this->log("Repos processed: {$results['repos_processed']}"); - $this->log("Repos with changes: {$results['repos_cleaned']}"); - $this->log("Branches deleted: {$results['branches_deleted']}"); - $this->log("PRs closed: {$results['prs_closed']}"); - $this->log("Issues closed: {$results['issues_closed']}"); - $this->log("Issues locked: {$results['issues_locked']}"); - $this->log("Retired files: {$results['retired_files']}"); - $this->log("Workflow runs: {$results['runs_deleted']}"); - $this->log("Logs cleaned: {$results['logs_deleted']}"); - $this->log("Labels missing: {$results['labels_missing']}"); - $this->log("Version drift: {$results['version_drift']}"); - $this->log("Errors: {$results['errors']}"); - $this->log('============================================================'); + $this->logMsg(''); + $this->logMsg('============================================================'); + $this->logMsg("🧹 Cleanup Complete ({$duration}s)"); + $this->logMsg('============================================================'); + $this->logMsg("Repos processed: {$results['repos_processed']}"); + $this->logMsg("Repos with changes: {$results['repos_cleaned']}"); + $this->logMsg("Branches deleted: {$results['branches_deleted']}"); + $this->logMsg("PRs closed: {$results['prs_closed']}"); + $this->logMsg("Issues closed: {$results['issues_closed']}"); + $this->logMsg("Issues locked: {$results['issues_locked']}"); + $this->logMsg("Retired files: {$results['retired_files']}"); + $this->logMsg("Workflow runs: {$results['runs_deleted']}"); + $this->logMsg("Logs cleaned: {$results['logs_deleted']}"); + $this->logMsg("Labels missing: {$results['labels_missing']}"); + $this->logMsg("Version drift: {$results['version_drift']}"); + $this->logMsg("Errors: {$results['errors']}"); + $this->logMsg('============================================================'); - if ($this->getOption('json', false)) { + if ($this->getArgument('--json', false)) { $results['duration_seconds'] = $duration; echo json_encode($results, JSON_PRETTY_PRINT) . "\n"; } @@ -226,8 +220,8 @@ class RepoCleanup extends CLIApp private function fetchRepositories(string $org): array { - $specificRepos = trim((string) $this->getOption('repos', '')); - $skipArchived = (bool) $this->getOption('skip-archived', false); + $specificRepos = trim((string) $this->getArgument('--repos', '')); + $skipArchived = (bool) $this->getArgument('--skip-archived', false); if (!empty($specificRepos)) { $names = preg_split('/[\s,]+/', $specificRepos); @@ -264,7 +258,7 @@ class RepoCleanup extends CLIApp if (($pr['number'] ?? 0) > 0 && !$this->dryRun) { $this->api->patch("/repos/{$org}/{$repo}/pulls/{$pr['number']}", ['state' => 'closed']); } - $this->log(" 🔒 Closed PR #{$pr['number']} ({$name})"); + $this->logMsg(" 🔒 Closed PR #{$pr['number']} ({$name})"); $results['prs_closed']++; $changed = true; } @@ -279,7 +273,7 @@ class RepoCleanup extends CLIApp continue; } } - $this->log(" 🗑️ Deleted branch: {$name}"); + $this->logMsg(" 🗑️ Deleted branch: {$name}"); $results['branches_deleted']++; $changed = true; } @@ -312,7 +306,7 @@ class RepoCleanup extends CLIApp 'state' => 'closed', 'state_reason' => 'completed', ]); } - $this->log(" ✅ Closed issue #{$num} (PR #{$prNum} merged)"); + $this->logMsg(" ✅ Closed issue #{$num} (PR #{$prNum} merged)"); $results['issues_closed']++; $changed = true; } @@ -361,7 +355,7 @@ class RepoCleanup extends CLIApp } if ($results['issues_locked'] > 0) { - $this->log(" 🔒 Locked {$results['issues_locked']} old closed issue(s)"); + $this->logMsg(" 🔒 Locked {$results['issues_locked']} old closed issue(s)"); } return $changed; } @@ -396,7 +390,7 @@ class RepoCleanup extends CLIApp 'branch' => $defaultBranch, ]); } - $this->log(" Deleted retired: {$wf} (from {$wfDir})"); + $this->logMsg(" Deleted retired: {$wf} (from {$wfDir})"); $results['retired_files']++; $changed = true; } catch (\Exception $e) { @@ -433,7 +427,7 @@ class RepoCleanup extends CLIApp } } if ($results['runs_deleted'] > 0) { - $this->log(" 🔄 Cleaned {$results['runs_deleted']} workflow run(s)"); + $this->logMsg(" 🔄 Cleaned {$results['runs_deleted']} workflow run(s)"); } return $changed; } @@ -441,7 +435,7 @@ class RepoCleanup extends CLIApp private function cleanOldLogs(string $org, string $repo, array &$results): bool { $changed = false; - $days = (int) $this->getOption('log-days', '30'); + $days = (int) $this->getArgument('--log-days', '30'); $cutoff = date('Y-m-d\TH:i:s\Z', strtotime("-{$days} days")); try { @@ -465,7 +459,7 @@ class RepoCleanup extends CLIApp } if ($results['logs_deleted'] > 0) { - $this->log(" 📋 Cleaned {$results['logs_deleted']} old log(s)"); + $this->logMsg(" 📋 Cleaned {$results['logs_deleted']} old log(s)"); } return $changed; } @@ -475,7 +469,7 @@ class RepoCleanup extends CLIApp try { $this->api->get("/repos/{$org}/{$repo}/labels/mokostandards"); } catch (\Exception $e) { - $this->log(" ⚠️ Missing 'mokostandards' label"); + $this->logMsg(" ⚠️ Missing 'mokostandards' label"); $results['labels_missing']++; $this->api->resetCircuitBreaker(); } @@ -495,7 +489,7 @@ class RepoCleanup extends CLIApp $mokoContent = base64_decode($mokoFile['content'] ?? ''); if (preg_match('/standards_version:\s*(\d{2}\.\d{2}\.\d{2})/m', $mokoContent, $vm)) { if ($vm[1] !== self::VERSION) { - $this->log(" ⚠️ Standards drift: {$vm[1]} (expected " . self::VERSION . ")"); + $this->logMsg(" ⚠️ Standards drift: {$vm[1]} (expected " . self::VERSION . ")"); $results['version_drift']++; } } @@ -510,14 +504,14 @@ class RepoCleanup extends CLIApp // ─── Helpers ───────────────────────────────────────────────────────── - private function log(string $message): void + private function logMsg(string $message): void { - if (!$this->getOption('quiet', false)) { + if (!$this->quiet) { echo $message . "\n"; } } - private function error(string $message): void + private function errorMsg(string $message): void { fwrite(STDERR, $message . "\n"); } diff --git a/cli/joomla_release.php b/cli/joomla_release.php index 7429bf8..88e8cd0 100644 --- a/cli/joomla_release.php +++ b/cli/joomla_release.php @@ -24,9 +24,9 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; -use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, PlatformAdapterFactory}; +use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory}; -class JoomlaRelease extends CLIApp +class JoomlaRelease extends CliFramework { private const VERSION = '04.06.00'; private const ORG = 'mokoconsulting-tech'; @@ -49,6 +49,7 @@ class JoomlaRelease extends CLIApp private ApiClient $api; private AuditLogger $logger; + private \MokoEnterprise\GitPlatformAdapter $adapter; protected function configure(): void { @@ -498,5 +499,5 @@ class JoomlaRelease extends CLIApp } } -$script = new JoomlaRelease('joomla_release', 'Joomla release pipeline'); -exit($script->execute()); +$app = new JoomlaRelease(); +exit($app->execute()); diff --git a/lib/Enterprise/ApiClient.php b/lib/Enterprise/ApiClient.php index f915af0..5d29a70 100644 --- a/lib/Enterprise/ApiClient.php +++ b/lib/Enterprise/ApiClient.php @@ -261,9 +261,9 @@ class ApiClient * @throws RateLimitExceeded * @throws CircuitBreakerOpen */ - public function delete(string $endpoint): array + public function delete(string $endpoint, ?array $body = null): array { - return $this->request('DELETE', $endpoint); + return $this->request('DELETE', $endpoint, $body); } /** diff --git a/phpstan.neon b/phpstan.neon index 2d0e87b..b5dcc07 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,10 +16,6 @@ parameters: analyseAndScan: - vendor - node_modules (?) - # Legacy CLIApp scripts — need migration to CliFramework - - automation/repo_cleanup.php - - automation/push_files.php - - cli/joomla_release.php reportUnmatchedIgnoredErrors: false diff --git a/validate/auto_detect_platform.php b/validate/auto_detect_platform.php index c4873a4..853fba1 100755 --- a/validate/auto_detect_platform.php +++ b/validate/auto_detect_platform.php @@ -22,7 +22,7 @@ require_once __DIR__ . '/../../vendor/autoload.php'; require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\{ - CLIApp, + CliFramework, ProjectTypeDetector, PluginFactory, PluginRegistry, @@ -36,7 +36,7 @@ use MokoEnterprise\{ * Detects whether a repository is a Joomla/WaaS component, Dolibarr/CRM module, * or generic repository, then validates against appropriate schema */ -class AutoDetectPlatform extends CLIApp +class AutoDetectPlatform extends CliFramework { private const DETECTION_THRESHOLD = 0.5; // 50% confidence required @@ -62,20 +62,19 @@ class AutoDetectPlatform extends CLIApp private string $schemaFile = ''; private ?object $detectedPlugin = null; - protected function setupArguments(): array + protected function configure(): void { - return [ - 'repo-path:' => 'Path to repository to analyze (default: current directory)', - 'schema-dir:' => 'Path to schema definitions directory (default: definitions/default)', - 'output-dir:' => 'Directory for output reports (default: var/logs/validation)', - ]; + $this->setDescription('Automatically detect platform type and validate repository'); + $this->addArgument('--repo-path', 'Path to repository to analyze', '.'); + $this->addArgument('--schema-dir', 'Path to schema definitions directory', 'definitions/default'); + $this->addArgument('--output-dir', 'Directory for output reports', 'var/logs/validation'); } protected function run(): int { - $repoPath = $this->getOption('repo-path', '.'); - $schemaDir = $this->getOption('schema-dir', 'definitions/default'); - $outputDir = $this->getOption('output-dir', 'var/logs/validation'); + $repoPath = $this->getArgument('--repo-path', '.'); + $schemaDir = $this->getArgument('--schema-dir', 'definitions/default'); + $outputDir = $this->getArgument('--output-dir', 'var/logs/validation'); // Make paths absolute $repoPath = $this->getAbsolutePath($repoPath); @@ -151,7 +150,7 @@ class AutoDetectPlatform extends CLIApp } // Output results - if ($this->jsonOutput) { + if ($this->getArgument("--json", false)) { $this->outputJson(); } else { $this->displayResults(); @@ -953,5 +952,5 @@ class AutoDetectPlatform extends CLIApp } // Run the application -$app = new AutoDetectPlatform('auto_detect_platform', 'Automatically detect platform type and validate repository'); +$app = new AutoDetectPlatform(); exit($app->execute()); diff --git a/validate/check_wiki_health.php b/validate/check_wiki_health.php index 3251ff3..ad7cd41 100644 --- a/validate/check_wiki_health.php +++ b/validate/check_wiki_health.php @@ -17,29 +17,24 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; -use MokoEnterprise\CLIApp; +use MokoEnterprise\CliFramework; -class CheckWikiHealth extends CLIApp +class CheckWikiHealth extends CliFramework { - public function __construct() + protected function configure(): void { - parent::__construct('check-wiki-health', 'Validate wiki health for a repository', '01.00.00'); - } - - protected function setupArguments(): array - { - return [ - 'path:' => 'Repository path (default: current directory)', - 'gitea-url:' => 'Gitea base URL (default: https://git.mokoconsulting.tech)', - 'token:' => 'Gitea API token (or set GITEA_TOKEN env var)', - ]; + $this->setDescription('Validate wiki health for a repository'); + $this->addArgument('--path', 'Repository path (default: current directory)', '.'); + $this->addArgument('--gitea-url', 'Gitea base URL', 'https://git.mokoconsulting.tech'); + $this->addArgument('--token', 'Gitea API token (or set GITEA_TOKEN env var)', ''); + $this->addArgument('--json', 'Output as JSON', false); } protected function run(): int { - $repoPath = realpath($this->getOption('path', '.')) ?: '.'; - $giteaUrl = $this->getOption('gitea-url', 'https://git.mokoconsulting.tech'); - $token = $this->getOption('token', getenv('GITEA_TOKEN') ?: ''); + $repoPath = realpath($this->getArgument('--path', '.')) ?: '.'; + $giteaUrl = $this->getArgument('--gitea-url', 'https://git.mokoconsulting.tech'); + $token = $this->getArgument('--token', getenv('GITEA_TOKEN') ?: ''); // Detect repo owner/name from git config $configFile = $repoPath . '/.git/config'; @@ -76,7 +71,7 @@ class CheckWikiHealth extends CLIApp if ($pages === null) { $this->log(' No wiki found or API error', 'WARNING'); $issues++; - if ($this->jsonOutput) { + if ($this->getArgument("--json", false)) { echo json_encode(['status' => 'no_wiki', 'issues' => $issues]); } return 0; @@ -118,7 +113,7 @@ class CheckWikiHealth extends CLIApp } } - if ($this->jsonOutput) { + if ($this->getArgument("--json", false)) { echo json_encode([ 'repo' => "{$owner}/{$repo}", 'pages' => $pageCount, @@ -153,4 +148,5 @@ class CheckWikiHealth extends CLIApp } } -(new CheckWikiHealth())->execute(); +$app = new CheckWikiHealth(); +exit($app->execute());