mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
feat: add multiple package sync sources and API integration
- Added undici dependency and implemented three package fetcher variants (simple text parsing, v2 with better error handling, and official repository sync) - Integrated real API calls in PlatformSelector and PackageBrowser components with fallback to mock data - Extended sync API route to support multiple data sources (debian-simple, ubuntu-simple, debian-official-v2, etc.) with platform initialization
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
-- 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');
|
||||||
+2
-1
@@ -28,7 +28,8 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
"zustand": "^4.4.7",
|
"zustand": "^4.4.7",
|
||||||
"pg": "^8.11.3"
|
"pg": "^8.11.3",
|
||||||
|
"undici": "^6.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
|||||||
Generated
+9
@@ -56,6 +56,9 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7([email protected])
|
version: 1.0.7([email protected])
|
||||||
|
undici:
|
||||||
|
specifier: ^6.6.2
|
||||||
|
version: 6.22.0
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^4.4.7
|
specifier: ^4.4.7
|
||||||
version: 4.5.7(@types/[email protected])([email protected])
|
version: 4.5.7(@types/[email protected])([email protected])
|
||||||
@@ -2258,6 +2261,10 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==}
|
||||||
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==}
|
resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==}
|
||||||
|
|
||||||
@@ -4713,6 +4720,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { PackageService } from '@/services/packageService'
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const platform_id = searchParams.get('platform_id') || 'debian'
|
||||||
|
|
||||||
|
console.log('🐛 Debug: Fetching packages for platform:', platform_id)
|
||||||
|
|
||||||
|
// Test 1: Simple query without filters
|
||||||
|
const result1 = await PackageService.getMany({
|
||||||
|
platform_id,
|
||||||
|
limit: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('🐛 Debug: Simple query result:', {
|
||||||
|
total: result1.total,
|
||||||
|
packageCount: result1.packages.length,
|
||||||
|
firstPackage: result1.packages[0]?.name || 'None'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 2: Query with search
|
||||||
|
const result2 = await PackageService.getMany({
|
||||||
|
platform_id,
|
||||||
|
search: 'firefox',
|
||||||
|
limit: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('🐛 Debug: Search query result:', {
|
||||||
|
total: result2.total,
|
||||||
|
packageCount: result2.packages.length,
|
||||||
|
firstPackage: result2.packages[0]?.name || 'None'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 3: Raw SQL query
|
||||||
|
const rawQuery = `
|
||||||
|
SELECT COUNT(*) as total,
|
||||||
|
MIN(p.name) as first_package
|
||||||
|
FROM packages p
|
||||||
|
WHERE p.platform_id = $1 AND p.is_active = true
|
||||||
|
`
|
||||||
|
|
||||||
|
const rawResult = await query(rawQuery, [platform_id])
|
||||||
|
console.log('🐛 Debug: Raw SQL result:', rawResult.rows[0])
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
simpleQuery: {
|
||||||
|
total: result1.total,
|
||||||
|
packages: result1.packages.slice(0, 3).map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
version: p.version
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
searchQuery: {
|
||||||
|
total: result2.total,
|
||||||
|
packages: result2.packages.slice(0, 3).map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
version: p.version
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
rawSql: rawResult.rows[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('🐛 Debug: Error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { PlatformInitializer } from '@/services/platformInitializer'
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
await PlatformInitializer.initializePlatforms()
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Platforms initialized successfully'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing platforms:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to initialize platforms' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
import { SimplePackageFetcher } from '@/services/simplePackageFetcher'
|
||||||
|
import { PlatformInitializer } from '@/services/platformInitializer'
|
||||||
|
import { setSyncInProgress, setSyncProgress } from '@/app/api/sync-status/route'
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
// Set sync as in progress
|
||||||
|
setSyncInProgress(true)
|
||||||
|
|
||||||
|
// Create a readable stream for SSE
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
try {
|
||||||
|
// Helper function to send progress updates
|
||||||
|
const sendProgress = (message: string, progress?: number, total?: number) => {
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
message,
|
||||||
|
progress,
|
||||||
|
total,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
||||||
|
setSyncProgress(message, progress, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendProgress('🔄 Starting package synchronization...')
|
||||||
|
|
||||||
|
// Initialize platforms
|
||||||
|
sendProgress('📋 Initializing platforms...')
|
||||||
|
await PlatformInitializer.initializePlatforms()
|
||||||
|
sendProgress('✅ Platforms initialized')
|
||||||
|
|
||||||
|
// Fetch Debian packages
|
||||||
|
sendProgress('🐧 Fetching Debian packages...')
|
||||||
|
await SimplePackageFetcher.fetchDebianPackagesWithProgress(sendProgress)
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
sendProgress('❌ Sync cancelled during Debian processing')
|
||||||
|
controller.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendProgress('✅ Debian packages completed')
|
||||||
|
|
||||||
|
// Fetch Ubuntu packages
|
||||||
|
sendProgress('🐧 Fetching Ubuntu packages...')
|
||||||
|
await SimplePackageFetcher.fetchUbuntuPackagesWithProgress(sendProgress)
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
sendProgress('❌ Sync cancelled during Ubuntu processing')
|
||||||
|
controller.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendProgress('✅ Ubuntu packages completed')
|
||||||
|
|
||||||
|
// Final success message
|
||||||
|
sendProgress('🎉 All packages synchronized successfully!')
|
||||||
|
|
||||||
|
// Close the stream
|
||||||
|
controller.close()
|
||||||
|
setSyncInProgress(false)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
const errorData = JSON.stringify({
|
||||||
|
error: errorMessage,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
controller.enqueue(encoder.encode(`data: ${errorData}\n\n`))
|
||||||
|
controller.close()
|
||||||
|
setSyncInProgress(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
cancelled = true
|
||||||
|
setSyncInProgress(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
// Global sync state (in production, use Redis or database)
|
||||||
|
let syncInProgress = false
|
||||||
|
let syncProgress = { message: '', progress: 0, total: 100 }
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
inProgress: syncInProgress,
|
||||||
|
progress: syncProgress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const body = await request.json()
|
||||||
|
const { action } = body
|
||||||
|
|
||||||
|
if (action === 'cancel') {
|
||||||
|
syncInProgress = false
|
||||||
|
syncProgress = { message: 'Sync cancelled', progress: 0, total: 100 }
|
||||||
|
return NextResponse.json({ message: 'Sync cancelled successfully' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'start') {
|
||||||
|
syncInProgress = true
|
||||||
|
syncProgress = { message: 'Starting sync...', progress: 0, total: 100 }
|
||||||
|
return NextResponse.json({ message: 'Sync started' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for other endpoints to use
|
||||||
|
export function setSyncProgress(message: string, progress?: number, total?: number) {
|
||||||
|
syncProgress = { message, progress: progress || 0, total: total || 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSyncInProgress(inProgress: boolean) {
|
||||||
|
syncInProgress = inProgress
|
||||||
|
}
|
||||||
@@ -1,20 +1,90 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { MetadataFetcher } from '@/services/metadataFetcher'
|
import { MetadataFetcher } from '@/services/metadataFetcher'
|
||||||
|
import { DebianPackageFetcher } from '@/services/debianPackageFetcher'
|
||||||
|
import { PackageFetcherV2 } from '@/services/packageFetcherV2'
|
||||||
|
import { SimplePackageFetcher } from '@/services/simplePackageFetcher'
|
||||||
|
import { PlatformInitializer } from '@/services/platformInitializer'
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { platform_id, all_platforms } = body
|
const { platform_id, all_platforms, source } = body
|
||||||
|
|
||||||
if (all_platforms) {
|
// Initialize platforms first
|
||||||
// Sync all platforms
|
await PlatformInitializer.initializePlatforms()
|
||||||
|
|
||||||
|
if (source === 'debian-simple') {
|
||||||
|
// Sync Debian packages (simple text parsing, no gzip)
|
||||||
|
await SimplePackageFetcher.fetchDebianPackages()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Debian packages synced (simple text parsing)',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'ubuntu-simple') {
|
||||||
|
// Sync Ubuntu packages (simple text parsing, no gzip)
|
||||||
|
await SimplePackageFetcher.fetchUbuntuPackages()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Ubuntu packages synced (simple text parsing)',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'all-simple') {
|
||||||
|
// Sync all Debian-based packages (simple text parsing)
|
||||||
|
await SimplePackageFetcher.syncAll()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'All Debian-based packages synced (simple text parsing)',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'debian-official-v2') {
|
||||||
|
// Sync Debian packages from official repository (v2 with better error handling)
|
||||||
|
await PackageFetcherV2.fetchDebianPackages()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Debian packages synced from official repository (v2)',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'ubuntu-official-v2') {
|
||||||
|
// Sync Ubuntu packages from official repository (v2 with better error handling)
|
||||||
|
await PackageFetcherV2.fetchUbuntuPackages()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Ubuntu packages synced from official repository (v2)',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'all-official-v2') {
|
||||||
|
// Sync all Debian-based packages from official repositories (v2)
|
||||||
|
await PackageFetcherV2.syncAll()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'All Debian-based packages synced from official repositories (v2)',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'debian-official') {
|
||||||
|
// Sync Debian packages from official repository
|
||||||
|
await DebianPackageFetcher.fetchDebianPackages()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Debian packages synced from official repository',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'ubuntu-official') {
|
||||||
|
// Sync Ubuntu packages from official repository
|
||||||
|
await DebianPackageFetcher.fetchUbuntuPackages()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Ubuntu packages synced from official repository',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (source === 'all-official') {
|
||||||
|
// Sync all Debian-based packages from official repositories
|
||||||
|
await DebianPackageFetcher.syncAll()
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'All Debian-based packages synced from official repositories',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
} else if (all_platforms) {
|
||||||
|
// Sync all platforms (legacy method)
|
||||||
await MetadataFetcher.syncAllPlatforms()
|
await MetadataFetcher.syncAllPlatforms()
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: 'All platforms synced successfully',
|
message: 'All platforms synced successfully',
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
})
|
})
|
||||||
} else if (platform_id) {
|
} else if (platform_id) {
|
||||||
// Sync specific platform
|
// Sync specific platform (legacy method)
|
||||||
await MetadataFetcher.syncPlatform(platform_id)
|
await MetadataFetcher.syncPlatform(platform_id)
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: `Platform ${platform_id} synced successfully`,
|
message: `Platform ${platform_id} synced successfully`,
|
||||||
@@ -22,7 +92,7 @@ export async function POST(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Either platform_id or all_platforms must be specified' },
|
{ error: 'Either platform_id, all_platforms, or source must be specified' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { query } from '@/lib/database/config'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Test 1: Database connection
|
||||||
|
const dbTest = await query('SELECT NOW() as current_time', [])
|
||||||
|
|
||||||
|
// Test 2: Packages table count
|
||||||
|
const packageCount = await query('SELECT COUNT(*) as count FROM packages', [])
|
||||||
|
|
||||||
|
// Test 3: Debian packages count
|
||||||
|
const debianCount = await query('SELECT COUNT(*) as count FROM packages WHERE platform_id = $1', ['debian'])
|
||||||
|
|
||||||
|
// Test 4: Active packages count
|
||||||
|
const activeCount = await query('SELECT COUNT(*) as count FROM packages WHERE is_active = true', [])
|
||||||
|
|
||||||
|
// Test 5: Sample packages
|
||||||
|
const samplePackages = await query(`
|
||||||
|
SELECT id, name, version, platform_id
|
||||||
|
FROM packages
|
||||||
|
WHERE platform_id = $1 AND is_active = true
|
||||||
|
LIMIT 5
|
||||||
|
`, ['debian'])
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
database: {
|
||||||
|
connected: true,
|
||||||
|
currentTime: dbTest.rows[0].current_time
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
total: packageCount.rows[0].count,
|
||||||
|
debian: debianCount.rows[0].count,
|
||||||
|
active: activeCount.rows[0].count,
|
||||||
|
sample: samplePackages.rows
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database test error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
database: { connected: false }
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { PackageFetcherV2 } from '@/services/packageFetcherV2'
|
||||||
|
import { PlatformInitializer } from '@/services/platformInitializer'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
console.log('🧪 Starting test sync...')
|
||||||
|
|
||||||
|
// Test 1: Initialize platforms
|
||||||
|
await PlatformInitializer.initializePlatforms()
|
||||||
|
console.log('✅ Platforms initialized')
|
||||||
|
|
||||||
|
// Test 2: Try to fetch a small sample
|
||||||
|
console.log('🔄 Testing Debian package fetch...')
|
||||||
|
|
||||||
|
// Just test the fetch and parse, don't store in DB
|
||||||
|
const response = await fetch('https://packages.debian.org/stable/allpackages?format=txt.gz', {
|
||||||
|
headers: {
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'User-Agent': 'RepoHub-Package-Fetcher/1.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Response received, size:', response.headers.get('content-length'))
|
||||||
|
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
console.log('✅ Buffer received, size:', buffer.byteLength)
|
||||||
|
|
||||||
|
// Check magic bytes
|
||||||
|
const bytes = new Uint8Array(buffer.slice(0, 2))
|
||||||
|
console.log('🔍 Magic bytes:', bytes[0].toString(16), bytes[1].toString(16))
|
||||||
|
|
||||||
|
// Try decompression
|
||||||
|
const { gunzip } = await import('zlib')
|
||||||
|
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
gunzip(new Uint8Array(buffer), (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('❌ Gunzip error:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
console.log('✅ Gunzip successful, size:', result.length)
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = decompressed.toString('utf-8')
|
||||||
|
console.log('✅ Text decoded, length:', text.length)
|
||||||
|
|
||||||
|
// Parse first 10 lines
|
||||||
|
const lines = text.split('\n').slice(0, 20)
|
||||||
|
console.log('📋 First 20 lines:')
|
||||||
|
lines.forEach((line, i) => console.log(`${i + 1}: ${line}`))
|
||||||
|
|
||||||
|
// Test parsing
|
||||||
|
const samplePackages = PackageFetcherV2['parseDebianPackageList'](text)
|
||||||
|
console.log('📊 Sample packages parsed:', samplePackages.length)
|
||||||
|
|
||||||
|
if (samplePackages.length > 0) {
|
||||||
|
console.log('📦 First package:', samplePackages[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Test completed successfully',
|
||||||
|
bufferSize: buffer.byteLength,
|
||||||
|
textSize: text.length,
|
||||||
|
samplePackages: samplePackages.length,
|
||||||
|
firstPackage: samplePackages[0] || null
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { mockPackages, categories, licenses } from '@/data/mockData'
|
import { apiClient } from '@/lib/api/client'
|
||||||
import { useLocale } from '@/contexts/LocaleContext'
|
import { useLocale } from '@/contexts/LocaleContext'
|
||||||
import { Package, FilterOptions, Platform } from '@/types'
|
import { Package, FilterOptions, Platform } from '@/types'
|
||||||
import { Search, Package as PackageIcon, Terminal, Monitor } from 'lucide-react'
|
import { Search, Package as PackageIcon, Terminal, Monitor } from 'lucide-react'
|
||||||
@@ -24,18 +24,53 @@ export function PackageBrowser({
|
|||||||
onFiltersChange
|
onFiltersChange
|
||||||
}: PackageBrowserProps) {
|
}: PackageBrowserProps) {
|
||||||
const { t } = useLocale()
|
const { t } = useLocale()
|
||||||
|
const [packages, setPackages] = useState<Package[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [filters, setFilters] = useState<FilterOptions>({
|
const [filters, setFilters] = useState<FilterOptions>({
|
||||||
platforms: [],
|
platform_id: selectedPlatform?.id || '',
|
||||||
categories: [],
|
type: '',
|
||||||
licenses: [],
|
repository: '',
|
||||||
types: [],
|
search: '',
|
||||||
repositories: [],
|
limit: 50,
|
||||||
searchQuery: ''
|
offset: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Load packages when platform changes
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPackages = async () => {
|
||||||
|
if (!selectedPlatform) {
|
||||||
|
setPackages([])
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await apiClient.getPackages({
|
||||||
|
platform_id: selectedPlatform.id,
|
||||||
|
search: searchQuery,
|
||||||
|
type: filters.type || undefined,
|
||||||
|
repository: filters.repository || undefined,
|
||||||
|
limit: filters.limit,
|
||||||
|
offset: filters.offset
|
||||||
|
})
|
||||||
|
setPackages(result.packages)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load packages:', error)
|
||||||
|
// Fallback to mock data if API fails
|
||||||
|
const { mockPackages } = await import('@/data/mockData')
|
||||||
|
setPackages(mockPackages.filter(pkg => pkg.platform === selectedPlatform.id))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPackages()
|
||||||
|
}, [selectedPlatform, searchQuery, filters.type, filters.repository])
|
||||||
|
|
||||||
const filteredPackages = useMemo(() => {
|
const filteredPackages = useMemo(() => {
|
||||||
return mockPackages.filter(pkg => {
|
return packages.filter(pkg => {
|
||||||
// Platform filter
|
// Platform filter
|
||||||
if (selectedPlatform && pkg.platform !== selectedPlatform.id) {
|
if (selectedPlatform && pkg.platform !== selectedPlatform.id) {
|
||||||
return false
|
return false
|
||||||
@@ -149,28 +184,52 @@ export function PackageBrowser({
|
|||||||
<SelectValue placeholder={t('packages.filters.repository')} />
|
<SelectValue placeholder={t('packages.filters.repository')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="official">{t('packages.filters.official')}</SelectItem>
|
{t('packages.description')}
|
||||||
<SelectItem value="third_party">{t('packages.filters.third_party')}</SelectItem>
|
</CardDescription>
|
||||||
</SelectContent>
|
</CardHeader>
|
||||||
</Select>
|
<CardContent className="pt-0">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
Loading packages...
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Package List */}
|
return (
|
||||||
<Card>
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader className="pb-4">
|
||||||
<CardTitle>{t('packages.browse')} ({filteredPackages.length})</CardTitle>
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
<CardDescription>
|
<PackageIcon className="h-5 w-5" />
|
||||||
|
{t('packages.title')}
|
||||||
|
{selectedPlatform && (
|
||||||
|
<span className="text-sm font-normal text-muted-foreground">
|
||||||
|
({packages.length} packages for {selectedPlatform.name})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
{t('packages.description')}
|
{t('packages.description')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{filteredPackages.map((pkg) => (
|
{/* Filters */}
|
||||||
<div
|
<Card>
|
||||||
key={pkg.id}
|
<CardHeader>
|
||||||
className={`p-4 border rounded-lg transition-colors ${
|
<CardTitle>Filters</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('packages.search')}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-input rounded-md bg-background"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
isPackageSelected(pkg)
|
isPackageSelected(pkg)
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
: 'border-border hover:bg-secondary/50'
|
: 'border-border hover:bg-secondary/50'
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Search, Monitor, Terminal, Package as PackageIcon } from 'lucide-react'
|
||||||
|
import { Package, FilterOptions, Platform } from '@/types'
|
||||||
|
import { apiClient } from '@/lib/api/client'
|
||||||
|
import { useLocale } from '@/contexts/LocaleContext'
|
||||||
|
|
||||||
|
interface PackageBrowserProps {
|
||||||
|
selectedPlatform: Platform | null
|
||||||
|
selectedPackages: Package[]
|
||||||
|
onPackageToggle: (pkg: Package) => void
|
||||||
|
onFiltersChange: (filters: FilterOptions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageBrowserV2({
|
||||||
|
selectedPlatform,
|
||||||
|
selectedPackages,
|
||||||
|
onPackageToggle,
|
||||||
|
onFiltersChange
|
||||||
|
}: PackageBrowserProps) {
|
||||||
|
const { t } = useLocale()
|
||||||
|
const [packages, setPackages] = useState<Package[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [totalCount, setTotalCount] = useState(0)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>('')
|
||||||
|
const [repositoryFilter, setRepositoryFilter] = useState<string>('')
|
||||||
|
const [scrollPosition, setScrollPosition] = useState(0)
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Load packages when platform changes
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPackages = async () => {
|
||||||
|
if (!selectedPlatform) {
|
||||||
|
setPackages([])
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setPackages([])
|
||||||
|
setHasMore(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Frontend: Fetching initial packages for', selectedPlatform.id)
|
||||||
|
const params: any = {
|
||||||
|
platform_id: selectedPlatform.id,
|
||||||
|
limit: 50,
|
||||||
|
offset: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery && searchQuery.trim()) {
|
||||||
|
params.search = searchQuery.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFilter && typeFilter !== 'all') {
|
||||||
|
params.type = typeFilter as 'gui' | 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repositoryFilter && repositoryFilter !== 'all') {
|
||||||
|
params.repository = repositoryFilter as 'official' | 'third-party'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Frontend: API params:', params)
|
||||||
|
const result = await apiClient.getPackages(params)
|
||||||
|
console.log('📦 Frontend: Received initial packages:', {
|
||||||
|
total: result.total,
|
||||||
|
packageCount: result.packages.length,
|
||||||
|
firstPackage: result.packages[0]?.name || 'None'
|
||||||
|
})
|
||||||
|
setPackages(result.packages)
|
||||||
|
setTotalCount(result.total)
|
||||||
|
setHasMore(result.packages.length < result.total)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load packages:', error)
|
||||||
|
setPackages([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPackages()
|
||||||
|
}, [selectedPlatform, typeFilter, repositoryFilter])
|
||||||
|
|
||||||
|
// Debounced search to prevent focus loss
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPlatform) return
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
const loadPackages = async () => {
|
||||||
|
// DON'T set loading to true - it causes re-render and focus loss
|
||||||
|
// setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
platform_id: selectedPlatform.id,
|
||||||
|
limit: 50,
|
||||||
|
offset: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery && searchQuery.trim()) {
|
||||||
|
params.search = searchQuery.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFilter && typeFilter !== 'all') {
|
||||||
|
params.type = typeFilter as 'gui' | 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repositoryFilter && repositoryFilter !== 'all') {
|
||||||
|
params.repository = repositoryFilter as 'official' | 'third-party'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Frontend: Debounced API params:', params)
|
||||||
|
const result = await apiClient.getPackages(params)
|
||||||
|
console.log('📦 Frontend: Debounced result:', result.packages.length)
|
||||||
|
|
||||||
|
// Update packages without triggering loading state
|
||||||
|
setPackages(result.packages)
|
||||||
|
setTotalCount(result.total)
|
||||||
|
setHasMore(result.packages.length < result.total)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load packages:', error)
|
||||||
|
// Don't clear packages on error during search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPackages()
|
||||||
|
}, 300) // 300ms debounce
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId)
|
||||||
|
}, [searchQuery])
|
||||||
|
|
||||||
|
// Load more packages
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (!selectedPlatform || loadingMore || !hasMore) return
|
||||||
|
|
||||||
|
setLoadingMore(true)
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
platform_id: selectedPlatform.id,
|
||||||
|
limit: 50,
|
||||||
|
offset: packages.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery && searchQuery.trim()) {
|
||||||
|
params.search = searchQuery.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFilter && typeFilter !== 'all') {
|
||||||
|
params.type = typeFilter as 'gui' | 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repositoryFilter && repositoryFilter !== 'all') {
|
||||||
|
params.repository = repositoryFilter as 'official' | 'third-party'
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiClient.getPackages(params)
|
||||||
|
setPackages(prev => [...prev, ...result.packages])
|
||||||
|
setHasMore(packages.length + result.packages.length < result.total)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load more packages:', error)
|
||||||
|
} finally {
|
||||||
|
setLoadingMore(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPackageSelected = (pkg: Package) => {
|
||||||
|
return selectedPackages.some(selected => selected.id === pkg.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPackageIcon = (type: string) => {
|
||||||
|
return type === 'gui' ? <Monitor className="h-4 w-4" /> : <Terminal className="h-4 w-4" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<PackageIcon className="h-5 w-5" />
|
||||||
|
{t('packages.title')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
{t('packages.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
Loading packages...
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedPlatform) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<PackageIcon className="h-5 w-5" />
|
||||||
|
{t('packages.title')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
{t('packages.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
Please select a platform first
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<PackageIcon className="h-5 w-5" />
|
||||||
|
{t('packages.title')}
|
||||||
|
<span className="text-sm font-normal text-muted-foreground">
|
||||||
|
({packages.length} of {totalCount} packages for {selectedPlatform?.name || 'Unknown Platform'})
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
{t('packages.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('packages.search')}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-input rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onFocus={() => searchInputRef.current?.focus()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type Filter */}
|
||||||
|
<Select value={typeFilter || "all"} onValueChange={(value) => setTypeFilter(value === "all" ? "" : value)}>
|
||||||
|
<SelectTrigger className="w-full sm:w-40">
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Types</SelectItem>
|
||||||
|
<SelectItem value="gui">GUI</SelectItem>
|
||||||
|
<SelectItem value="cli">CLI</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Repository Filter */}
|
||||||
|
<Select value={repositoryFilter || "all"} onValueChange={(value) => setRepositoryFilter(value === "all" ? "" : value)}>
|
||||||
|
<SelectTrigger className="w-full sm:w-40">
|
||||||
|
<SelectValue placeholder="Repository" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Repositories</SelectItem>
|
||||||
|
<SelectItem value="official">Official</SelectItem>
|
||||||
|
<SelectItem value="third-party">Third Party</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Package List */}
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto min-h-[400px]">
|
||||||
|
{loading && packages.length === 0 ? (
|
||||||
|
// Skeleton loading to prevent layout shift
|
||||||
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
|
<div key={`skeleton-${index}`} className="flex items-center space-x-3 p-3 border border-border rounded-lg">
|
||||||
|
<div className="w-4 h-4 bg-muted animate-pulse rounded"></div>
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="h-4 bg-muted animate-pulse rounded w-1/3"></div>
|
||||||
|
<div className="h-3 bg-muted animate-pulse rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : packages.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
{t('packages.no_packages')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
packages.map((pkg) => (
|
||||||
|
<div
|
||||||
|
key={pkg.id}
|
||||||
|
className={`flex items-start space-x-3 p-3 rounded-lg border transition-colors cursor-pointer ${
|
||||||
|
isPackageSelected(pkg)
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-border hover:bg-secondary/50'
|
||||||
|
}`}
|
||||||
|
onClick={() => onPackageToggle(pkg)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={isPackageSelected(pkg)}
|
||||||
|
onCheckedChange={() => onPackageToggle(pkg)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-start space-x-2 flex-1">
|
||||||
|
<div className="mt-0.5">
|
||||||
|
{getPackageIcon(pkg.type || 'cli')}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-medium truncate">{pkg.name}</h4>
|
||||||
|
<span className="text-xs text-muted-foreground bg-secondary px-2 py-1 rounded">
|
||||||
|
{pkg.version}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
||||||
|
{pkg.description || 'No description available'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{pkg.type?.toUpperCase() || 'CLI'}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{pkg.repository || 'official'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Load More Button */}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={loadMore}
|
||||||
|
disabled={loadingMore}
|
||||||
|
className="min-w-32"
|
||||||
|
>
|
||||||
|
{loadingMore ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
|
||||||
|
Loading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`Load More (${Math.min(50, totalCount - packages.length)} remaining)`
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { platforms } from '@/data/mockData'
|
import { apiClient } from '@/lib/api/client'
|
||||||
import { useLocale } from '@/contexts/LocaleContext'
|
import { useLocale } from '@/contexts/LocaleContext'
|
||||||
import { Platform } from '@/types'
|
import { Platform } from '@/types'
|
||||||
|
|
||||||
@@ -14,6 +14,44 @@ interface PlatformSelectorProps {
|
|||||||
|
|
||||||
export function PlatformSelector({ selectedPlatform, onPlatformSelect }: PlatformSelectorProps) {
|
export function PlatformSelector({ selectedPlatform, onPlatformSelect }: PlatformSelectorProps) {
|
||||||
const { t } = useLocale()
|
const { t } = useLocale()
|
||||||
|
const [platforms, setPlatforms] = useState<Platform[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPlatforms = async () => {
|
||||||
|
try {
|
||||||
|
const platformsData = await apiClient.getPlatforms()
|
||||||
|
setPlatforms(platformsData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load platforms:', error)
|
||||||
|
// Fallback to mock data if API fails
|
||||||
|
const { platforms: mockPlatforms } = await import('@/data/mockData')
|
||||||
|
setPlatforms(mockPlatforms)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlatforms()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="text-lg">{t('platform.select')}</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
{t('platform.description')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
Loading platforms...
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { LocaleProvider } from '@/contexts/LocaleContext'
|
import { LocaleProvider } from '@/contexts/LocaleContext'
|
||||||
import { Header } from '@/components/Header'
|
import { Header } from './Header'
|
||||||
import { PlatformSelector } from '@/components/PlatformSelector'
|
import { PlatformSelector } from './PlatformSelector'
|
||||||
import { PackageBrowser } from '@/components/PackageBrowser'
|
import { PackageBrowserV2 } from './PackageBrowserV2'
|
||||||
import { SelectionManager } from '@/components/SelectionManager'
|
import { SelectionManager } from './SelectionManager'
|
||||||
import { ScriptPreview } from '@/components/ScriptPreview'
|
import { ScriptPreview } from '@/components/ScriptPreview'
|
||||||
import { generateScript } from '@/lib/scriptGenerator'
|
import { generateScript } from '@/lib/scriptGenerator'
|
||||||
import { useLocale } from '@/contexts/LocaleContext'
|
import { useLocale } from '@/contexts/LocaleContext'
|
||||||
@@ -86,7 +86,7 @@ function RepoHubAppContent() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Package Browser */}
|
{/* Package Browser */}
|
||||||
<PackageBrowser
|
<PackageBrowserV2
|
||||||
selectedPlatform={selectedPlatform}
|
selectedPlatform={selectedPlatform}
|
||||||
selectedPackages={selectedPackages}
|
selectedPackages={selectedPackages}
|
||||||
onPackageToggle={handlePackageToggle}
|
onPackageToggle={handlePackageToggle}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ 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}.sh`
|
a.download = `install-packages-${selectedPlatform?.id || 'unknown'}.sh`
|
||||||
document.body.appendChild(a)
|
document.body.appendChild(a)
|
||||||
a.click()
|
a.click()
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
@@ -48,18 +48,22 @@ export function ScriptPreview({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getScriptLanguage = () => {
|
const getScriptLanguage = () => {
|
||||||
switch (selectedPlatform.id) {
|
switch (selectedPlatform?.id) {
|
||||||
case 'windows':
|
case 'windows':
|
||||||
return 'powershell'
|
return 'powershell'
|
||||||
|
case 'macos':
|
||||||
|
return 'bash'
|
||||||
default:
|
default:
|
||||||
return 'bash'
|
return 'bash'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getScriptExtension = () => {
|
const getScriptExtension = () => {
|
||||||
switch (selectedPlatform.id) {
|
switch (selectedPlatform?.id) {
|
||||||
case 'windows':
|
case 'windows':
|
||||||
return '.ps1'
|
return '.ps1'
|
||||||
|
case 'macos':
|
||||||
|
return '.sh'
|
||||||
default:
|
default:
|
||||||
return '.sh'
|
return '.sh'
|
||||||
}
|
}
|
||||||
@@ -76,7 +80,7 @@ export function ScriptPreview({
|
|||||||
<span>Installation Script</span>
|
<span>Installation Script</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Idempotent script for {selectedPlatform.name} using {selectedPlatform.packageManager}
|
Idempotent script for {selectedPlatform?.name || 'Unknown Platform'} using {selectedPlatform?.packageManager || 'Unknown Package Manager'}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>
|
||||||
@@ -162,7 +166,7 @@ export function ScriptPreview({
|
|||||||
<h4 className="font-medium text-blue-900 mb-2">How to use:</h4>
|
<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">
|
<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>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}{getScriptExtension()}</code></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>Run the script with appropriate permissions</li>
|
||||||
<li>The script will automatically handle repository setup and package installation</li>
|
<li>The script will automatically handle repository setup and package installation</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -73,11 +73,9 @@ export function SelectionManager({
|
|||||||
{pkg.description}
|
{pkg.description}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center space-x-2 text-xs text-muted-foreground mt-1">
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground mt-1">
|
||||||
<span>{pkg.platform}</span>
|
<span>{pkg.type?.toUpperCase() || 'CLI'}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{pkg.category}</span>
|
<span>{pkg.repository || 'official'}</span>
|
||||||
<span>•</span>
|
|
||||||
<span>{pkg.license}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const translations = {
|
|||||||
selected: "Selected"
|
selected: "Selected"
|
||||||
},
|
},
|
||||||
packages: {
|
packages: {
|
||||||
|
title: "Available Packages",
|
||||||
browse: "Available Packages",
|
browse: "Available Packages",
|
||||||
description: "Select packages to include in your installation script",
|
description: "Select packages to include in your installation script",
|
||||||
search: "Search packages...",
|
search: "Search packages...",
|
||||||
@@ -74,6 +75,7 @@ const translations = {
|
|||||||
selected: "Seçildi"
|
selected: "Seçildi"
|
||||||
},
|
},
|
||||||
packages: {
|
packages: {
|
||||||
|
title: "Mevcut Paketler",
|
||||||
browse: "Mevcut Paketler",
|
browse: "Mevcut Paketler",
|
||||||
description: "Kurulum scriptinize dahil edilecek paketleri seçin",
|
description: "Kurulum scriptinize dahil edilecek paketleri seçin",
|
||||||
search: "Paket ara...",
|
search: "Paket ara...",
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Platform, Package, FilterOptions } from '@/types'
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3002/api'
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform operations
|
||||||
|
async getPlatforms(): Promise<Platform[]> {
|
||||||
|
return this.request<Platform[]>('/platforms')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPlatform(id: string): Promise<Platform | null> {
|
||||||
|
try {
|
||||||
|
return await this.request<Platform>(`/platforms/${id}`)
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package operations
|
||||||
|
async getPackages(filters: FilterOptions = {}): Promise<{ packages: Package[], total: number }> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
if (filters.platform_id) params.append('platform_id', filters.platform_id)
|
||||||
|
if (filters.category_id) params.append('category_id', filters.category_id.toString())
|
||||||
|
if (filters.type) params.append('type', filters.type)
|
||||||
|
if (filters.repository) params.append('repository', filters.repository)
|
||||||
|
if (filters.search) params.append('search', filters.search)
|
||||||
|
if (filters.limit) params.append('limit', filters.limit.toString())
|
||||||
|
if (filters.offset) params.append('offset', filters.offset.toString())
|
||||||
|
if (filters.sort_by) params.append('sort_by', filters.sort_by)
|
||||||
|
if (filters.sort_order) params.append('sort_order', filters.sort_order)
|
||||||
|
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : ''
|
||||||
|
return this.request<{ packages: Package[], total: number }>(`/packages${query}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPackage(id: string): Promise<Package | null> {
|
||||||
|
try {
|
||||||
|
return await this.request<Package>(`/packages/${id}`)
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync operations
|
||||||
|
async syncDebianPackages(): Promise<{ message: string, timestamp: string }> {
|
||||||
|
return this.request<{ message: string, timestamp: string }>('/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ source: 'debian-official' }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncUbuntuPackages(): Promise<{ message: string, timestamp: string }> {
|
||||||
|
return this.request<{ message: string, timestamp: string }>('/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ source: 'ubuntu-official' }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncAllDebianPackages(): Promise<{ message: string, timestamp: string }> {
|
||||||
|
return this.request<{ message: string, timestamp: string }>('/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ source: 'all-official' }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSyncStatus(): Promise<{ status: string, last_sync: string | null, platforms: string[] }> {
|
||||||
|
return this.request<{ status: string, last_sync: string | null, platforms: string[] }>('/sync')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiClient = new ApiClient()
|
||||||
@@ -13,8 +13,9 @@ export function generateScript(packages: SelectedPackage[], platform: Platform):
|
|||||||
|
|
||||||
function createScriptContent(packages: SelectedPackage[], platform: Platform): string {
|
function createScriptContent(packages: SelectedPackage[], platform: Platform): string {
|
||||||
switch (platform.id) {
|
switch (platform.id) {
|
||||||
|
case 'debian':
|
||||||
case 'ubuntu':
|
case 'ubuntu':
|
||||||
return generateUbuntuScript(packages)
|
return generateDebianBasedScript(packages, platform.name)
|
||||||
case 'fedora':
|
case 'fedora':
|
||||||
return generateFedoraScript(packages)
|
return generateFedoraScript(packages)
|
||||||
case 'arch':
|
case 'arch':
|
||||||
@@ -28,18 +29,18 @@ function createScriptContent(packages: SelectedPackage[], platform: Platform): s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateUbuntuScript(packages: SelectedPackage[]): string {
|
function generateDebianBasedScript(packages: SelectedPackage[], platformName: string): string {
|
||||||
const packageNames = packages.map(p => p.name).join(' ')
|
const packageNames = packages.map(p => p.name).join(' ')
|
||||||
|
|
||||||
return `#!/bin/bash
|
return `#!/bin/bash
|
||||||
|
|
||||||
# RepoHub Installation Script for Ubuntu/Debian
|
# RepoHub Installation Script for ${platformName}
|
||||||
# Generated on ${new Date().toISOString()}
|
# Generated on ${new Date().toISOString()}
|
||||||
# This script is idempotent and safe to run multiple times
|
# This script is idempotent and safe to run multiple times
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "Starting package installation for Ubuntu/Debian..."
|
echo "Starting package installation for ${platformName}..."
|
||||||
|
|
||||||
# Update package lists
|
# Update package lists
|
||||||
echo "Updating package lists..."
|
echo "Updating package lists..."
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
// Using Node.js built-in fetch instead of undici for better compatibility
|
||||||
|
// import { fetch } from 'undici'
|
||||||
|
import { PackageService } from './packageService'
|
||||||
|
import { CreatePackageInput } from '@/models/Package'
|
||||||
|
|
||||||
|
interface DebianPackageInfo {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
description?: string
|
||||||
|
section?: string
|
||||||
|
architecture?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DebianPackageFetcher {
|
||||||
|
private static readonly DEBIAN_PACKAGES_URL = 'https://packages.debian.org/stable/allpackages?format=txt.gz'
|
||||||
|
private static readonly UBUNTU_PACKAGES_URL = 'https://packages.ubuntu.com/noble/allpackages?format=txt.gz'
|
||||||
|
|
||||||
|
// Fetch and parse Debian packages
|
||||||
|
static async fetchDebianPackages(): Promise<void> {
|
||||||
|
console.log('🔄 Fetching Debian packages from official repository...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.DEBIAN_PACKAGES_URL)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Debian packages: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the compressed data
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
const compressed = new Uint8Array(buffer)
|
||||||
|
|
||||||
|
// Decompress using Node.js zlib with proper error handling
|
||||||
|
const { gunzip } = await import('zlib')
|
||||||
|
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
gunzip(compressed, {
|
||||||
|
finishFlush: 1, // Z_SYNC_FLUSH
|
||||||
|
windowBits: 15 + 16 // Enable gzip header decoding
|
||||||
|
}, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Gunzip error:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = decompressed.toString('utf-8')
|
||||||
|
const packages = this.parseDebianPackageList(text)
|
||||||
|
|
||||||
|
console.log(`📊 Parsed ${packages.length} Debian packages`)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
await this.storePackages('debian', packages)
|
||||||
|
|
||||||
|
console.log('✅ Debian packages fetched and stored successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching Debian packages:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and parse Ubuntu packages
|
||||||
|
static async fetchUbuntuPackages(): Promise<void> {
|
||||||
|
console.log('🔄 Fetching Ubuntu packages from official repository...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.UBUNTU_PACKAGES_URL)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Ubuntu packages: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the compressed data
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
const compressed = new Uint8Array(buffer)
|
||||||
|
|
||||||
|
// Decompress using Node.js zlib with proper error handling
|
||||||
|
const { gunzip } = await import('zlib')
|
||||||
|
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
gunzip(compressed, {
|
||||||
|
finishFlush: 1, // Z_SYNC_FLUSH
|
||||||
|
windowBits: 15 + 16 // Enable gzip header decoding
|
||||||
|
}, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Gunzip error:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = decompressed.toString('utf-8')
|
||||||
|
const packages = this.parseUbuntuPackageList(text)
|
||||||
|
|
||||||
|
console.log(`📊 Parsed ${packages.length} Ubuntu packages`)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
await this.storePackages('ubuntu', packages)
|
||||||
|
|
||||||
|
console.log('✅ Ubuntu packages fetched and stored successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching Ubuntu packages:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Debian package list format
|
||||||
|
private static parseDebianPackageList(text: string): CreatePackageInput[] {
|
||||||
|
const packages: CreatePackageInput[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
// Skip header lines until we find package entries
|
||||||
|
let packageStartIndex = 0
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].match(/^[a-z0-9][a-z0-9+.-]*\s+\([^)]+\)/)) {
|
||||||
|
packageStartIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = packageStartIndex; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
|
||||||
|
// Parse package line format: "package-name (version) description"
|
||||||
|
const match = line.match(/^([a-z0-9][a-z0-9+.-]*)\s+\(([^)]+)\)\s*(.+)?$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, name, version, description] = match
|
||||||
|
|
||||||
|
packages.push({
|
||||||
|
id: `debian-${name}`,
|
||||||
|
name: name,
|
||||||
|
description: description?.trim(),
|
||||||
|
version: version.trim(),
|
||||||
|
platform_id: 'debian',
|
||||||
|
type: this.determinePackageType(name, description),
|
||||||
|
repository: 'official',
|
||||||
|
popularity_score: Math.floor(Math.random() * 100) // Placeholder - would be calculated from download stats
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Ubuntu package list format (with repository info)
|
||||||
|
private static parseUbuntuPackageList(text: string): CreatePackageInput[] {
|
||||||
|
const packages: CreatePackageInput[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
// Skip header lines until we find package entries
|
||||||
|
let packageStartIndex = 0
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].match(/^[a-z0-9][a-z0-9+.-]*\s+\([^)]+\)\s+\[.*\]/)) {
|
||||||
|
packageStartIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = packageStartIndex; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
|
||||||
|
// Parse package line format: "package-name (version) [repository] description"
|
||||||
|
const match = line.match(/^([a-z0-9][a-z0-9+.-]*)\s+\(([^)]+)\)\s+\[([^\]]+)\]\s*(.+)?$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, name, version, repository, description] = match
|
||||||
|
|
||||||
|
packages.push({
|
||||||
|
id: `ubuntu-${name}`,
|
||||||
|
name: name,
|
||||||
|
description: description?.trim(),
|
||||||
|
version: version.trim(),
|
||||||
|
platform_id: 'ubuntu',
|
||||||
|
type: this.determinePackageType(name, description),
|
||||||
|
repository: this.mapUbuntuRepository(repository),
|
||||||
|
popularity_score: Math.floor(Math.random() * 100) // Placeholder - would be calculated from download stats
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Ubuntu repository to our enum
|
||||||
|
private static mapUbuntuRepository(repo: string): 'official' | 'third-party' {
|
||||||
|
switch (repo.toLowerCase()) {
|
||||||
|
case 'main':
|
||||||
|
case 'restricted':
|
||||||
|
case 'universe':
|
||||||
|
case 'multiverse':
|
||||||
|
return 'official'
|
||||||
|
default:
|
||||||
|
return 'third-party'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if package is GUI or CLI based on name and description
|
||||||
|
private static determinePackageType(name: string, description?: string): 'gui' | 'cli' {
|
||||||
|
const guiKeywords = [
|
||||||
|
'gui', 'gtk', 'qt', 'x11', 'desktop', 'window', 'display',
|
||||||
|
'graphical', 'visual', 'image', 'video', 'audio', 'media',
|
||||||
|
'browser', 'editor', 'viewer', 'player', 'manager',
|
||||||
|
'game', 'games', 'steam', 'wine'
|
||||||
|
]
|
||||||
|
|
||||||
|
const cliKeywords = [
|
||||||
|
'cli', 'command', 'terminal', 'console', 'shell', 'bash',
|
||||||
|
'tool', 'utility', 'daemon', 'service', 'server', 'client',
|
||||||
|
'lib', 'dev', 'debug', 'build', 'compile'
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchText = `${name} ${description || ''}`.toLowerCase()
|
||||||
|
|
||||||
|
for (const keyword of guiKeywords) {
|
||||||
|
if (searchText.includes(keyword)) return 'gui'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const keyword of cliKeywords) {
|
||||||
|
if (searchText.includes(keyword)) return 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to CLI for system packages
|
||||||
|
return 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store packages in database
|
||||||
|
private static async storePackages(platformId: string, packages: CreatePackageInput[]): Promise<void> {
|
||||||
|
console.log(`💾 Storing ${packages.length} packages for platform ${platformId}...`)
|
||||||
|
|
||||||
|
let added = 0
|
||||||
|
let updated = 0
|
||||||
|
let batchSize = 100
|
||||||
|
let processed = 0
|
||||||
|
|
||||||
|
// Process in batches to avoid overwhelming the database
|
||||||
|
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 exists
|
||||||
|
const existing = await PackageService.getById(pkg.id)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Update existing package
|
||||||
|
await PackageService.update(pkg.id, {
|
||||||
|
description: pkg.description,
|
||||||
|
version: pkg.version,
|
||||||
|
type: pkg.type,
|
||||||
|
repository: pkg.repository,
|
||||||
|
popularity_score: pkg.popularity_score
|
||||||
|
})
|
||||||
|
updated++
|
||||||
|
} else {
|
||||||
|
// Create new package
|
||||||
|
await PackageService.create(pkg)
|
||||||
|
added++
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error storing package ${pkg.id}:`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++
|
||||||
|
|
||||||
|
// Progress indicator
|
||||||
|
if (processed % 100 === 0) {
|
||||||
|
console.log(`📈 Processed ${processed}/${packages.length} packages...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📈 Final: Added: ${added}, Updated: ${updated}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync both Debian and Ubuntu packages
|
||||||
|
static async syncAll(): Promise<void> {
|
||||||
|
console.log('🔄 Starting sync of all Debian-based packages...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchDebianPackages()
|
||||||
|
await this.fetchUbuntuPackages()
|
||||||
|
|
||||||
|
console.log('✅ All Debian-based packages synced successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during sync:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
import { PackageService } from './packageService'
|
||||||
|
import { CreatePackageInput } from '@/models/Package'
|
||||||
|
|
||||||
|
export class PackageFetcherV2 {
|
||||||
|
private static readonly DEBIAN_PACKAGES_URL = 'https://packages.debian.org/stable/allpackages?format=txt.gz'
|
||||||
|
private static readonly UBUNTU_PACKAGES_URL = 'https://packages.ubuntu.com/noble/allpackages?format=txt.gz'
|
||||||
|
|
||||||
|
// Fetch and parse Debian packages with better error handling
|
||||||
|
static async fetchDebianPackages(): Promise<void> {
|
||||||
|
console.log('🔄 Fetching Debian packages from official repository...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use Node.js built-in fetch with proper headers
|
||||||
|
const response = await fetch(this.DEBIAN_PACKAGES_URL, {
|
||||||
|
headers: {
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'User-Agent': 'RepoHub-Package-Fetcher/1.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Debian packages: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the response as buffer
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
|
||||||
|
// Try to decompress with multiple methods
|
||||||
|
let text: string
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Method 1: Try direct text (in case it's not compressed)
|
||||||
|
text = new TextDecoder().decode(buffer)
|
||||||
|
|
||||||
|
// Check if it looks like gzip (starts with gzip magic bytes)
|
||||||
|
const bytes = new Uint8Array(buffer.slice(0, 2))
|
||||||
|
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
||||||
|
throw new Error('Data is compressed, need to decompress')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Method 2: Try zlib decompression
|
||||||
|
try {
|
||||||
|
const { gunzip } = await import('zlib')
|
||||||
|
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
gunzip(new Uint8Array(buffer), (err, result) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
text = decompressed.toString('utf-8')
|
||||||
|
} catch (gzipError) {
|
||||||
|
// Method 3: Try streaming decompression
|
||||||
|
try {
|
||||||
|
const { createGunzip } = await import('zlib')
|
||||||
|
const { Readable } = await import('stream')
|
||||||
|
const { pipeline } = await import('stream/promises')
|
||||||
|
|
||||||
|
const readable = Readable.from([new Uint8Array(buffer)])
|
||||||
|
const gunzip = createGunzip()
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
|
||||||
|
gunzip.on('data', (chunk) => chunks.push(chunk))
|
||||||
|
|
||||||
|
await pipeline(readable, gunzip)
|
||||||
|
text = Buffer.concat(chunks).toString('utf-8')
|
||||||
|
} catch (streamError) {
|
||||||
|
throw new Error(`All decompression methods failed. Gzip error: ${gzipError}, Stream error: ${streamError}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const packages = this.parseDebianPackageList(text)
|
||||||
|
console.log(`📊 Parsed ${packages.length} Debian packages`)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
await this.storePackages('debian', packages)
|
||||||
|
|
||||||
|
console.log('✅ Debian packages fetched and stored successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching Debian packages:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and parse Ubuntu packages with better error handling
|
||||||
|
static async fetchUbuntuPackages(): Promise<void> {
|
||||||
|
console.log('🔄 Fetching Ubuntu packages from official repository...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use Node.js built-in fetch with proper headers
|
||||||
|
const response = await fetch(this.UBUNTU_PACKAGES_URL, {
|
||||||
|
headers: {
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'User-Agent': 'RepoHub-Package-Fetcher/1.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Ubuntu packages: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the response as buffer
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
|
||||||
|
// Try to decompress with multiple methods
|
||||||
|
let text: string
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Method 1: Try direct text (in case it's not compressed)
|
||||||
|
text = new TextDecoder().decode(buffer)
|
||||||
|
|
||||||
|
// Check if it looks like gzip (starts with gzip magic bytes)
|
||||||
|
const bytes = new Uint8Array(buffer.slice(0, 2))
|
||||||
|
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
||||||
|
throw new Error('Data is compressed, need to decompress')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Method 2: Try zlib decompression
|
||||||
|
try {
|
||||||
|
const { gunzip } = await import('zlib')
|
||||||
|
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
||||||
|
gunzip(new Uint8Array(buffer), (err, result) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
text = decompressed.toString('utf-8')
|
||||||
|
} catch (gzipError) {
|
||||||
|
// Method 3: Try streaming decompression
|
||||||
|
try {
|
||||||
|
const { createGunzip } = await import('zlib')
|
||||||
|
const { Readable } = await import('stream')
|
||||||
|
const { pipeline } = await import('stream/promises')
|
||||||
|
|
||||||
|
const readable = Readable.from([new Uint8Array(buffer)])
|
||||||
|
const gunzip = createGunzip()
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
|
||||||
|
gunzip.on('data', (chunk) => chunks.push(chunk))
|
||||||
|
|
||||||
|
await pipeline(readable, gunzip)
|
||||||
|
text = Buffer.concat(chunks).toString('utf-8')
|
||||||
|
} catch (streamError) {
|
||||||
|
throw new Error(`All decompression methods failed. Gzip error: ${gzipError}, Stream error: ${streamError}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const packages = this.parseUbuntuPackageList(text)
|
||||||
|
console.log(`📊 Parsed ${packages.length} Ubuntu packages`)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
await this.storePackages('ubuntu', packages)
|
||||||
|
|
||||||
|
console.log('✅ Ubuntu packages fetched and stored successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching Ubuntu packages:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Debian package list format
|
||||||
|
private static parseDebianPackageList(text: string): CreatePackageInput[] {
|
||||||
|
const packages: CreatePackageInput[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
// Skip header lines until we find package entries
|
||||||
|
let packageStartIndex = 0
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].match(/^[a-z0-9][a-z0-9+.-]*\s+\([^)]+\)/)) {
|
||||||
|
packageStartIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = packageStartIndex; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
|
||||||
|
// Parse package line format: "package-name (version) description"
|
||||||
|
const match = line.match(/^([a-z0-9][a-z0-9+.-]*)\s+\(([^)]+)\)\s*(.+)?$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, name, version, description] = match
|
||||||
|
|
||||||
|
packages.push({
|
||||||
|
id: `debian-${name}`,
|
||||||
|
name: name,
|
||||||
|
description: description?.trim(),
|
||||||
|
version: version.trim(),
|
||||||
|
platform_id: 'debian',
|
||||||
|
type: this.determinePackageType(name, description),
|
||||||
|
repository: 'official',
|
||||||
|
popularity_score: Math.floor(Math.random() * 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Ubuntu package list format (with repository info)
|
||||||
|
private static parseUbuntuPackageList(text: string): CreatePackageInput[] {
|
||||||
|
const packages: CreatePackageInput[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
// Skip header lines until we find package entries
|
||||||
|
let packageStartIndex = 0
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].match(/^[a-z0-9][a-z0-9+.-]*\s+\([^)]+\)\s+\[.*\]/)) {
|
||||||
|
packageStartIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = packageStartIndex; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
|
||||||
|
// Parse package line format: "package-name (version) [repository] description"
|
||||||
|
const match = line.match(/^([a-z0-9][a-z0-9+.-]*)\s+\(([^)]+)\)\s+\[([^\]]+)\]\s*(.+)?$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, name, version, repository, description] = match
|
||||||
|
|
||||||
|
packages.push({
|
||||||
|
id: `ubuntu-${name}`,
|
||||||
|
name: name,
|
||||||
|
description: description?.trim(),
|
||||||
|
version: version.trim(),
|
||||||
|
platform_id: 'ubuntu',
|
||||||
|
type: this.determinePackageType(name, description),
|
||||||
|
repository: this.mapUbuntuRepository(repository),
|
||||||
|
popularity_score: Math.floor(Math.random() * 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Ubuntu repository to our enum
|
||||||
|
private static mapUbuntuRepository(repo: string): 'official' | 'third-party' {
|
||||||
|
switch (repo.toLowerCase()) {
|
||||||
|
case 'main':
|
||||||
|
case 'restricted':
|
||||||
|
case 'universe':
|
||||||
|
case 'multiverse':
|
||||||
|
return 'official'
|
||||||
|
default:
|
||||||
|
return 'third-party'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if package is GUI or CLI based on name and description
|
||||||
|
private static determinePackageType(name: string, description?: string): 'gui' | 'cli' {
|
||||||
|
const guiKeywords = [
|
||||||
|
'gui', 'gtk', 'qt', 'x11', 'desktop', 'window', 'display',
|
||||||
|
'graphical', 'visual', 'image', 'video', 'audio', 'media',
|
||||||
|
'browser', 'editor', 'viewer', 'player', 'manager',
|
||||||
|
'game', 'games', 'steam', 'wine'
|
||||||
|
]
|
||||||
|
|
||||||
|
const cliKeywords = [
|
||||||
|
'cli', 'command', 'terminal', 'console', 'shell', 'bash',
|
||||||
|
'tool', 'utility', 'daemon', 'service', 'server', 'client',
|
||||||
|
'lib', 'dev', 'debug', 'build', 'compile'
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchText = `${name} ${description || ''}`.toLowerCase()
|
||||||
|
|
||||||
|
for (const keyword of guiKeywords) {
|
||||||
|
if (searchText.includes(keyword)) return 'gui'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const keyword of cliKeywords) {
|
||||||
|
if (searchText.includes(keyword)) return 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store packages in database
|
||||||
|
private static async storePackages(platformId: string, packages: CreatePackageInput[]): Promise<void> {
|
||||||
|
console.log(`💾 Storing ${packages.length} packages for platform ${platformId}...`)
|
||||||
|
|
||||||
|
let added = 0
|
||||||
|
let updated = 0
|
||||||
|
let batchSize = 100
|
||||||
|
let processed = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < packages.length; i += batchSize) {
|
||||||
|
const batch = packages.slice(i, i + batchSize)
|
||||||
|
|
||||||
|
for (const pkg of batch) {
|
||||||
|
try {
|
||||||
|
const existing = await PackageService.getById(pkg.id)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await PackageService.update(pkg.id, {
|
||||||
|
description: pkg.description,
|
||||||
|
version: pkg.version,
|
||||||
|
type: pkg.type,
|
||||||
|
repository: pkg.repository,
|
||||||
|
popularity_score: pkg.popularity_score
|
||||||
|
})
|
||||||
|
updated++
|
||||||
|
} else {
|
||||||
|
await PackageService.create(pkg)
|
||||||
|
added++
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error storing package ${pkg.id}:`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++
|
||||||
|
|
||||||
|
if (processed % 100 === 0) {
|
||||||
|
console.log(`📈 Processed ${processed}/${packages.length} packages...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📈 Final: Added: ${added}, Updated: ${updated}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync both Debian and Ubuntu packages
|
||||||
|
static async syncAll(): Promise<void> {
|
||||||
|
console.log('🔄 Starting sync of all Debian-based packages...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchDebianPackages()
|
||||||
|
await this.fetchUbuntuPackages()
|
||||||
|
|
||||||
|
console.log('✅ All Debian-based packages synced successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during sync:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,10 +43,10 @@ export class PackageService {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
whereConditions.push(`(
|
whereConditions.push(`(
|
||||||
to_tsvector('english', p.name) @@ to_tsquery('english', $${paramIndex}) OR
|
p.name ILIKE $${paramIndex} OR
|
||||||
to_tsvector('english', p.description) @@ to_tsquery('english', $${paramIndex})
|
p.description ILIKE $${paramIndex}
|
||||||
)`)
|
)`)
|
||||||
values.push(search.split(' ').join(' & '))
|
values.push(`%${search}%`)
|
||||||
paramIndex++
|
paramIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,27 +69,26 @@ export class PackageService {
|
|||||||
const countResult = await query(countQuery, values)
|
const countResult = await query(countQuery, values)
|
||||||
const total = parseInt(countResult.rows[0].total)
|
const total = parseInt(countResult.rows[0].total)
|
||||||
|
|
||||||
// Get packages with joins
|
// Get packages with joins (simplified)
|
||||||
const packagesQuery = `
|
const packagesQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
p.*,
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.version,
|
||||||
|
p.platform_id,
|
||||||
|
p.type,
|
||||||
|
p.repository,
|
||||||
|
p.popularity_score,
|
||||||
|
p.is_active,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at,
|
||||||
pl.name as platform_name,
|
pl.name as platform_name,
|
||||||
pl.package_manager as platform_package_manager,
|
pl.package_manager as platform_package_manager,
|
||||||
pl.icon as platform_icon,
|
pl.icon as platform_icon
|
||||||
c.name as category_name,
|
|
||||||
l.name as license_name,
|
|
||||||
l.url as license_url,
|
|
||||||
COALESCE(
|
|
||||||
ARRAY_AGG(DISTINCT pt.tag) FILTER (WHERE pt.tag IS NOT NULL),
|
|
||||||
ARRAY[]::VARCHAR[]
|
|
||||||
) as tags
|
|
||||||
FROM packages p
|
FROM packages p
|
||||||
LEFT JOIN platforms pl ON p.platform_id = pl.id
|
LEFT JOIN platforms pl ON p.platform_id = pl.id
|
||||||
LEFT JOIN categories c ON p.category_id = c.id
|
|
||||||
LEFT JOIN licenses l ON p.license_id = l.id
|
|
||||||
LEFT JOIN package_tags pt ON p.id = pt.package_id
|
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY p.id, pl.name, pl.package_manager, pl.icon, c.name, l.name, l.url
|
|
||||||
${orderClause}
|
${orderClause}
|
||||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
||||||
`
|
`
|
||||||
@@ -101,18 +100,12 @@ export class PackageService {
|
|||||||
const packages = packagesResult.rows.map((row: any) => ({
|
const packages = packagesResult.rows.map((row: any) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description || 'No description available',
|
||||||
version: row.version,
|
version: row.version,
|
||||||
platform_id: row.platform_id,
|
platform_id: row.platform_id,
|
||||||
category_id: row.category_id,
|
type: row.type || 'cli',
|
||||||
license_id: row.license_id,
|
repository: row.repository || 'official',
|
||||||
type: row.type,
|
popularity_score: row.popularity_score || 0,
|
||||||
repository: row.repository,
|
|
||||||
homepage_url: row.homepage_url,
|
|
||||||
download_url: row.download_url,
|
|
||||||
last_updated: row.last_updated,
|
|
||||||
downloads_count: row.downloads_count,
|
|
||||||
popularity_score: row.popularity_score,
|
|
||||||
is_active: row.is_active,
|
is_active: row.is_active,
|
||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
@@ -122,16 +115,7 @@ export class PackageService {
|
|||||||
package_manager: row.platform_package_manager,
|
package_manager: row.platform_package_manager,
|
||||||
icon: row.platform_icon
|
icon: row.platform_icon
|
||||||
} : undefined,
|
} : undefined,
|
||||||
category: row.category_name ? {
|
tags: [] // Empty tags for now
|
||||||
id: row.category_id,
|
|
||||||
name: row.category_name
|
|
||||||
} : undefined,
|
|
||||||
license: row.license_name ? {
|
|
||||||
id: row.license_id,
|
|
||||||
name: row.license_name,
|
|
||||||
url: row.license_url
|
|
||||||
} : undefined,
|
|
||||||
tags: row.tags || []
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return { packages, total }
|
return { packages, total }
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { PlatformService } from './platformService'
|
||||||
|
|
||||||
|
export class PlatformInitializer {
|
||||||
|
// Initialize all platforms in database
|
||||||
|
static async initializePlatforms(): Promise<void> {
|
||||||
|
console.log('🔄 Initializing platforms in database...')
|
||||||
|
|
||||||
|
const platforms = [
|
||||||
|
{
|
||||||
|
id: 'debian',
|
||||||
|
name: 'Debian',
|
||||||
|
package_manager: 'apt',
|
||||||
|
icon: '🐧'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ubuntu',
|
||||||
|
name: 'Ubuntu',
|
||||||
|
package_manager: 'apt',
|
||||||
|
icon: '🐧'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fedora',
|
||||||
|
name: 'Fedora',
|
||||||
|
package_manager: 'dnf',
|
||||||
|
icon: '🎩'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arch',
|
||||||
|
name: 'Arch Linux',
|
||||||
|
package_manager: 'pacman',
|
||||||
|
icon: '🏛️'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'windows',
|
||||||
|
name: 'Windows',
|
||||||
|
package_manager: 'winget',
|
||||||
|
icon: '🪟'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'macos',
|
||||||
|
name: 'macOS',
|
||||||
|
package_manager: 'homebrew',
|
||||||
|
icon: '🍎'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const platform of platforms) {
|
||||||
|
try {
|
||||||
|
const existing = await PlatformService.getById(platform.id)
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await PlatformService.create(platform)
|
||||||
|
console.log(`✅ Created platform: ${platform.name}`)
|
||||||
|
} else {
|
||||||
|
// Update existing platform to ensure correct name
|
||||||
|
await PlatformService.update(platform.id, platform)
|
||||||
|
console.log(`🔄 Updated platform: ${platform.name}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error initializing platform ${platform.id}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Platform initialization completed')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
import { PackageService } from './packageService'
|
||||||
|
import { CreatePackageInput } from '@/models/Package'
|
||||||
|
|
||||||
|
export class SimplePackageFetcher {
|
||||||
|
private static readonly DEBIAN_PACKAGES_URL = 'https://packages.debian.org/stable/allpackages?format=txt.gz'
|
||||||
|
private static readonly UBUNTU_PACKAGES_URL = 'https://packages.ubuntu.com/noble/allpackages?format=txt.gz'
|
||||||
|
|
||||||
|
// Fetch and parse Debian packages (simple text parsing)
|
||||||
|
static async fetchDebianPackages(): Promise<void> {
|
||||||
|
await this.fetchDebianPackagesWithProgress(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch with progress callback
|
||||||
|
static async fetchDebianPackagesWithProgress(
|
||||||
|
onProgress: (message: string, progress?: number, total?: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
onProgress('🔄 Fetching Debian packages from official repository...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
onProgress('📡 Downloading package list...')
|
||||||
|
const response = await fetch(this.DEBIAN_PACKAGES_URL, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'RepoHub-Package-Fetcher/1.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Debian packages: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get as text directly (not compressed!)
|
||||||
|
const text = await response.text()
|
||||||
|
onProgress(`📊 Downloaded ${Math.round(text.length / 1024 / 1024)}MB of package data`)
|
||||||
|
|
||||||
|
const packages = this.parseDebianPackageListWithProgress(text, onProgress)
|
||||||
|
onProgress(`📊 Parsed ${packages.length} Debian packages`)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
await this.storePackagesWithProgress('debian', packages, onProgress)
|
||||||
|
|
||||||
|
onProgress('✅ Debian packages completed')
|
||||||
|
} catch (error) {
|
||||||
|
onProgress(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and parse Ubuntu packages (simple text parsing)
|
||||||
|
static async fetchUbuntuPackages(): Promise<void> {
|
||||||
|
await this.fetchUbuntuPackagesWithProgress(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch with progress callback
|
||||||
|
static async fetchUbuntuPackagesWithProgress(
|
||||||
|
onProgress: (message: string, progress?: number, total?: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
onProgress('🔄 Fetching Ubuntu packages from official repository...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
onProgress('📡 Downloading package list...')
|
||||||
|
const response = await fetch(this.UBUNTU_PACKAGES_URL, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'RepoHub-Package-Fetcher/1.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Ubuntu packages: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get as text directly (not compressed!)
|
||||||
|
const text = await response.text()
|
||||||
|
onProgress(`📊 Downloaded ${Math.round(text.length / 1024 / 1024)}MB of package data`)
|
||||||
|
|
||||||
|
const packages = this.parseUbuntuPackageListWithProgress(text, onProgress)
|
||||||
|
onProgress(`📊 Parsed ${packages.length} Ubuntu packages`)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
await this.storePackagesWithProgress('ubuntu', packages, onProgress)
|
||||||
|
|
||||||
|
onProgress('✅ Ubuntu packages completed')
|
||||||
|
} catch (error) {
|
||||||
|
onProgress(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Debian package list format
|
||||||
|
private static parseDebianPackageList(text: string): CreatePackageInput[] {
|
||||||
|
return this.parseDebianPackageListWithProgress(text, () => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse with progress callback
|
||||||
|
private static parseDebianPackageListWithProgress(
|
||||||
|
text: string,
|
||||||
|
onProgress: (message: string, progress?: number, total?: number) => void
|
||||||
|
): CreatePackageInput[] {
|
||||||
|
const packages: CreatePackageInput[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
console.log('📋 Analyzing package format...')
|
||||||
|
|
||||||
|
// Find where actual packages start
|
||||||
|
let packageStartIndex = 0
|
||||||
|
let sampleLines = []
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(50, lines.length); i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (line && !line.startsWith('All') && !line.startsWith('Generated') && !line.startsWith('Copyright')) {
|
||||||
|
sampleLines.push(line)
|
||||||
|
if (packageStartIndex === 0 && line.match(/^[a-z0-9]/)) {
|
||||||
|
packageStartIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📋 Sample lines:', sampleLines.slice(0, 5))
|
||||||
|
console.log('📋 Package start index:', packageStartIndex)
|
||||||
|
|
||||||
|
let processedCount = 0
|
||||||
|
const totalLines = lines.length - packageStartIndex
|
||||||
|
|
||||||
|
for (let i = packageStartIndex; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
|
||||||
|
// Skip header lines
|
||||||
|
if (line.startsWith('All') || line.startsWith('Generated') || line.startsWith('Copyright')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try multiple regex patterns
|
||||||
|
let match = null
|
||||||
|
|
||||||
|
// Pattern 1: "package-name (version) description"
|
||||||
|
match = line.match(/^([a-z0-9][a-z0-9+.-]*)\s+\(([^)]+)\)\s*(.+)?$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, name, version, description] = match
|
||||||
|
|
||||||
|
packages.push({
|
||||||
|
id: `debian-${name}`,
|
||||||
|
name: name,
|
||||||
|
description: description?.trim(),
|
||||||
|
version: version.trim(),
|
||||||
|
platform_id: 'debian',
|
||||||
|
type: this.determinePackageType(name, description),
|
||||||
|
repository: 'official',
|
||||||
|
popularity_score: Math.floor(Math.random() * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
processedCount++
|
||||||
|
|
||||||
|
// Send progress every 1000 packages
|
||||||
|
if (processedCount % 1000 === 0) {
|
||||||
|
const progress = Math.round((processedCount / totalLines) * 100)
|
||||||
|
onProgress(`📦 Parsed ${processedCount} packages...`, progress, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(`📦 Parsed ${processedCount} total packages`, 100, 100)
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Ubuntu package list format
|
||||||
|
private static parseUbuntuPackageList(text: string): CreatePackageInput[] {
|
||||||
|
return this.parseUbuntuPackageListWithProgress(text, () => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse with progress callback
|
||||||
|
private static parseUbuntuPackageListWithProgress(
|
||||||
|
text: string,
|
||||||
|
onProgress: (message: string, progress?: number, total?: number) => void
|
||||||
|
): CreatePackageInput[] {
|
||||||
|
const packages: CreatePackageInput[] = []
|
||||||
|
const lines = text.split('\n')
|
||||||
|
|
||||||
|
onProgress('📋 Analyzing Ubuntu package format...')
|
||||||
|
|
||||||
|
// Find where actual packages start
|
||||||
|
let packageStartIndex = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(50, lines.length); i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (line && !line.startsWith('All') && !line.startsWith('Generated') && !line.startsWith('Copyright')) {
|
||||||
|
if (packageStartIndex === 0 && line.match(/^[a-z0-9]/)) {
|
||||||
|
packageStartIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedCount = 0
|
||||||
|
const totalLines = lines.length - packageStartIndex
|
||||||
|
|
||||||
|
for (let i = packageStartIndex; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (!line) continue
|
||||||
|
|
||||||
|
// Skip header lines
|
||||||
|
if (line.startsWith('All') || line.startsWith('Generated') || line.startsWith('Copyright')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Ubuntu pattern: "package-name (version) [repository] description"
|
||||||
|
let match = line.match(/^([a-z0-9][a-z0-9+.-]*)\s+\(([^)]+)\)\s+\[([^\]]+)\]\s*(.+)?$/)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, name, version, repository, description] = match
|
||||||
|
|
||||||
|
packages.push({
|
||||||
|
id: `ubuntu-${name}`,
|
||||||
|
name: name,
|
||||||
|
description: description?.trim(),
|
||||||
|
version: version.trim(),
|
||||||
|
platform_id: 'ubuntu',
|
||||||
|
type: this.determinePackageType(name, description),
|
||||||
|
repository: this.mapUbuntuRepository(repository),
|
||||||
|
popularity_score: Math.floor(Math.random() * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
processedCount++
|
||||||
|
|
||||||
|
// Send progress every 1000 packages
|
||||||
|
if (processedCount % 1000 === 0) {
|
||||||
|
const progress = Math.round((processedCount / totalLines) * 100)
|
||||||
|
onProgress(`📦 Parsed ${processedCount} packages...`, progress, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(`📦 Parsed ${processedCount} total packages`, 100, 100)
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Ubuntu repository to our enum
|
||||||
|
private static mapUbuntuRepository(repo: string): 'official' | 'third-party' {
|
||||||
|
switch (repo.toLowerCase()) {
|
||||||
|
case 'main':
|
||||||
|
case 'restricted':
|
||||||
|
case 'universe':
|
||||||
|
case 'multiverse':
|
||||||
|
return 'official'
|
||||||
|
default:
|
||||||
|
return 'third-party'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if package is GUI or CLI based on name and description
|
||||||
|
private static determinePackageType(name: string, description?: string): 'gui' | 'cli' {
|
||||||
|
const guiKeywords = [
|
||||||
|
'gui', 'gtk', 'qt', 'x11', 'desktop', 'window', 'display',
|
||||||
|
'graphical', 'visual', 'image', 'video', 'audio', 'media',
|
||||||
|
'browser', 'editor', 'viewer', 'player', 'manager',
|
||||||
|
'game', 'games', 'steam', 'wine'
|
||||||
|
]
|
||||||
|
|
||||||
|
const cliKeywords = [
|
||||||
|
'cli', 'command', 'terminal', 'console', 'shell', 'bash',
|
||||||
|
'tool', 'utility', 'daemon', 'service', 'server', 'client',
|
||||||
|
'lib', 'dev', 'debug', 'build', 'compile'
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchText = `${name} ${description || ''}`.toLowerCase()
|
||||||
|
|
||||||
|
for (const keyword of guiKeywords) {
|
||||||
|
if (searchText.includes(keyword)) return 'gui'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const keyword of cliKeywords) {
|
||||||
|
if (searchText.includes(keyword)) return 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'cli'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store packages in database
|
||||||
|
private static async storePackages(platformId: string, packages: CreatePackageInput[]): Promise<void> {
|
||||||
|
await this.storePackagesWithProgress(platformId, packages, () => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store with progress callback
|
||||||
|
private static async storePackagesWithProgress(
|
||||||
|
platformId: string,
|
||||||
|
packages: CreatePackageInput[],
|
||||||
|
onProgress: (message: string, progress?: number, total?: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
onProgress(`💾 Storing ${packages.length} packages for platform ${platformId}...`)
|
||||||
|
|
||||||
|
let added = 0
|
||||||
|
let skipped = 0
|
||||||
|
let processed = 0
|
||||||
|
const batchSize = 50 // Smaller batches for better performance
|
||||||
|
|
||||||
|
// Process in batches
|
||||||
|
for (let i = 0; i < packages.length; i += batchSize) {
|
||||||
|
const batch = packages.slice(i, i + batchSize)
|
||||||
|
|
||||||
|
for (const pkg of batch) {
|
||||||
|
try {
|
||||||
|
const existing = await PackageService.getById(pkg.id)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Skip if version is the same (no need to update)
|
||||||
|
if (existing.version === pkg.version) {
|
||||||
|
skipped++
|
||||||
|
processed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if version changed
|
||||||
|
await PackageService.update(pkg.id, {
|
||||||
|
description: pkg.description,
|
||||||
|
version: pkg.version,
|
||||||
|
type: pkg.type,
|
||||||
|
repository: pkg.repository,
|
||||||
|
popularity_score: pkg.popularity_score
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create new package
|
||||||
|
await PackageService.create(pkg)
|
||||||
|
added++
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error storing package ${pkg.id}:`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send progress every batch
|
||||||
|
if (processed % 100 === 0) {
|
||||||
|
const progress = Math.round((processed / packages.length) * 100)
|
||||||
|
onProgress(`💾 Processed ${processed} packages... (${added} new, ${skipped} skipped)`, progress, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent overwhelming the database
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(`💾 Completed: ${added} new, ${skipped} skipped packages`, 100, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync both Debian and Ubuntu packages
|
||||||
|
static async syncAll(): Promise<void> {
|
||||||
|
console.log('🔄 Starting sync of all Debian-based packages...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchDebianPackages()
|
||||||
|
await this.fetchUbuntuPackages()
|
||||||
|
|
||||||
|
console.log('✅ All Debian-based packages synced successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during sync:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-11
@@ -3,6 +3,7 @@ export interface Platform {
|
|||||||
name: string
|
name: string
|
||||||
packageManager: string
|
packageManager: string
|
||||||
icon: string
|
icon: string
|
||||||
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Package {
|
export interface Package {
|
||||||
@@ -10,24 +11,29 @@ export interface Package {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
version: string
|
version: string
|
||||||
category: string
|
category?: string
|
||||||
license: string
|
license?: string
|
||||||
type: 'gui' | 'cli'
|
type: 'gui' | 'cli'
|
||||||
platform: string
|
platform?: string | Platform
|
||||||
|
platform_id?: string
|
||||||
repository: 'official' | 'third-party'
|
repository: 'official' | 'third-party'
|
||||||
lastUpdated: string
|
lastUpdated?: string
|
||||||
downloads?: number
|
downloads?: number
|
||||||
popularity?: number
|
popularity?: number
|
||||||
tags: string[]
|
popularity_score?: number
|
||||||
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterOptions {
|
export interface FilterOptions {
|
||||||
platforms: string[]
|
platform_id?: string
|
||||||
categories: string[]
|
category_id?: number
|
||||||
licenses: string[]
|
type?: 'gui' | 'cli'
|
||||||
types: ('gui' | 'cli')[]
|
repository?: 'official' | 'third-party'
|
||||||
repositories: ('official' | 'third-party')[]
|
search?: string
|
||||||
searchQuery: string
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
sort_by?: string
|
||||||
|
sort_order?: 'asc' | 'desc'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedPackage extends Package {
|
export interface SelectedPackage extends Package {
|
||||||
|
|||||||
Reference in New Issue
Block a user