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
})
}
+63 -23
View File
@@ -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>
+42 -2
View File
@@ -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ı"
}
}
}
}
+4 -1
View File
@@ -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 })
+22
View File
@@ -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}"
+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}`)
}
}