diff --git a/fix-ubuntu-name.sql b/fix-ubuntu-name.sql deleted file mode 100644 index e8602fe..0000000 --- a/fix-ubuntu-name.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Fix Ubuntu platform name -UPDATE platforms -SET name = 'Ubuntu' -WHERE id = 'ubuntu' AND name = 'Ubuntu/Debian'; - --- Verify the change -SELECT id, name, package_manager FROM platforms WHERE id IN ('ubuntu', 'debian'); diff --git a/src/app/api/sync-winget/route.ts b/src/app/api/sync-winget/route.ts new file mode 100644 index 0000000..b2f7350 --- /dev/null +++ b/src/app/api/sync-winget/route.ts @@ -0,0 +1,86 @@ +import { NextResponse } from 'next/server' +import { WingetPackageFetcher } from '@/services/wingetPackageFetcher' + +export const dynamic = 'force-dynamic' +export const maxDuration = 300 // 5 minutes timeout + +let syncInProgress = false +let syncStatus = { + status: 'idle' as 'idle' | 'fetching' | 'storing' | 'complete' | 'error', + fetchProgress: 0, + fetchTotal: 0, + storeProgress: 0, + storeTotal: 0, + currentPackage: '', + error: null as string | null +} + +export async function GET() { + return NextResponse.json(syncStatus) +} + +export async function POST() { + if (syncInProgress) { + return NextResponse.json( + { error: 'Sync already in progress' }, + { status: 409 } + ) + } + + syncInProgress = true + syncStatus = { + status: 'fetching', + fetchProgress: 0, + fetchTotal: 0, + storeProgress: 0, + storeTotal: 0, + currentPackage: '', + error: null + } + + // Start sync in background + ;(async () => { + try { + const fetcher = new WingetPackageFetcher() + + // Fetch packages + console.log('🔄 Starting Winget package fetch...') + const packages = await fetcher.fetchAllPackages( + (current, total, packageName) => { + syncStatus.fetchProgress = current + syncStatus.fetchTotal = total + syncStatus.currentPackage = packageName + } + ) + + console.log(`✅ Fetched ${packages.length} Winget packages`) + + // Store packages + syncStatus.status = 'storing' + syncStatus.storeTotal = packages.length + + await fetcher.storePackages( + packages, + (current, total) => { + syncStatus.storeProgress = current + syncStatus.storeTotal = total + } + ) + + syncStatus.status = 'complete' + console.log('✅ Winget sync completed successfully') + + } catch (error) { + console.error('❌ Winget sync error:', error) + syncStatus.status = 'error' + syncStatus.error = error instanceof Error ? error.message : 'Unknown error' + } finally { + syncInProgress = false + } + })() + + return NextResponse.json({ + message: 'Winget package sync started', + status: syncStatus + }) +} diff --git a/src/components/ScriptPreview.tsx b/src/components/ScriptPreview.tsx index 512187f..79f13f7 100644 --- a/src/components/ScriptPreview.tsx +++ b/src/components/ScriptPreview.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Button } from '@/components/ui/button' import { GeneratedScript, SelectedPackage, Platform } from '@/types' import { Download, Copy, Check, Terminal, Shield } from 'lucide-react' +import { useLocale } from '@/contexts/LocaleContext' interface ScriptPreviewProps { generatedScript: GeneratedScript | null @@ -20,6 +21,7 @@ export function ScriptPreview({ onClose }: ScriptPreviewProps) { const [copied, setCopied] = useState(false) + const { t } = useLocale() if (!generatedScript || !selectedPlatform) { return null @@ -40,7 +42,8 @@ export function ScriptPreview({ const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url - a.download = `install-packages-${selectedPlatform?.id || 'unknown'}.sh` + const ext = getScriptExtension() + a.download = `install-packages-${selectedPlatform?.id || 'unknown'}${ext}` document.body.appendChild(a) a.click() document.body.removeChild(a) @@ -71,58 +74,58 @@ export function ScriptPreview({ return (
- +
- Installation Script + {t('script.title')} - Idempotent script for {selectedPlatform?.name || 'Unknown Platform'} using {selectedPlatform?.packageManager || 'Unknown Package Manager'} + {t('script.description')} {selectedPlatform?.name || 'Unknown Platform'}
- + {/* Script Info */}
- Official Repositories + {t('script.official_repos')}

- All packages from trusted sources + {t('script.official_repos_desc')}

- Idempotent + {t('script.idempotent')}

- Safe to run multiple times + {t('script.idempotent_desc')}

- {selectedPackages.length} Packages + {selectedPackages.length} {t('script.packages_count')}

- Ready for installation + {t('script.packages_count_desc')}

{/* Package List */}
-

Included Packages:

+

{t('script.included_packages')}

{selectedPackages.map((pkg) => (
-

Script Content:

+

{t('script.script_content')}

@@ -163,13 +166,50 @@ export function ScriptPreview({ {/* Usage Instructions */}
-

How to use:

-
    -
  1. Download the script file to your target machine
  2. -
  3. Make it executable (for Linux/macOS): chmod +x install-packages-{selectedPlatform?.id || 'unknown'}{getScriptExtension()}
  4. -
  5. Run the script with appropriate permissions
  6. -
  7. The script will automatically handle repository setup and package installation
  8. -
+

{t('script.usage')}

+ {selectedPlatform?.id === 'windows' ? ( +
    +
  1. + {t('script.windows_usage.download_intro')} + {t('script.windows_usage.file_name')}) +
  2. +
  3. + {t('script.windows_usage.open_powershell_intro')} + {t('script.windows_usage.disabled_error')}, + {t('script.windows_usage.run_one')} +
    +
    + • {t('script.windows_usage.persistent_title')} + {t('script.windows_usage.persistent_cmd')} + {t('script.windows_usage.then_run')} {t('script.windows_usage.then_run_cmd')} +
    +
    + • {t('script.windows_usage.onetime_title')} + {t('script.windows_usage.onetime_cmd')} +
    +
    +
  4. +
  5. + {t('script.windows_usage.unblock_title')} + {t('script.windows_usage.unblock_cmd')} +
  6. +
  7. + {t('script.windows_usage.winget_title')} + {t('script.windows_usage.store_link_label')}: ms-windows-store://pdp/?productId=9NBLGGH4NNS1 + {t('script.windows_usage.direct_link_label')}: https://aka.ms/getwinget +
  8. +
+ ) : ( +
    +
  1. {t('script.usage_steps.0')}
  2. +
  3. + {t('script.usage_steps.1')} + chmod +x install-packages-{selectedPlatform?.id || 'unknown'}{getScriptExtension()} +
  4. +
  5. {t('script.usage_steps.2')}
  6. +
  7. {t('script.usage_steps.3')}
  8. +
+ )}
diff --git a/src/contexts/LocaleContext.tsx b/src/contexts/LocaleContext.tsx index e49a703..36142bf 100644 --- a/src/contexts/LocaleContext.tsx +++ b/src/contexts/LocaleContext.tsx @@ -9,7 +9,8 @@ const translations = { common: { title: "RepoHub - Cross-Platform Package Manager", subtitle: "Cross-Platform Package Manager", - description: "Simplify software installation across Linux, Windows, and macOS with official repositories" + description: "Simplify software installation across Linux, Windows, and macOS with official repositories", + close: "Close" }, platform: { select: "Select Your Platform", @@ -61,13 +62,33 @@ const translations = { "Run the script with appropriate permissions", "The script will automatically handle repository setup and package installation" ] + , + windows_usage: { + download_intro: "Download the script file (it will be saved as", + file_name: "install-packages-windows.ps1", + open_powershell_intro: "Open PowerShell (preferably as Administrator). If you see", + disabled_error: "running scripts is disabled on this system", + run_one: "run one of the following and then execute the script:", + persistent_title: "Persistent (Current User):", + persistent_cmd: "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force", + then_run: "then run:", + then_run_cmd: ".\\install-packages-windows.ps1", + onetime_title: "One-time run (no policy change):", + onetime_cmd: "powershell -ExecutionPolicy Bypass -File .\\install-packages-windows.ps1", + unblock_title: "If Windows blocked the file, unblock it:", + unblock_cmd: "Unblock-File -Path .\\install-packages-windows.ps1", + winget_title: "If winget is not found, install 'App Installer' and re-run the script:", + store_link_label: "Store link", + direct_link_label: "Direct link" + } } }, tr: { common: { title: "RepoHub - Çok Platformlu Paket Yöneticisi", subtitle: "Çok Platformlu Paket Yöneticisi", - description: "Linux, Windows ve macOS'te resmi depoları kullanarak yazılım kurulumunu basitleştirin" + description: "Linux, Windows ve macOS'te resmi depoları kullanarak yazılım kurulumunu basitleştirin", + close: "Kapat" }, platform: { select: "Platformunuzu Seçin", @@ -119,6 +140,25 @@ const translations = { "Scripti uygun izinlerle çalıştırın", "Script otomatik olarak depo kurulumunu ve paket kurulumunu yönetecektir" ] + , + windows_usage: { + download_intro: "Script dosyasını indirin (şu adla kaydedilecektir", + file_name: "install-packages-windows.ps1", + open_powershell_intro: "PowerShell'i açın (tercihen Yönetici olarak). Eğer şu hatayı görürseniz", + disabled_error: "running scripts is disabled on this system", + run_one: "aşağıdakilerden birini uygulayıp ardından scripti çalıştırın:", + persistent_title: "Kalıcı (Mevcut Kullanıcı):", + persistent_cmd: "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force", + then_run: "sonra çalıştırın:", + then_run_cmd: ".\\install-packages-windows.ps1", + onetime_title: "Tek seferlik (politika değiştirmeden):", + onetime_cmd: "powershell -ExecutionPolicy Bypass -File .\\install-packages-windows.ps1", + unblock_title: "Windows dosyayı engellediyse, engeli kaldırın:", + unblock_cmd: "Unblock-File -Path .\\install-packages-windows.ps1", + winget_title: "winget bulunamazsa, 'App Installer'ı yükleyin ve scripti tekrar çalıştırın:", + store_link_label: "Mağaza bağlantısı", + direct_link_label: "Doğrudan bağlantı" + } } } } diff --git a/src/lib/database/config.ts b/src/lib/database/config.ts index 68653b6..4fac563 100644 --- a/src/lib/database/config.ts +++ b/src/lib/database/config.ts @@ -35,7 +35,10 @@ export async function query(text: string, params?: any[]) { try { const res = await pool.query(text, params) const duration = Date.now() - start - console.log('📊 Query executed', { text, duration, rows: res.rowCount }) + // Only log slow queries (> 100ms) or errors + if (duration > 100) { + console.log('⚠️ Slow query', { text: text.substring(0, 50), duration, rows: res.rowCount }) + } return res } catch (error) { console.error('❌ Query failed', { text, error }) diff --git a/src/lib/scriptGenerator.ts b/src/lib/scriptGenerator.ts index 7113bf0..303a4d4 100644 --- a/src/lib/scriptGenerator.ts +++ b/src/lib/scriptGenerator.ts @@ -140,6 +140,28 @@ function generateWindowsScript(packages: SelectedPackage[]): string { Write-Host "Starting package installation for Windows..." +# Try to unblock this script (no-op if not needed) +try { Unblock-File -Path $MyInvocation.MyCommand.Path -ErrorAction SilentlyContinue } catch {} + +# Warn if not running as Administrator (some installs may require elevation) +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin) { + Write-Warning "It's recommended to run this script as Administrator for best results." +} + +# Ensure winget is available +if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + Write-Warning "Windows Package Manager (winget) is not installed." + Write-Host "We'll open the Microsoft Store page for 'App Installer' (includes winget)." + Write-Host "After installation completes, please re-run this script." + try { + Start-Process "ms-windows-store://pdp/?productId=9NBLGGH4NNS1" + } catch { + Write-Host "If the Store didn't open, install 'App Installer' manually from: https://aka.ms/getwinget" + } + exit 1 +} + # Install packages using winget Write-Host "Installing packages: ${packageNames}" diff --git a/src/services/wingetPackageFetcher.ts b/src/services/wingetPackageFetcher.ts new file mode 100644 index 0000000..efd89c2 --- /dev/null +++ b/src/services/wingetPackageFetcher.ts @@ -0,0 +1,240 @@ +interface WingetPackage { + name: string + publisher: string + version: string + description: string +} + +export class WingetPackageFetcher { + private githubToken = process.env.GITHUB_TOKEN || '' + private requestCount = 0 + private startTime = Date.now() + + /** + * Fetch all Winget packages by scraping GitHub repo structure + * Uses GitHub API with optional token for higher rate limits + */ + async fetchAllPackages( + onProgress?: (current: number, total: number, packageName: string) => void + ): Promise { + console.log('🪟 Fetching Winget packages from GitHub repo...') + + // Rate limits: 5000/hour with token, 60/hour without + const rateLimit = this.githubToken ? 5000 : 60 + const safeLimit = Math.floor(rateLimit * 0.8) // Use 80% to be safe + + if (this.githubToken) { + console.log(`✅ Using GitHub token (${rateLimit} req/hour, using ${safeLimit} to be safe)`) + } else { + console.log(`⚠️ No GitHub token - limited to ${rateLimit} req/hour`) + console.log(' Add GITHUB_TOKEN to .env for higher limits') + } + + const allPackages: WingetPackage[] = [] + const packageSet = new Set() + + // Letters and numbers in manifests folder + const folders = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z'] + + try { + for (let i = 0; i < folders.length; i++) { + const letter = folders[i] + console.log(`📁 Processing folder: ${letter} (${i + 1}/${folders.length})`) + + try { + // Fetch folder listing from GitHub + const url = `https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/${letter}` + const headers: HeadersInit = {} + + if (this.githubToken) { + headers['Authorization'] = `Bearer ${this.githubToken}` + } + + const response = await fetch(url, { headers }) + this.requestCount++ + + if (!response.ok) { + console.error(` ❌ Failed to fetch ${letter}: ${response.status}`) + + // If rate limited, wait and retry + if (response.status === 403) { + console.log(' ⏳ Rate limit hit, waiting 60 seconds...') + await new Promise(resolve => setTimeout(resolve, 60000)) + continue + } + continue + } + + const publishers = await response.json() + console.log(` Found ${publishers.length} publishers`) + + for (const publisher of publishers) { + if (publisher.type !== 'dir') continue + + try { + // Get packages for this publisher + const pkgResponse = await fetch(publisher.url, { headers }) + this.requestCount++ + + if (!pkgResponse.ok) { + if (pkgResponse.status === 403) { + console.log(' ⏳ Rate limit hit, waiting 60 seconds...') + await new Promise(resolve => setTimeout(resolve, 60000)) + } + continue + } + + const packages = await pkgResponse.json() + + for (const pkg of packages) { + if (pkg.type !== 'dir') continue + + const identifier = `${publisher.name}.${pkg.name}` + + if (!packageSet.has(identifier)) { + packageSet.add(identifier) + allPackages.push({ + name: pkg.name, + publisher: publisher.name, + version: 'latest', + description: `${pkg.name} by ${publisher.name}` + }) + } + } + + // Adaptive rate limiting + await this.adaptiveDelay(safeLimit) + + } catch (err) { + // Skip publisher on error + } + } + + if (onProgress) { + onProgress(allPackages.length, folders.length * 300, allPackages[allPackages.length - 1]?.name || 'N/A') + } + + // Log progress + const elapsed = (Date.now() - this.startTime) / 1000 / 60 // minutes + const reqPerMin = this.requestCount / elapsed + console.log(` 📊 Requests: ${this.requestCount} (${reqPerMin.toFixed(1)}/min)`) + + // Delay between folders + await this.adaptiveDelay(safeLimit) + + } catch (err) { + console.error(` Error processing ${letter}:`, err) + } + } + + console.log(`✅ Fetch completed! Total: ${allPackages.length}`) + return allPackages + + } catch (error) { + console.error('❌ Error:', error) + throw error + } + } + + /** + * Adaptive delay to stay under rate limits + * Calculates delay based on current request rate + */ + private async adaptiveDelay(safeLimit: number): Promise { + const elapsedMs = Date.now() - this.startTime + const elapsedHours = elapsedMs / (1000 * 60 * 60) + + // Calculate current rate + const currentRate = this.requestCount / elapsedHours + + // If we're going too fast, add delay + if (currentRate > safeLimit) { + // Calculate how long to wait to stay under limit + const targetRate = safeLimit * 0.9 // 90% of safe limit + const msPerRequest = (1000 * 60 * 60) / targetRate + const delayMs = Math.max(0, msPerRequest - (elapsedMs / this.requestCount)) + + if (delayMs > 100) { + await new Promise(resolve => setTimeout(resolve, delayMs)) + } + } else { + // Small base delay + await new Promise(resolve => setTimeout(resolve, 100)) + } + } + + /** + * Store packages in database + */ + async storePackages( + packages: WingetPackage[], + onProgress?: (current: number, total: number) => void + ): Promise { + const { query } = await import('@/lib/database/config') + + console.log(`💾 Storing ${packages.length} Winget packages in database...`) + + const batchSize = 100 + let stored = 0 + let updated = 0 + let skipped = 0 + + for (let i = 0; i < packages.length; i += batchSize) { + const batch = packages.slice(i, i + batchSize) + + for (const pkg of batch) { + try { + // Check if package already exists + const existingResult = await query( + 'SELECT id, version FROM packages WHERE name = $1 AND platform_id = $2', + [pkg.name, 'windows'] + ) + + // Debug: log first few checks + if (stored + updated + skipped < 5) { + console.log(`Checking: ${pkg.name}`) + console.log(` Found: ${existingResult.rows.length} rows`) + if (existingResult.rows.length > 0) { + console.log(` DB version: ${existingResult.rows[0].version}`) + console.log(` API version: ${pkg.version}`) + console.log(` Same? ${existingResult.rows[0].version === pkg.version}`) + } + } + + if (existingResult.rows.length === 0) { + // Insert new package + await query( + `INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active) + VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)`, + [pkg.name, pkg.description, pkg.version, 'windows', 'gui', 'official', 0, true] + ) + stored++ + } else if (existingResult.rows[0].version !== pkg.version) { + // Update if version changed + await query( + `UPDATE packages + SET version = $1, description = $2, updated_at = NOW() + WHERE id = $3`, + [pkg.version, pkg.description, existingResult.rows[0].id] + ) + updated++ + } else { + skipped++ + } + + if (onProgress && (stored + updated + skipped) % 100 === 0) { + onProgress(stored + updated + skipped, packages.length) + } + + } catch (error) { + console.error(`Error storing package ${pkg.name}:`, error) + } + } + } + + console.log(`✅ Stored ${stored} new packages, updated ${updated}, skipped ${skipped}`) + } +}