Developer Hub

Blade

A premium Developer Portfolio, Repository Catalog, and Markdown Documentation Explorer built with Laravel. Features automated GitHub sync, a developer blog platform, and secure user access with 2FA.

Stars
2
Forks
0
Downloads
11
Open Issues
0
Files main

Repository Files

Loading file structure...
app/Console/Commands/SyncGithubRepos.php
<?php

namespace App\Console\Commands;

use App\Models\Project;
use App\Models\ProjectPage;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class SyncGithubRepos extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'github:sync {--force : Force sync all repos even if API limit is low}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Sync GitHub repositories, READMEs, and metadata for ghostcompiler profile';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $username = 'ghostcompiler';
        $this->info("Starting GitHub sync for profile: {$username}...");

        $token = env('GITHUB_TOKEN');
        if (empty($token) || $token === 'github_pat_antigravitydummytoken') {
            $token = \App\Models\Setting::get('github_token');
        }
        if (empty($token) || $token === 'github_pat_antigravitydummytoken') {
            $token = null;
        }

        if (!empty($token)) {
            $this->info("Using GitHub Personal Access Token for authentication.");
            $response = Http::withToken($token)->withHeaders([
                'User-Agent' => 'Laravel-Github-Sync-Client',
            ])->get("https://api.github.com/users/{$username}/repos?per_page=100&sort=pushed");
            
            if ($response->status() === 401) {
                $this->warn("Provided GITHUB_TOKEN returned 401 (Bad Credentials). Falling back to unauthenticated request...");
                $response = Http::withHeaders([
                    'User-Agent' => 'Laravel-Github-Sync-Client',
                ])->get("https://api.github.com/users/{$username}/repos?per_page=100&sort=pushed");
                $token = null;
            }
        } else {
            $this->warn("No GITHUB_TOKEN found in environment. Rate limit will be capped at 60 requests/hr.");
            $response = Http::withHeaders([
                'User-Agent' => 'Laravel-Github-Sync-Client',
            ])->get("https://api.github.com/users/{$username}/repos?per_page=100&sort=pushed");
        }

        $repos = [];
        if ($response->successful()) {
            $repos = $response->json();
            if (!is_array($repos)) {
                $this->error("Unexpected response format from GitHub API.");
                return Command::FAILURE;
            }
        } else {
            $this->warn("Failed to fetch repository list from GitHub API: " . $response->body());
            $this->warn("Falling back to local database entries for repair sync...");
            
            // Build list of repos from projects already in DB
            $dbProjects = Project::all();
            foreach ($dbProjects as $proj) {
                $repos[] = [
                    'name' => $proj->name,
                    'description' => $proj->description,
                    'stargazers_count' => $proj->stars_count,
                    'forks_count' => $proj->forks_count,
                    'open_issues_count' => $proj->open_issues_count,
                    'language' => $proj->language,
                    'license' => ['spdx_id' => $proj->license_name],
                    'html_url' => $proj->github_url,
                    'homepage' => $proj->homepage_url,
                    'default_branch' => $proj->default_branch,
                    'topics' => $proj->topics,
                    'fork' => false,
                ];
            }
        }

        $this->info("Processing " . count($repos) . " repositories...");
        
        $syncedProjectNames = [];
        $count = 0;
        foreach ($repos as $repoData) {
            $name = $repoData['name'];
            
            // Skip the profile README repository
            if (Str::lower($name) === 'ghostcompiler') {
                $this->info("Skipping profile repository: {$name}");
                continue;
            }

            // Skip forks by default
            if ($repoData['fork']) {
                $this->info("Skipping fork repository: {$name}");
                continue;
            }

            $this->line("Processing repository: <info>{$name}</info>");

            // Fresh request instance for README
            $readmeRequest = Http::withHeaders([
                'User-Agent' => 'Laravel-Github-Sync-Client',
                'Accept' => 'application/vnd.github.html',
            ]);
            if (!empty($token)) {
                $readmeRequest = $readmeRequest->withToken($token);
            }

            $readmeUrl = "https://api.github.com/repos/{$username}/{$name}/readme";
            $readmeResponse = $readmeRequest->get($readmeUrl);

            $readmeHtml = null;
            if ($readmeResponse->successful()) {
                $readmeHtml = $readmeResponse->body();
            } else {
                $this->warn("  - GitHub API README fetch failed (Status " . $readmeResponse->status() . "). Trying raw Markdown fallback...");
                
                // Fetch raw Markdown from raw.githubusercontent.com
                $defaultBranch = $repoData['default_branch'] ?? 'main';
                $rawUrl = "https://raw.githubusercontent.com/{$username}/{$name}/{$defaultBranch}/README.md";
                $rawResponse = Http::get($rawUrl);
                
                if ($rawResponse->failed()) {
                    // Try master branch if main failed
                    $rawUrl = "https://raw.githubusercontent.com/{$username}/{$name}/master/README.md";
                    $rawResponse = Http::get($rawUrl);
                }
                
                if ($rawResponse->successful()) {
                    $rawMarkdown = $rawResponse->body();
                    try {
                        $converter = new \League\CommonMark\CommonMarkConverter([
                            'html_input'         => 'strip',  // Strip raw HTML to prevent stored XSS
                            'allow_unsafe_links' => false,
                        ]);
                        $readmeHtml = $converter->convert($rawMarkdown)->getContent();
                        $this->info("  - [SUCCESS] Locally converted raw README to HTML.");
                    } catch (\Exception $e) {
                        $this->error("  - [ERROR] Failed to parse Markdown: " . $e->getMessage());
                    }
                } else {
                    $this->warn("  - Raw Markdown fetch failed.");
                }
            }

            // Fresh request instance for releases
            $releaseRequest = Http::withHeaders([
                'User-Agent' => 'Laravel-Github-Sync-Client',
            ]);
            if (!empty($token)) {
                $releaseRequest = $releaseRequest->withToken($token);
            }

            $releaseUrl = "https://api.github.com/repos/{$username}/{$name}/releases/latest";
            $releaseResponse = $releaseRequest->get($releaseUrl);
            
            $releaseInfo = null;
            if ($releaseResponse->successful()) {
                $releaseData = $releaseResponse->json();
                $releaseInfo = [
                    'tag_name' => $releaseData['tag_name'] ?? null,
                    'name' => $releaseData['name'] ?? null,
                    'published_at' => $releaseData['published_at'] ?? null,
                    'zipball_url' => $releaseData['zipball_url'] ?? null,
                    'tarball_url' => $releaseData['tarball_url'] ?? null,
                    'html_url' => $releaseData['html_url'] ?? null,
                    'body' => $releaseData['body'] ?? null,
                ];
                $this->line("  - Found release: <comment>{$releaseInfo['tag_name']}</comment>");
            }

            // 4. Fetch Packagist / NPM Downloads Count
            $downloadsCount = 0;
            if ($name === 'bhidu-language') {
                $npmUrl = "https://api.npmjs.org/downloads/point/last-month/bhidu-lang";
                $npmResponse = Http::get($npmUrl);
                if ($npmResponse->successful()) {
                    $downloadsCount = $npmResponse->json()['downloads'] ?? 0;
                }
            } else {
                // Try Packagist first
                $packagistUrl = "https://packagist.org/packages/ghostcompiler/{$name}.json";
                $packagistResponse = Http::get($packagistUrl);
                if ($packagistResponse->successful()) {
                    $downloadsCount = $packagistResponse->json()['package']['downloads']['total'] ?? 0;
                } else {
                    // Try NPM as fallback for JS/TS packages
                    $npmUrl = "https://api.npmjs.org/downloads/point/last-month/{$name}";
                    $npmResponse = Http::get($npmUrl);
                    if ($npmResponse->successful()) {
                        $downloadsCount = $npmResponse->json()['downloads'] ?? 0;
                    }
                }
            }
            $this->line("  - Downloads count: <comment>{$downloadsCount}</comment>");

            // 5. Update or Create in DB with cache protection
            $project = Project::where('name', $name)->first();
            
            $updateData = [
                'slug' => Str::slug($name),
                'description' => $repoData['description'],
                'stars_count' => $repoData['stargazers_count'] ?? 0,
                'forks_count' => $repoData['forks_count'] ?? 0,
                'open_issues_count' => $repoData['open_issues_count'] ?? 0,
                'downloads_count' => $downloadsCount,
                'language' => $repoData['language'] ?? 'N/A',
                'license_name' => $repoData['license']['spdx_id'] ?? ($repoData['license']['name'] ?? null),
                'github_url' => $repoData['html_url'],
                'homepage_url' => $repoData['homepage'],
                'default_branch' => $repoData['default_branch'] ?? 'main',
                'topics' => $repoData['topics'] ?? [],
                'synced_at' => now(),
            ];

            if ($readmeHtml !== null) {
                $updateData['readme_html'] = $readmeHtml;
            }
            if ($releaseInfo !== null) {
                $updateData['releases_info'] = $releaseInfo;
            }

            if ($project) {
                $project->update($updateData);
            } else {
                $updateData['name'] = $name;
                $updateData['readme_html'] = $readmeHtml;
                $updateData['releases_info'] = $releaseInfo;
                $project = Project::create($updateData);
            }

            // Clear file tree cache so it updates immediately
            \Illuminate\Support\Facades\Cache::forget("project_tree_{$project->id}");

            $syncedProjectNames[] = $name;

            // --- 6. Sync Additional Documentation Pages (e.g. functions.md, developer.md) ---
            $pagesToSync = [];
            $hasApiAccess = !empty($token) || $response->successful(); 
            
            if ($hasApiAccess) {
                $contentsUrl = "https://api.github.com/repos/{$username}/{$name}/contents";
                $contentsRequest = Http::withHeaders(['User-Agent' => 'Laravel-Github-Sync-Client']);
                if (!empty($token)) {
                    $contentsRequest = $contentsRequest->withToken($token);
                }
                
                $contentsResponse = $contentsRequest->get($contentsUrl);
                if ($contentsResponse->successful()) {
                    $files = $contentsResponse->json();
                    $hasDocsDir = false;
                    
                    if (is_array($files)) {
                        foreach ($files as $file) {
                            if ($file['type'] === 'file' && str_ends_with(strtolower($file['name']), '.md')) {
                                if (strtolower($file['name']) !== 'readme.md') {
                                    $pageTitle = Str::title(str_replace('-', ' ', pathinfo($file['name'], PATHINFO_FILENAME)));
                                    $pagesToSync[] = [
                                        'title' => $pageTitle,
                                        'slug' => Str::slug(pathinfo($file['name'], PATHINFO_FILENAME)),
                                        'path' => $file['path'],
                                    ];
                                }
                            }
                            if ($file['type'] === 'dir' && strtolower($file['name']) === 'docs') {
                                $hasDocsDir = true;
                            }
                        }
                    }
                    
                    if ($hasDocsDir) {
                        $docsUrl = "https://api.github.com/repos/{$username}/{$name}/contents/docs";
                        $docsResponse = $contentsRequest->get($docsUrl);
                        if ($docsResponse->successful()) {
                            $docsFiles = $docsResponse->json();
                            if (is_array($docsFiles)) {
                                foreach ($docsFiles as $file) {
                                    if ($file['type'] === 'file' && str_ends_with(strtolower($file['name']), '.md')) {
                                        $pageTitle = Str::title(str_replace('-', ' ', pathinfo($file['name'], PATHINFO_FILENAME)));
                                        $pagesToSync[] = [
                                            'title' => $pageTitle,
                                            'slug' => Str::slug(pathinfo($file['name'], PATHINFO_FILENAME)),
                                            'path' => $file['path'],
                                        ];
                                    }
                                }
                            }
                        }
                    }
                }
            }
            
            // Fallback to common files check via raw.githubusercontent.com
            if (empty($pagesToSync)) {
                $commonPaths = [
                    'docs/functions.md' => 'Functions Reference',
                    'functions.md' => 'Functions Reference',
                    'docs/developer.md' => 'Developer Guide',
                    'developer.md' => 'Developer Guide',
                    'docs/developer-guide.md' => 'Developer Guide',
                    'developer-guide.md' => 'Developer Guide',
                    'docs/usage.md' => 'Usage Guide',
                    'usage.md' => 'Usage Guide',
                ];
                
                foreach ($commonPaths as $path => $title) {
                    $defaultBranch = $repoData['default_branch'] ?? 'main';
                    $rawUrl = "https://raw.githubusercontent.com/{$username}/{$name}/{$defaultBranch}/{$path}";
                    
                    $rawResponse = Http::get($rawUrl);
                    if ($rawResponse->failed() && ($defaultBranch === 'main' || $defaultBranch === 'master')) {
                        $altBranch = $defaultBranch === 'main' ? 'master' : 'main';
                        $rawUrl = "https://raw.githubusercontent.com/{$username}/{$name}/{$altBranch}/{$path}";
                        $rawResponse = Http::get($rawUrl);
                    }
                    
                    if ($rawResponse->successful()) {
                        $pagesToSync[] = [
                            'title' => $title,
                            'slug' => Str::slug(pathinfo($path, PATHINFO_FILENAME)),
                            'path' => $path,
                            'raw_content' => $rawResponse->body(),
                        ];
                    }
                }
            }
            
            // Sync/Save pages in database
            $syncedPageIds = [];
            foreach ($pagesToSync as $pageInfo) {
                $pageSlug = $pageInfo['slug'];
                $pagePath = $pageInfo['path'];
                $pageTitle = $pageInfo['title'];
                $pageHtml = null;
                
                if (isset($pageInfo['raw_content'])) {
                    try {
                        $converter = new \League\CommonMark\CommonMarkConverter([
                            'html_input'         => 'strip',  // Strip raw HTML to prevent stored XSS
                            'allow_unsafe_links' => false,
                        ]);
                        $pageHtml = $converter->convert($pageInfo['raw_content'])->getContent();
                    } catch (\Exception $e) {
                        $this->error("    - Failed parsing fallback MD: " . $e->getMessage());
                    }
                } else {
                    $pageApiUrl = "https://api.github.com/repos/{$username}/{$name}/contents/{$pagePath}";
                    $pageRequest = Http::withHeaders([
                        'User-Agent' => 'Laravel-Github-Sync-Client',
                        'Accept' => 'application/vnd.github.html',
                    ]);
                    if (!empty($token)) {
                        $pageRequest = $pageRequest->withToken($token);
                    }
                    
                    $pageResponse = $pageRequest->get($pageApiUrl);
                    if ($pageResponse->successful()) {
                        $pageHtml = $pageResponse->body();
                    } else {
                        $defaultBranch = $repoData['default_branch'] ?? 'main';
                        $rawUrl = "https://raw.githubusercontent.com/{$username}/{$name}/{$defaultBranch}/{$pagePath}";
                        $rawResponse = Http::get($rawUrl);
                        if ($rawResponse->successful()) {
                            try {
                                $converter = new \League\CommonMark\CommonMarkConverter([
                                    'html_input'         => 'strip',  // Strip raw HTML to prevent stored XSS
                                    'allow_unsafe_links' => false,
                                ]);
                                $pageHtml = $converter->convert($rawResponse->body())->getContent();
                            } catch (\Exception $e) {
                                $this->error("    - Failed parsing fallback MD: " . $e->getMessage());
                            }
                        }
                    }
                }
                
                if ($pageHtml !== null) {
                    $savedPage = ProjectPage::updateOrCreate(
                        [
                            'project_id' => $project->id,
                            'slug' => $pageSlug,
                        ],
                        [
                            'title' => $pageTitle,
                            'path' => $pagePath,
                            'content_html' => $pageHtml,
                        ]
                    );
                    $syncedPageIds[] = $savedPage->id;
                    $this->line("    - Synced subpage: <info>{$pageTitle}</info> ({$pagePath})");
                }
            }
            
            // Delete deleted subpages
            ProjectPage::where('project_id', $project->id)
                ->whereNotIn('id', $syncedPageIds)
                ->delete();

            $count++;
        }

        if ($response->successful()) {
            // Delete projects in DB that are not in the list of synced repositories
            $deletedCount = Project::whereNotIn('name', $syncedProjectNames)->delete();
            if ($deletedCount > 0) {
                $this->info("Deleted {$deletedCount} obsolete/removed repositories from the system.");
            }
        }

        $this->info("Sync complete! Successfully synchronized {$count} repositories.");
        return Command::SUCCESS;
    }
}