archy/neode-ui/src/utils/githubAppInfo.ts
Dorian 6fee6befed refactor: update dependencies and remove unused code
- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`.
- Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27.
- Removed the `backup.rs` file as it is no longer needed.
- Introduced tests for configuration and credential management.
- Enhanced the `identity` module to generate W3C compliant DID documents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:19:30 +00:00

183 lines
6.1 KiB
TypeScript

// Utility to fetch app information from GitHub repositories
// Used to get icons, descriptions, and other metadata for dummy apps
export interface GitHubAppInfo {
icon?: string
description?: string
readme?: string
homepage?: string
}
/**
* Fetch app information from GitHub repository
* @param repoUrl GitHub repository URL (e.g., https://github.com/start9labs/bitcoin)
* @param appId App ID to help find the correct repository
*/
export async function fetchGitHubAppInfo(repoUrl: string, appId: string): Promise<GitHubAppInfo> {
try {
// Extract owner and repo from URL
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/)
if (!match) {
if (import.meta.env.DEV) console.warn(`[GitHub] Invalid repo URL: ${repoUrl}`)
return {}
}
const [, owner, repo] = match
// Try to find Start9 wrapper repo first (e.g., bitcoin-startos)
const start9RepoName = `${appId}-startos`
let targetOwner = owner
let targetRepo = repo
// If the repo URL doesn't match the expected pattern, try Start9Labs
if (repo && !repo.includes('startos') && !repo.includes('start9')) {
// Try Start9Labs wrapper repo
try {
const start9RepoUrl = `https://api.github.com/repos/Start9Labs/${start9RepoName}`
const start9Response = await fetch(start9RepoUrl)
if (start9Response.ok) {
targetOwner = 'Start9Labs'
targetRepo = start9RepoName
}
} catch (e) {
if (import.meta.env.DEV) console.warn('Start9 repo lookup failed, falling back to original repo', e)
}
}
// Fetch repository info
const repoApiUrl = `https://api.github.com/repos/${targetOwner}/${targetRepo}`
const repoResponse = await fetch(repoApiUrl)
if (!repoResponse.ok) {
if (import.meta.env.DEV) console.warn(`[GitHub] Failed to fetch repo ${targetOwner}/${targetRepo}: ${repoResponse.status}`)
return {}
}
const repoData = await repoResponse.json()
// Fetch README
let readme = ''
try {
const readmeResponse = await fetch(`https://api.github.com/repos/${targetOwner}/${targetRepo}/readme`)
if (readmeResponse.ok) {
const readmeData = await readmeResponse.json()
readme = atob(readmeData.content) // Base64 decode
}
} catch (e) {
if (import.meta.env.DEV) console.warn(`[GitHub] Failed to fetch README for ${targetOwner}/${targetRepo}`)
}
// Try to find icon in repository
// Common locations: icon.png, icon.svg, assets/icon.png, etc.
let icon: string | undefined
const iconPaths = [
'icon.png',
'icon.svg',
'assets/icon.png',
'assets/icon.svg',
'icon/icon.png',
'icon/icon.svg'
]
for (const iconPath of iconPaths) {
try {
const iconResponse = await fetch(`https://api.github.com/repos/${targetOwner}/${targetRepo}/contents/${iconPath}`)
if (iconResponse.ok) {
const iconData = await iconResponse.json()
if (iconData.download_url) {
icon = iconData.download_url
break
}
}
} catch (e) {
if (import.meta.env.DEV) console.warn('Icon path lookup failed, trying next path', e)
}
}
// If no icon found, try to get from releases/assets
if (!icon) {
try {
const releasesResponse = await fetch(`https://api.github.com/repos/${targetOwner}/${targetRepo}/releases/latest`)
if (releasesResponse.ok) {
const releasesData = await releasesResponse.json()
const asset = releasesData.assets?.find((a: { name: string; browser_download_url: string }) =>
a.name.includes('icon') || a.name.includes('logo')
)
if (asset) {
icon = asset.browser_download_url
}
}
} catch (e) {
if (import.meta.env.DEV) console.warn('No icon from releases', e)
}
}
// If still no icon, try raw GitHub content URLs for common icon names
if (!icon) {
const rawIconPaths = [
`https://raw.githubusercontent.com/${targetOwner}/${targetRepo}/main/icon.png`,
`https://raw.githubusercontent.com/${targetOwner}/${targetRepo}/main/icon.svg`,
`https://raw.githubusercontent.com/${targetOwner}/${targetRepo}/master/icon.png`,
`https://raw.githubusercontent.com/${targetOwner}/${targetRepo}/master/icon.svg`,
`https://raw.githubusercontent.com/${targetOwner}/${targetRepo}/main/assets/icon.png`,
`https://raw.githubusercontent.com/${targetOwner}/${targetRepo}/main/assets/icon.svg`,
]
// Test each URL
for (const iconUrl of rawIconPaths) {
try {
const testResponse = await fetch(iconUrl, { method: 'HEAD' })
if (testResponse.ok) {
icon = iconUrl
break
}
} catch (e) {
if (import.meta.env.DEV) console.warn('Raw icon URL failed, trying next URL', e)
}
}
}
return {
icon,
description: repoData.description || '',
readme,
homepage: repoData.homepage || repoData.html_url
}
} catch (error) {
if (import.meta.env.DEV) console.error(`[GitHub] Error fetching app info for ${repoUrl}:`, error)
return {}
}
}
/**
* Batch fetch app info for multiple apps
*/
export async function fetchMultipleAppInfo(
apps: Array<{ id: string; 'wrapper-repo': string }>
): Promise<Record<string, GitHubAppInfo>> {
const results: Record<string, GitHubAppInfo> = {}
// Fetch in parallel with rate limiting (max 5 concurrent)
const batchSize = 5
for (let i = 0; i < apps.length; i += batchSize) {
const batch = apps.slice(i, i + batchSize)
const batchPromises = batch.map(async (app) => {
const info = await fetchGitHubAppInfo(app['wrapper-repo'], app.id)
return { id: app.id, info }
})
const batchResults = await Promise.all(batchPromises)
batchResults.forEach(({ id, info }) => {
results[id] = info
})
// Rate limit: wait 1 second between batches
if (i + batchSize < apps.length) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
return results
}