Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218// src/plugin/loader/plugin-forge.tsimport type { PluginManifest } from '../types'
/** * Represents a release from a plugin forge (GitHub, GitLab, Codeberg, etc.) */export interface PluginRelease { /** Release version (e.g., "1.5.10") */ version: string /** Release tag name (e.g., "1.5.10" or "v1.5.10") */ tag: string /** Whether this is a prerelease */ prerelease: boolean /** Direct download URLs for assets */ assets: { manifestUrl: string mainJsUrl: string }}
/** * Abstract interface for plugin forges (GitHub, GitLab, Codeberg, etc.) * * A forge is a Git hosting service that provides releases with downloadable assets. * Each forge implementation knows how to: * 1. Parse repository URLs for its platform * 2. Fetch release information via API * 3. Construct download URLs for plugin assets */export interface PluginForge { /** Human-readable name for this forge (e.g., "GitHub") */ readonly name: string
/** * Check if this forge can handle the given URL. * @param url URL to check * @returns true if this forge handles URLs from this domain */ canHandle(url: string): boolean
/** * Parse a repository URL into owner/repo components. * @param url Repository URL (e.g., https://github.com/liamcain/obsidian-calendar-plugin) * @returns Parsed repo info, or null if URL is invalid */ parseRepoUrl(url: string): { owner: string; repo: string } | null
/** * Fetch the latest non-prerelease release for a repository. * @param owner Repository owner * @param repo Repository name * @returns Release info with asset download URLs * @throws If no suitable release is found or API request fails */ fetchLatestRelease(owner: string, repo: string): Promise<PluginRelease>
/** * Download and parse the manifest.json from a release. * @param release Release to download from * @returns Parsed manifest * @throws If download or parsing fails */ downloadManifest(release: PluginRelease): Promise<PluginManifest>
/** * Download the main.js source code from a release. * @param release Release to download from * @returns JavaScript source code * @throws If download fails */ downloadMainJs(release: PluginRelease): Promise<string>}
/** * GitHub implementation of PluginForge. * * Uses the GitHub Releases API to find releases and downloads assets directly. * No authentication required for public repositories. */export class GitHubForge implements PluginForge { readonly name = 'GitHub'
canHandle(url: string): boolean { try { const parsed = new URL(url) return parsed.hostname === 'github.com' || parsed.hostname === 'www.github.com' } catch { return false } }
parseRepoUrl(url: string): { owner: string; repo: string } | null { try { const parsed = new URL(url) if (parsed.hostname !== 'github.com' && parsed.hostname !== 'www.github.com') { return null }
// Path should be /owner/repo or /owner/repo/... const pathParts = parsed.pathname.split('/').filter(Boolean) if (pathParts.length < 2) { return null }
return { owner: pathParts[0], repo: pathParts[1], } } catch { return null } }
async fetchLatestRelease(owner: string, repo: string): Promise<PluginRelease> { // Fetch all releases (GitHub API returns newest first) const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`) if (!response.ok) { if (response.status === 404) { throw new Error(`Repository not found: ${owner}/${repo}`) } throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) }
interface GitHubRelease { tag_name: string prerelease: boolean draft: boolean assets: Array<{ name: string browser_download_url: string }> }
const releases: GitHubRelease[] = await response.json()
// Find the latest non-prerelease, non-draft release with the required assets for (const release of releases) { if (release.prerelease || release.draft) { continue }
const manifestAsset = release.assets.find(a => a.name === 'manifest.json') const mainJsAsset = release.assets.find(a => a.name === 'main.js')
if (manifestAsset && mainJsAsset) { // Extract version from tag (remove leading 'v' if present) const version = release.tag_name.replace(/^v/, '')
return { version, tag: release.tag_name, prerelease: release.prerelease, assets: { manifestUrl: manifestAsset.browser_download_url, mainJsUrl: mainJsAsset.browser_download_url, }, } } }
throw new Error( `No suitable release found for ${owner}/${repo}. ` + `Releases must include manifest.json and main.js assets.` ) }
async downloadManifest(release: PluginRelease): Promise<PluginManifest> { const response = await fetch(release.assets.manifestUrl) if (!response.ok) { throw new Error(`Failed to download manifest: ${response.status}`) }
const manifest: PluginManifest = await response.json() return manifest }
async downloadMainJs(release: PluginRelease): Promise<string> { const response = await fetch(release.assets.mainJsUrl) if (!response.ok) { throw new Error(`Failed to download main.js: ${response.status}`) }
return response.text() }}
/** * Registry of available plugin forges. * * To add support for a new forge (e.g., GitLab, Codeberg): * 1. Implement PluginForge interface * 2. Add instance to FORGES array */const FORGES: PluginForge[] = [ new GitHubForge(),]
/** * Find a forge that can handle the given URL. * @param url Repository or plugin URL * @returns Matching forge, or null if no forge handles this URL */export function findForge(url: string): PluginForge | null { return FORGES.find(forge => forge.canHandle(url)) ?? null}
/** * Check if a URL looks like a forge repository URL (as opposed to a direct plugin URL). * Forge URLs point to the repository root, like https://github.com/owner/repo * Direct URLs point to plugin files, like https://example.com/my-plugin/ */export function isForgeUrl(url: string): boolean { return findForge(url) !== null}