mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
feat: add comprehensive Windows PowerShell script usage instructions
- Enhanced ScriptPreview with detailed Windows-specific installation steps including execution policy handling and winget availability checks - Added localization support for all script preview UI elements and Windows usage instructions - Improved script generator with automatic unblocking, admin privilege warnings, and winget validation
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<Card className="w-full max-w-4xl max-h-[90vh] overflow-hidden">
|
||||
<Card className="w-full max-w-4xl max-h-[90vh] overflow-auto">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Terminal className="h-5 w-5" />
|
||||
<span>Installation Script</span>
|
||||
<span>{t('script.title')}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Idempotent script for {selectedPlatform?.name || 'Unknown Platform'} using {selectedPlatform?.packageManager || 'Unknown Package Manager'}
|
||||
{t('script.description')} {selectedPlatform?.name || 'Unknown Platform'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Close
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-4 overflow-y-auto max-h-[70vh] pr-2">
|
||||
{/* Script Info */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-3 bg-secondary rounded-lg">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Shield className="h-4 w-4 text-green-600" />
|
||||
<span className="font-medium text-sm">Official Repositories</span>
|
||||
<span className="font-medium text-sm">{t('script.official_repos')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
All packages from trusted sources
|
||||
{t('script.official_repos_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-secondary rounded-lg">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Terminal className="h-4 w-4 text-blue-600" />
|
||||
<span className="font-medium text-sm">Idempotent</span>
|
||||
<span className="font-medium text-sm">{t('script.idempotent')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Safe to run multiple times
|
||||
{t('script.idempotent_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-secondary rounded-lg">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<Download className="h-4 w-4 text-purple-600" />
|
||||
<span className="font-medium text-sm">{selectedPackages.length} Packages</span>
|
||||
<span className="font-medium text-sm">{selectedPackages.length} {t('script.packages_count')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Ready for installation
|
||||
{t('script.packages_count_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Package List */}
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Included Packages:</h4>
|
||||
<h4 className="font-medium mb-2">{t('script.included_packages')}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedPackages.map((pkg) => (
|
||||
<span
|
||||
@@ -138,7 +141,7 @@ export function ScriptPreview({
|
||||
{/* Script Content */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium">Script Content:</h4>
|
||||
<h4 className="font-medium">{t('script.script_content')}</h4>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleCopy}>
|
||||
{copied ? (
|
||||
@@ -146,11 +149,11 @@ export function ScriptPreview({
|
||||
) : (
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
{copied ? t('script.copied') : t('script.copy')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleDownload}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download {getScriptExtension()}
|
||||
{t('script.download')} {getScriptExtension()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,13 +166,50 @@ export function ScriptPreview({
|
||||
|
||||
{/* Usage Instructions */}
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<h4 className="font-medium text-blue-900 mb-2">How to use:</h4>
|
||||
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
|
||||
<li>Download the script file to your target machine</li>
|
||||
<li>Make it executable (for Linux/macOS): <code className="bg-blue-100 px-1 rounded">chmod +x install-packages-{selectedPlatform?.id || 'unknown'}{getScriptExtension()}</code></li>
|
||||
<li>Run the script with appropriate permissions</li>
|
||||
<li>The script will automatically handle repository setup and package installation</li>
|
||||
</ol>
|
||||
<h4 className="font-medium text-blue-900 mb-2">{t('script.usage')}</h4>
|
||||
{selectedPlatform?.id === 'windows' ? (
|
||||
<ol className="text-sm text-blue-800 space-y-2 list-decimal list-inside">
|
||||
<li>
|
||||
{t('script.windows_usage.download_intro')}
|
||||
<code className="bg-blue-100 px-1 mx-1 rounded">{t('script.windows_usage.file_name')}</code>)
|
||||
</li>
|
||||
<li>
|
||||
{t('script.windows_usage.open_powershell_intro')}
|
||||
<code className="bg-blue-100 px-1 mx-1 rounded">{t('script.windows_usage.disabled_error')}</code>,
|
||||
{t('script.windows_usage.run_one')}
|
||||
<div className="mt-1 space-y-1">
|
||||
<div>
|
||||
• {t('script.windows_usage.persistent_title')}
|
||||
<code className="block bg-blue-100 px-2 py-1 mt-1 rounded">{t('script.windows_usage.persistent_cmd')}</code>
|
||||
{t('script.windows_usage.then_run')} <code className="bg-blue-100 px-1 rounded">{t('script.windows_usage.then_run_cmd')}</code>
|
||||
</div>
|
||||
<div>
|
||||
• {t('script.windows_usage.onetime_title')}
|
||||
<code className="block bg-blue-100 px-2 py-1 mt-1 rounded">{t('script.windows_usage.onetime_cmd')}</code>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{t('script.windows_usage.unblock_title')}
|
||||
<code className="block bg-blue-100 px-2 py-1 mt-1 rounded">{t('script.windows_usage.unblock_cmd')}</code>
|
||||
</li>
|
||||
<li>
|
||||
{t('script.windows_usage.winget_title')}
|
||||
<span className="block mt-1">{t('script.windows_usage.store_link_label')}: ms-windows-store://pdp/?productId=9NBLGGH4NNS1</span>
|
||||
<span className="block">{t('script.windows_usage.direct_link_label')}: https://aka.ms/getwinget</span>
|
||||
</li>
|
||||
</ol>
|
||||
) : (
|
||||
<ol className="text-sm text-blue-800 space-y-2 list-decimal list-inside">
|
||||
<li>{t('script.usage_steps.0')}</li>
|
||||
<li>
|
||||
{t('script.usage_steps.1')}
|
||||
<code className="block bg-blue-100 px-2 py-1 mt-1 rounded">chmod +x install-packages-{selectedPlatform?.id || 'unknown'}{getScriptExtension()}</code>
|
||||
</li>
|
||||
<li>{t('script.usage_steps.2')}</li>
|
||||
<li>{t('script.usage_steps.3')}</li>
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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ı"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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<WingetPackage[]> {
|
||||
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<string>()
|
||||
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user