option('org') ?? config('upstream.gitea.org', 'core'); $dryRun = $this->option('dry-run'); if (! $token) { $this->error('No Forge token configured. Set FORGE_TOKEN or GITEA_TOKEN in .env'); return self::FAILURE; } $this->info("Fetching repositories from {$baseUrl}/api/v1/orgs/{$org}/repos..."); $repos = $this->fetchAllRepos($baseUrl, $token, $org); if ($repos === null) { $this->error('Failed to fetch repositories from Forge API.'); return self::FAILURE; } $this->info(count($repos) . ' repositories found.'); $this->newLine(); $created = 0; $skipped = 0; $updated = 0; foreach ($repos as $repo) { $fullName = $repo['full_name']; $slug = Str::slug($fullName, '-'); $repoUrl = "{$baseUrl}/{$fullName}"; $existing = Vendor::withTrashed()->where('slug', $slug)->first(); if ($existing && ! $existing->trashed()) { // Update git_repo_url if it changed if ($existing->git_repo_url !== $repoUrl) { if (! $dryRun) { $existing->update(['git_repo_url' => $repoUrl]); } $this->line(' Updated ' . $fullName); $updated++; } else { $this->line(' Exists ' . $fullName); $skipped++; } continue; } if ($existing && $existing->trashed()) { if (! $dryRun) { $existing->restore(); $existing->update([ 'git_repo_url' => $repoUrl, 'is_active' => true, ]); } $this->line(' Restored ' . $fullName); $created++; continue; } // Detect language hint for platform $platform = $this->detectPlatform($repo); if (! $dryRun) { Vendor::create([ 'slug' => $slug, 'name' => $repo['name'], 'vendor_name' => $org, 'source_type' => Vendor::SOURCE_OSS, 'plugin_platform' => $platform, 'git_repo_url' => $repoUrl, 'current_version' => null, 'target_repo' => $fullName, 'target_branch' => $repo['default_branch'] ?? 'main', 'is_active' => true, 'path_mapping' => [], 'ignored_paths' => ['.git/*', 'vendor/*', 'node_modules/*'], 'priority_paths' => [], ]); } $this->line(' Created ' . $fullName . ' (' . ($platform ?? 'oss') . ')'); $created++; } $this->newLine(); $prefix = $dryRun ? '[DRY RUN] ' : ''; $this->info("{$prefix}Sync complete: {$created} created, {$updated} updated, {$skipped} unchanged."); return self::SUCCESS; } /** * Fetch all repositories from a Forge organisation, handling pagination. * * @return array>|null */ private function fetchAllRepos(string $baseUrl, string $token, string $org): ?array { $allRepos = []; $page = 1; $limit = 50; do { $response = Http::withHeaders(['Authorization' => "token {$token}"]) ->timeout(30) ->get("{$baseUrl}/api/v1/orgs/{$org}/repos", [ 'page' => $page, 'limit' => $limit, ]); if (! $response->successful()) { $this->error("Forge API error: {$response->status()}"); return null; } $repos = $response->json(); if (empty($repos)) { break; } $allRepos = array_merge($allRepos, $repos); $page++; } while (count($repos) === $limit); return $allRepos; } /** * Detect the platform type from repository metadata. */ private function detectPlatform(array $repo): ?string { $name = $repo['name'] ?? ''; if (str_starts_with($name, 'php-') || str_starts_with($name, 'core-')) { return Vendor::PLATFORM_LARAVEL; } return Vendor::PLATFORM_OTHER; } }