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:
Yusuf İpek
2025-11-11 16:13:29 +03:00
parent a462c18cc6
commit 299e951368
7 changed files with 457 additions and 33 deletions
-7
View File
@@ -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');
+86
View File
@@ -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
})
}
+62 -22
View File
@@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { GeneratedScript, SelectedPackage, Platform } from '@/types' import { GeneratedScript, SelectedPackage, Platform } from '@/types'
import { Download, Copy, Check, Terminal, Shield } from 'lucide-react' import { Download, Copy, Check, Terminal, Shield } from 'lucide-react'
import { useLocale } from '@/contexts/LocaleContext'
interface ScriptPreviewProps { interface ScriptPreviewProps {
generatedScript: GeneratedScript | null generatedScript: GeneratedScript | null
@@ -20,6 +21,7 @@ export function ScriptPreview({
onClose onClose
}: ScriptPreviewProps) { }: ScriptPreviewProps) {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const { t } = useLocale()
if (!generatedScript || !selectedPlatform) { if (!generatedScript || !selectedPlatform) {
return null return null
@@ -40,7 +42,8 @@ export function ScriptPreview({
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
a.href = url 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) document.body.appendChild(a)
a.click() a.click()
document.body.removeChild(a) document.body.removeChild(a)
@@ -71,58 +74,58 @@ export function ScriptPreview({
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"> <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> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="flex items-center space-x-2"> <CardTitle className="flex items-center space-x-2">
<Terminal className="h-5 w-5" /> <Terminal className="h-5 w-5" />
<span>Installation Script</span> <span>{t('script.title')}</span>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Idempotent script for {selectedPlatform?.name || 'Unknown Platform'} using {selectedPlatform?.packageManager || 'Unknown Package Manager'} {t('script.description')} {selectedPlatform?.name || 'Unknown Platform'}
</CardDescription> </CardDescription>
</div> </div>
<Button variant="outline" onClick={onClose}> <Button variant="outline" onClick={onClose}>
Close {t('common.close')}
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4 overflow-y-auto max-h-[70vh] pr-2">
{/* Script Info */} {/* Script Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-3 bg-secondary rounded-lg"> <div className="p-3 bg-secondary rounded-lg">
<div className="flex items-center space-x-2 mb-1"> <div className="flex items-center space-x-2 mb-1">
<Shield className="h-4 w-4 text-green-600" /> <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> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
All packages from trusted sources {t('script.official_repos_desc')}
</p> </p>
</div> </div>
<div className="p-3 bg-secondary rounded-lg"> <div className="p-3 bg-secondary rounded-lg">
<div className="flex items-center space-x-2 mb-1"> <div className="flex items-center space-x-2 mb-1">
<Terminal className="h-4 w-4 text-blue-600" /> <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> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Safe to run multiple times {t('script.idempotent_desc')}
</p> </p>
</div> </div>
<div className="p-3 bg-secondary rounded-lg"> <div className="p-3 bg-secondary rounded-lg">
<div className="flex items-center space-x-2 mb-1"> <div className="flex items-center space-x-2 mb-1">
<Download className="h-4 w-4 text-purple-600" /> <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> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Ready for installation {t('script.packages_count_desc')}
</p> </p>
</div> </div>
</div> </div>
{/* Package List */} {/* Package List */}
<div> <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"> <div className="flex flex-wrap gap-2">
{selectedPackages.map((pkg) => ( {selectedPackages.map((pkg) => (
<span <span
@@ -138,7 +141,7 @@ export function ScriptPreview({
{/* Script Content */} {/* Script Content */}
<div> <div>
<div className="flex items-center justify-between mb-2"> <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"> <div className="flex space-x-2">
<Button variant="outline" size="sm" onClick={handleCopy}> <Button variant="outline" size="sm" onClick={handleCopy}>
{copied ? ( {copied ? (
@@ -146,11 +149,11 @@ export function ScriptPreview({
) : ( ) : (
<Copy className="h-4 w-4 mr-2" /> <Copy className="h-4 w-4 mr-2" />
)} )}
{copied ? 'Copied!' : 'Copy'} {copied ? t('script.copied') : t('script.copy')}
</Button> </Button>
<Button size="sm" onClick={handleDownload}> <Button size="sm" onClick={handleDownload}>
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
Download {getScriptExtension()} {t('script.download')} {getScriptExtension()}
</Button> </Button>
</div> </div>
</div> </div>
@@ -163,13 +166,50 @@ export function ScriptPreview({
{/* Usage Instructions */} {/* Usage Instructions */}
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg"> <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> <h4 className="font-medium text-blue-900 mb-2">{t('script.usage')}</h4>
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside"> {selectedPlatform?.id === 'windows' ? (
<li>Download the script file to your target machine</li> <ol className="text-sm text-blue-800 space-y-2 list-decimal list-inside">
<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>
<li>Run the script with appropriate permissions</li> {t('script.windows_usage.download_intro')}
<li>The script will automatically handle repository setup and package installation</li> <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>
) : (
<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> </div>
</CardContent> </CardContent>
</Card> </Card>
+42 -2
View File
@@ -9,7 +9,8 @@ const translations = {
common: { common: {
title: "RepoHub - Cross-Platform Package Manager", title: "RepoHub - Cross-Platform Package Manager",
subtitle: "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: { platform: {
select: "Select Your Platform", select: "Select Your Platform",
@@ -61,13 +62,33 @@ const translations = {
"Run the script with appropriate permissions", "Run the script with appropriate permissions",
"The script will automatically handle repository setup and package installation" "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: { tr: {
common: { common: {
title: "RepoHub - Çok Platformlu Paket Yöneticisi", title: "RepoHub - Çok Platformlu Paket Yöneticisi",
subtitle: "Ç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: { platform: {
select: "Platformunuzu Seçin", select: "Platformunuzu Seçin",
@@ -119,6 +140,25 @@ const translations = {
"Scripti uygun izinlerle çalıştırın", "Scripti uygun izinlerle çalıştırın",
"Script otomatik olarak depo kurulumunu ve paket kurulumunu yönetecektir" "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ı"
}
} }
} }
} }
+4 -1
View File
@@ -35,7 +35,10 @@ export async function query(text: string, params?: any[]) {
try { try {
const res = await pool.query(text, params) const res = await pool.query(text, params)
const duration = Date.now() - start 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 return res
} catch (error) { } catch (error) {
console.error('❌ Query failed', { text, error }) console.error('❌ Query failed', { text, error })
+22
View File
@@ -140,6 +140,28 @@ function generateWindowsScript(packages: SelectedPackage[]): string {
Write-Host "Starting package installation for Windows..." 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 # Install packages using winget
Write-Host "Installing packages: ${packageNames}" Write-Host "Installing packages: ${packageNames}"
+240
View File
@@ -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}`)
}
}