Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
| Nix Flake | `flake.nix` |
| Docker | `Dockerfile` |
| Curl | `install.sh` |
| Iex (Windows) | `install.ps1` |

## Architecture

Expand All @@ -32,7 +33,7 @@ flowchart LR
User -->|selects channels| UI[Web UI]
UI -->|POST /generate| API[drb99 API]
API -->|JSON response| Files[Generated files]
Files --> npm & GitHub & AUR & Nix & Docker & Curl
Files --> npm & GitHub & AUR & Nix & Docker & Curl & Iex
```

## Example request
Expand Down
37 changes: 17 additions & 20 deletions generator/generator.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package generator

import (
"maps"

service "github.com/h3yng/drb99/services"
)

Expand All @@ -18,46 +20,36 @@ func (g *Generator) Generate(cfg service.WrapperConfig) (map[string]string, erro
if err != nil {
return nil, err
}
for name, content := range npmFiles {
files[name] = content
}
maps.Copy(files, npmFiles)
}

if cfg.Features.GoReleaser {
goreleaserFiles, err := g.generateGoReleaser(cfg)
if err != nil {
return nil, err
}
for name, content := range goreleaserFiles {
files[name] = content
}
maps.Copy(files, goreleaserFiles)
}

if cfg.Features.GithubActions {
githubActionsFiles := g.generateGithubActions(cfg)
for name, content := range githubActionsFiles {
files[name] = content
}
maps.Copy(files, githubActionsFiles)
}

if cfg.Features.AUR {
aurFiles, err := g.generateAUR(cfg)
if err != nil {
return nil, err
}
for name, content := range aurFiles {
files[name] = content
}
maps.Copy(files, aurFiles)
}

if cfg.Features.NixFlake {
nixFlakeFiles, err := g.generateNixFlake(cfg)
if err != nil {
return nil, err
}
for name, content := range nixFlakeFiles {
files[name] = content
}
maps.Copy(files, nixFlakeFiles)
}

if cfg.Features.DockerContainer {
Expand All @@ -66,9 +58,7 @@ func (g *Generator) Generate(cfg service.WrapperConfig) (map[string]string, erro
return nil, err
}

for name, content := range dockerFiles {
files[name] = content
}
maps.Copy(files, dockerFiles)
}

if cfg.Features.Curl {
Expand All @@ -77,9 +67,16 @@ func (g *Generator) Generate(cfg service.WrapperConfig) (map[string]string, erro
return nil, err
}

for name, content := range curlFiles {
files[name] = content
maps.Copy(files, curlFiles)
}

if cfg.Features.Iex {
iexFiles, err := g.generateIex(cfg)
if err != nil {
return nil, err
}

maps.Copy(files, iexFiles)
}

return files, nil
Expand Down
275 changes: 275 additions & 0 deletions generator/iex_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package generator

import service "github.com/h3yng/drb99/services"

func (g *Generator) generateIex(cfg service.WrapperConfig) (map[string]string, error) {
installPs1, err := renderTemplate(iexInstallTemplate, cfg)
if err != nil {
return nil, err
}

return map[string]string{
"install.ps1": installPs1,
}, nil
}

const iexInstallTemplate = `# Generated by drb99 — iex installer for {{ .BinaryName }}
# https://github.com/h3yng/drb99
# Repo: {{ .RepoURL }} Version: {{ .Version }}
# Usage:
# iex (irm https://raw.githubusercontent.com/{{ .Owner }}/{{ .Repo }}/main/install.ps1)
# iex "& { $(irm https://raw.githubusercontent.com/{{ .Owner }}/{{ .Repo }}/main/install.ps1) } -Version v1.2.3 -InstallDir $env:USERPROFILE\bin"
# Invoke-WebRequest -Uri https://github.com/{{ .Owner }}/{{ .Repo }}/releases/download/{{ .Version }}/install.ps1 -OutFile install.ps1; powershell -ExecutionPolicy Bypass -File install.ps1
# powershell -ExecutionPolicy Bypass -File install.ps1 -InstallDir "C:\tools\bin" -Version {{ .Version }}
# # custom URL install (manual mode) — URL is baked into install.ps1 per-platform

param(
[string]$Version = {{ printf "%q" .Version }},
[string]$InstallDir = "",
[switch]$Help
)

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"

$BinaryName = {{ printf "%q" .BinaryName }}
$Owner = {{ printf "%q" .Owner }}
$Repo = {{ printf "%q" .Repo }}
$RepoURL = {{ printf "%q" .RepoURL }}

if ($Help) {
@"
Usage: install.ps1 [OPTIONS]

Options:
-Version <VER> Version to install (default: {{ .Version }})
-InstallDir <DIR> Install directory (default: $env:USERPROFILE\bin or $env:LOCALAPPDATA\{{ .BinaryName }}\bin)
-Help Show this help

Env:
BINDIR / INSTALL_DIR Same as -InstallDir
VERSION Same as -Version

Examples:
iex (irm https://raw.githubusercontent.com/$Owner/$Repo/main/install.ps1)
.\install.ps1 -InstallDir C:\tools\bin -Version v1.2.3
"@
exit 0
}

# allow env overrides
if (-not $Version -or $Version -eq "") {
if ($env:VERSION) { $Version = $env:VERSION }
elseif ($env:BINDIR -or $env:INSTALL_DIR) { }
}
if (-not $InstallDir -or $InstallDir -eq "") {
if ($env:BINDIR) { $InstallDir = $env:BINDIR }
elseif ($env:INSTALL_DIR) { $InstallDir = $env:INSTALL_DIR }
}

if (-not $Version -or $Version -eq "") {
Write-Error 'version is empty (pass -Version or set $env:VERSION)'
exit 1
}

function Get-OsArchSuffix {
$os = "windows"
$arch = $env:PROCESSOR_ARCHITECTURE
if (-not $arch) { $arch = "AMD64" }
$arch = $arch.ToLower()
switch ($arch) {
"amd64" { $arch = "amd64" }
"x86_64" { $arch = "amd64" }
"x64" { $arch = "amd64" }
"arm64" { $arch = "arm64" }
"aarch64" { $arch = "arm64" }
default { $arch = "amd64" }
}
# map to drb99 suffix convention (windows_amd64)
switch ("${os}_${arch}") {
"windows_amd64" { return "windows_amd64" }
"windows_arm64" { return "windows_arm64" }
default { return "windows_amd64" }
}
}

$Suffix = Get-OsArchSuffix

# --- drb99: platform -> asset URL(s) (supports custom naming via asset_urls) ---
# Prefill note: POST /api/v1/prefill returns latest assets; frontend maps them to platforms
# so you can pass custom URLs via asset_urls in manual mode.
$AssetMap = @{}
{{- range .Platforms }}
# {{ .InputKey }} ({{ .NodeKey }}) -> {{ .BinaryFile }} [{{ .Archive }}]
$AssetMap[{{ printf "%q" .GoSuffix }}] = @{
Urls = @({{ range $i, $u := .URLs }}{{ if $i }}, {{ end }}{{ printf "%q" $u }}{{ end }})
Archive = {{ printf "%q" .BinaryFile }}
ArchiveType = {{ printf "%q" .Archive }}
}
{{- end }}

$Selected = $null
if ($AssetMap.ContainsKey($Suffix)) {
$Selected = $AssetMap[$Suffix]
} else {
# fallback: try windows_amd64 if arch detection produced different suffix
if ($AssetMap.ContainsKey("windows_amd64")) {
$Selected = $AssetMap["windows_amd64"]
$Suffix = "windows_amd64"
} else {
Write-Error "unsupported platform suffix: $Suffix (available: $($AssetMap.Keys -join ', '))"
exit 1
}
}

$Urls = $Selected.Urls
$Archive = $Selected.Archive
$ArchiveType = $Selected.ArchiveType

# backwards compat: if no URLs baked, construct GitHub URL
if (-not $Urls -or $Urls.Count -eq 0 -or [string]::IsNullOrWhiteSpace($Urls[0])) {
$Archive = "${BinaryName}_${Version}_${Suffix}.zip"
$Urls = @("https://github.com/$Owner/$Repo/releases/download/$Version/$Archive")
$ArchiveType = "zip"
}

$AllUrls = $Urls

# resolve InstallDir
if (-not $InstallDir -or $InstallDir -eq "") {
$candidate1 = Join-Path $env:USERPROFILE "bin"
$candidate2 = Join-Path $env:LOCALAPPDATA "{{ .BinaryName }}\bin"
if ($candidate1 -and (Test-Path $candidate1 -IsValid)) {
$InstallDir = $candidate1
} elseif ($candidate2) {
$InstallDir = $candidate2
} else {
$InstallDir = $candidate1
}
}

Write-Host "[drb99] Installing $BinaryName $Version for $Suffix"
Write-Host "[drb99] InstallDir: $InstallDir"

# ensure TLS 1.2
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}

$TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("drb99-" + [Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Force -Path $TempDir | Out-Null
$ArchivePath = ""
$downloadSuccess = $false

foreach ($Url in $AllUrls) {
$fileName = Split-Path $Url -Leaf
if (-not $fileName -or $fileName -eq "/") { $fileName = $Archive }
# sanitize filename
$fileName = $fileName.Split("?")[0]
$ArchivePath = Join-Path $TempDir $fileName
Write-Host "[drb99] Downloading $Url"
$ok = $false
try {
# prefer Invoke-WebRequest, fallback to curl.exe / wget
if (Get-Command Invoke-WebRequest -ErrorAction SilentlyContinue) {
Invoke-WebRequest -Uri $Url -OutFile $ArchivePath -UseBasicParsing -ErrorAction Stop
$ok = $true
} elseif (Get-Command curl.exe -ErrorAction SilentlyContinue) {
& curl.exe -fsSL $Url -o $ArchivePath
if ($LASTEXITCODE -eq 0) { $ok = $true }
} elseif (Get-Command wget -ErrorAction SilentlyContinue) {
& wget -qO $ArchivePath $Url
if ($LASTEXITCODE -eq 0) { $ok = $true }
} else {
# try .NET WebClient
(New-Object Net.WebClient).DownloadFile($Url, $ArchivePath)
$ok = $true
}
} catch {
Write-Host "[drb99] Download failed from $Url : $($_.Exception.Message)" -ForegroundColor Yellow
$ok = $false
}
if ($ok -and (Test-Path $ArchivePath) -and ((Get-Item $ArchivePath).Length -gt 0)) {
$downloadSuccess = $true
break
}
if (Test-Path $ArchivePath) { Remove-Item $ArchivePath -Force -ErrorAction SilentlyContinue }
Write-Host "[drb99] Trying next URL..." -ForegroundColor Yellow
}

if (-not $downloadSuccess -or -not (Test-Path $ArchivePath) -or ((Get-Item $ArchivePath).Length -eq 0)) {
Write-Error "all downloads failed. Tried: $($AllUrls -join ', ')"
exit 1
}

# detect archive type
if (-not $ArchiveType) {
if ($ArchivePath -like "*.zip") { $ArchiveType = "zip" }
elseif ($ArchivePath -like "*.tar.gz") { $ArchiveType = "tar.gz" }
else { $ArchiveType = "zip" }
}

Write-Host "[drb99] Extracting $(Split-Path $ArchivePath -Leaf) [$ArchiveType]"

$ExtractDir = Join-Path $TempDir "extract"
New-Item -ItemType Directory -Force -Path $ExtractDir | Out-Null

if ($ArchiveType -eq "zip") {
try {
Expand-Archive -Path $ArchivePath -DestinationPath $ExtractDir -Force
} catch {
# fallback to .NET
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($ArchivePath, $ExtractDir)
}
} else {
# tar.gz on Windows (requires tar)
if (Get-Command tar -ErrorAction SilentlyContinue) {
& tar -xzf $ArchivePath -C $ExtractDir
} else {
Write-Error "tar is required for tar.gz archive"
exit 1
}
}

# find binary
$BinSrc = $null
$expected = Join-Path $ExtractDir "$BinaryName.exe"
if (Test-Path $expected) {
$BinSrc = $expected
} else {
$found = Get-ChildItem -Path $ExtractDir -Recurse -Filter "$BinaryName.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($found) { $BinSrc = $found.FullName }
else {
$found = Get-ChildItem -Path $ExtractDir -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
if ($found) { $BinSrc = $found.FullName }
}
}

if (-not $BinSrc -or -not (Test-Path $BinSrc)) {
Write-Error "could not find binary $BinaryName.exe in archive"
Get-ChildItem -Path $ExtractDir -Recurse | ForEach-Object { Write-Host $_.FullName }
exit 1
}

New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
$BinDst = Join-Path $InstallDir "$BinaryName.exe"

try {
Move-Item -Path $BinSrc -Destination $BinDst -Force
} catch {
Write-Host "[drb99] $BinDst is not writable, try running as Administrator" -ForegroundColor Yellow
throw
}

Write-Host "[drb99] Installed $BinDst" -ForegroundColor Green

# PATH hint
if (-not ($env:PATH -split ";" | Where-Object { $_ -eq $InstallDir })) {
Write-Host ("[drb99] NOTE: $InstallDir is not in PATH. Add it: " + '$env:PATH += "' + $InstallDir + '"') -ForegroundColor Yellow
Write-Host ('[drb99] To persist: setx PATH "%PATH%;' + $InstallDir + '"')
}

Write-Host "[drb99] Run: $BinaryName --help"

# cleanup
Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
`
Loading
Loading