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:
-
- - Download the script file to your target machine
- - Make it executable (for Linux/macOS):
chmod +x install-packages-{selectedPlatform?.id || 'unknown'}{getScriptExtension()}
- - Run the script with appropriate permissions
- - The script will automatically handle repository setup and package installation
-
+
{t('script.usage')}
+ {selectedPlatform?.id === 'windows' ? (
+
+ -
+ {t('script.windows_usage.download_intro')}
+
{t('script.windows_usage.file_name')})
+
+ -
+ {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')}
+
+
+
+ -
+ {t('script.windows_usage.unblock_title')}
+
{t('script.windows_usage.unblock_cmd')}
+
+ -
+ {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
+
+
+ ) : (
+
+ - {t('script.usage_steps.0')}
+ -
+ {t('script.usage_steps.1')}
+
chmod +x install-packages-{selectedPlatform?.id || 'unknown'}{getScriptExtension()}
+
+ - {t('script.usage_steps.2')}
+ - {t('script.usage_steps.3')}
+
+ )}
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}`)
+ }
+}