From 847846da3509c0264c21ad890d25a33b2bdda5c3 Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:20:54 -0600 Subject: [PATCH 1/9] feat(bore): add bore tunnel provider - Add bore provider for free tunnel access (no signup required) - Provider downloads binary from GitHub releases - Supports macOS (arm64/x86), Linux (arm64/x86), Windows - Auto-extracts tar.gz archives - Parse URL from logs with regex bore.pub:\d+ --- internal/config/tunnel.go | 1 + internal/process/manager.go | 2 + internal/providers/bore/bore.go | 133 +++++++++++++++++++++++++++ internal/providers/bore/installer.go | 107 +++++++++++++++++++++ internal/providers/downloader.go | 54 +++++++++++ 5 files changed, 297 insertions(+) create mode 100644 internal/providers/bore/bore.go create mode 100644 internal/providers/bore/installer.go diff --git a/internal/config/tunnel.go b/internal/config/tunnel.go index db9be31..27f70ad 100644 --- a/internal/config/tunnel.go +++ b/internal/config/tunnel.go @@ -8,6 +8,7 @@ const ( ProviderLocalhostRun Provider = "localhostrun" ProviderServeo Provider = "serveo" ProviderPinggy Provider = "pinggy" + ProviderBore Provider = "bore" ) type TunnelState string diff --git a/internal/process/manager.go b/internal/process/manager.go index 29c2193..0986784 100644 --- a/internal/process/manager.go +++ b/internal/process/manager.go @@ -9,6 +9,7 @@ import ( "github.com/sthbryan/ftm/internal/config" "github.com/sthbryan/ftm/internal/providers" + "github.com/sthbryan/ftm/internal/providers/bore" "github.com/sthbryan/ftm/internal/providers/cloudflared" "github.com/sthbryan/ftm/internal/providers/pinggy" "github.com/sthbryan/ftm/internal/providers/ssh" @@ -85,6 +86,7 @@ func NewManager() *Manager { config.ProviderLocalhostRun: ssh.NewLocalhostRun(), config.ProviderServeo: ssh.NewServeo(), config.ProviderPinggy: pinggy.New(), + config.ProviderBore: bore.New(), }, } } diff --git a/internal/providers/bore/bore.go b/internal/providers/bore/bore.go new file mode 100644 index 0000000..a4411aa --- /dev/null +++ b/internal/providers/bore/bore.go @@ -0,0 +1,133 @@ +package bore + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/sthbryan/ftm/internal/config" + "github.com/sthbryan/ftm/internal/providers" +) + +type BoreProvider struct { + installer *Installer +} + +func New() providers.Provider { + configDir, _ := os.UserHomeDir() + if configDir == "" { + configDir = "." + } + baseDir := filepath.Join(configDir, ".config", "foundry-tunnel", "bin") + + return &BoreProvider{ + installer: NewInstaller(baseDir), + } +} + +func (p *BoreProvider) Name() string { + return "bore" +} + +func (p *BoreProvider) BinaryName() string { + if runtime.GOOS == "windows" { + return "bore.exe" + } + return "bore" +} + +func (p *BoreProvider) IsInstalled() bool { + if _, err := exec.LookPath(p.BinaryName()); err == nil { + return true + } + return p.installer.IsInstalled() +} + +func (p *BoreProvider) Install(progress chan<- providers.DownloadProgress) error { + return p.installer.Install(progress) +} + +func (p *BoreProvider) FindBinary() string { + if path, err := exec.LookPath(p.BinaryName()); err == nil { + return path + } + + if p.installer.IsInstalled() { + return p.installer.BoreBin() + } + + home, _ := os.UserHomeDir() + candidates := []string{ + filepath.Join(home, ".config", "foundry-tunnel", "bin", p.BinaryName()), + "/usr/local/bin/" + p.BinaryName(), + "/usr/bin/" + p.BinaryName(), + } + + if runtime.GOOS == "windows" { + candidates = []string{ + p.BinaryName(), + filepath.Join(os.Getenv("ProgramFiles"), "bore", p.BinaryName()), + } + } + + for _, path := range candidates { + if _, err := os.Stat(path); err == nil { + return path + } + } + + return "" +} + +func (p *BoreProvider) Start(ctx context.Context, tunnel config.TunnelConfig, logWriter io.Writer) (*providers.Process, error) { + binary := p.FindBinary() + if binary == "" { + if err := p.installer.Install(nil); err != nil { + return nil, fmt.Errorf("failed to install bore: %w", err) + } + binary = p.installer.BoreBin() + } + + ctx, cancel := context.WithCancel(ctx) + + args := []string{ + "local", + fmt.Sprintf("%d", tunnel.LocalPort), + "--to", "bore.pub", + } + + cmd := exec.CommandContext(ctx, binary, args...) + cmd.Stdout = logWriter + cmd.Stderr = logWriter + + if err := cmd.Start(); err != nil { + cancel() + return nil, fmt.Errorf("failed to start bore: %w", err) + } + + return &providers.Process{ + Cancel: cancel, + }, nil +} + +var boreURLRegex = regexp.MustCompile(`bore\.pub:\d+`) + +func (p *BoreProvider) ParseURL(line string) string { + matches := boreURLRegex.FindString(line) + if matches != "" { + return matches + } + return "" +} + +func (p *BoreProvider) IsReady(line string) bool { + lineLower := strings.ToLower(line) + return strings.Contains(lineLower, "bore.pub") && + (strings.Contains(lineLower, "listening") || strings.Contains(lineLower, "port")) +} diff --git a/internal/providers/bore/installer.go b/internal/providers/bore/installer.go new file mode 100644 index 0000000..918fc03 --- /dev/null +++ b/internal/providers/bore/installer.go @@ -0,0 +1,107 @@ +package bore + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/sthbryan/ftm/internal/providers" +) + +type Installer struct { + BaseDir string +} + +func NewInstaller(baseDir string) *Installer { + return &Installer{BaseDir: baseDir} +} + +func (i *Installer) BoreBin() string { + if runtime.GOOS == "windows" { + return filepath.Join(i.BaseDir, "bore.exe") + } + return filepath.Join(i.BaseDir, "bore") +} + +func (i *Installer) IsInstalled() bool { + if _, err := os.Stat(i.BoreBin()); err == nil { + return true + } + return false +} + +func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { + if err := os.MkdirAll(i.BaseDir, 0755); err != nil { + return fmt.Errorf("failed to create base dir: %w", err) + } + + if i.IsInstalled() { + return nil + } + + platform := runtime.GOOS + arch := runtime.GOARCH + + var url string + var filename string + + switch platform { + case "darwin": + if arch == "arm64" { + filename = "bore-v0.6.0-aarch64-apple-darwin.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + } else { + filename = "bore-v0.6.0-x86_64-apple-darwin.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + } + case "linux": + if arch == "arm64" { + filename = "bore-v0.6.0-aarch64-unknown-linux-musl.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + } else { + filename = "bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + } + case "windows": + filename = "bore-v0.6.0-x86_64-pc-windows-msvc.zip" + url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + default: + return fmt.Errorf("unsupported platform: %s", platform) + } + + destArchive := filepath.Join(i.BaseDir, filename) + + if progress != nil { + progress <- providers.DownloadProgress{ + Percent: 10, + Current: 0, + Total: 100, + Name: "bore", + } + } + + if err := providers.DownloadWithProgress(url, destArchive, progress, "bore"); err != nil { + return fmt.Errorf("failed to download bore: %w", err) + } + + defer os.Remove(destArchive) + + if err := providers.ExtractTarGz(destArchive, i.BaseDir); err != nil { + return fmt.Errorf("failed to extract bore: %w", err) + } + + if _, err := os.Stat(i.BoreBin()); err != nil { + return fmt.Errorf("binary not found after install: %w", err) + } + + if progress != nil { + progress <- providers.DownloadProgress{ + Percent: 100, + Done: true, + Name: "bore", + } + } + + return nil +} diff --git a/internal/providers/downloader.go b/internal/providers/downloader.go index 8bac83c..a632875 100644 --- a/internal/providers/downloader.go +++ b/internal/providers/downloader.go @@ -1,6 +1,8 @@ package providers import ( + "archive/tar" + "compress/gzip" "fmt" "io" "net/http" @@ -92,3 +94,55 @@ func downloadWithProgress(url, dest string, progress chan<- DownloadProgress, na return nil } + +func DownloadWithProgress(url, dest string, progress chan<- DownloadProgress, name string) error { + return downloadWithProgress(url, dest, progress, name) +} + +func ExtractTarGz(src, destDir string) error { + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + gzr, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + if header.Typeflag == tar.TypeReg { + name := filepath.Base(header.Name) + if name == "bore" || name == "bore.exe" || name == "cloudflared" || name == "cloudflared.exe" { + destPath := filepath.Join(destDir, name) + out, err := os.Create(destPath) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, tr); err != nil { + return err + } + + if err := os.Chmod(destPath, 0755); err != nil { + return err + } + return nil + } + } + } + + return fmt.Errorf("executable not found in archive") +} From fd706b12c3b8efff960c13d46fd465a84d7de728 Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:21:00 -0600 Subject: [PATCH 2/9] feat(bore): add bore to provider selector UI - Add bore to form provider navigation - Add i18n translations for bore provider name --- internal/app/form.go | 1 + internal/i18n/helpers.go | 2 ++ internal/i18n/locales/en.yaml | 1 + internal/i18n/locales/es.yaml | 1 + 4 files changed, 5 insertions(+) diff --git a/internal/app/form.go b/internal/app/form.go index 2942324..7c3695a 100644 --- a/internal/app/form.go +++ b/internal/app/form.go @@ -55,6 +55,7 @@ func (m *Model) handleProviderNav(dir string) { config.ProviderLocalhostRun, config.ProviderServeo, config.ProviderPinggy, + config.ProviderBore, } current := config.Provider(m.FormValues.Provider) diff --git a/internal/i18n/helpers.go b/internal/i18n/helpers.go index 80fca84..dd9000b 100644 --- a/internal/i18n/helpers.go +++ b/internal/i18n/helpers.go @@ -35,6 +35,8 @@ func ProviderText(provider string) string { return T("provider_tunnelmole") case "localhostrun": return T("provider_localhostrun") + case "bore": + return T("provider_bore") default: return provider } diff --git a/internal/i18n/locales/en.yaml b/internal/i18n/locales/en.yaml index 6e212e5..74e81e6 100644 --- a/internal/i18n/locales/en.yaml +++ b/internal/i18n/locales/en.yaml @@ -45,6 +45,7 @@ provider_serveo: "Serveo" provider_cloudflared: "Cloudflared" provider_tunnelmole: "Tunnelmole" provider_localhostrun: "LocalhostRun" +provider_bore: "bore" # Actions connect: "Connect" diff --git a/internal/i18n/locales/es.yaml b/internal/i18n/locales/es.yaml index 3082eab..ea0fe7a 100644 --- a/internal/i18n/locales/es.yaml +++ b/internal/i18n/locales/es.yaml @@ -45,6 +45,7 @@ provider_serveo: "Serveo" provider_cloudflared: "Cloudflared" provider_tunnelmole: "Tunnelmole" provider_localhostrun: "LocalhostRun" +provider_bore: "bore" # Actions connect: "Conectar" From a376adcb75ae2b2db3461f05c0ae2c5197acfc3e Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:41:00 -0600 Subject: [PATCH 3/9] refactor(bore): enhance archive extraction functions to support binary name parameter --- internal/providers/downloader.go | 86 ++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/internal/providers/downloader.go b/internal/providers/downloader.go index a632875..e3a1bcd 100644 --- a/internal/providers/downloader.go +++ b/internal/providers/downloader.go @@ -2,12 +2,14 @@ package providers import ( "archive/tar" + "archive/zip" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" + "strings" ) type DownloadProgress struct { @@ -99,7 +101,7 @@ func DownloadWithProgress(url, dest string, progress chan<- DownloadProgress, na return downloadWithProgress(url, dest, progress, name) } -func ExtractTarGz(src, destDir string) error { +func ExtractTarGz(src, destDir, binaryName string) error { file, err := os.Open(src) if err != nil { return err @@ -124,25 +126,77 @@ func ExtractTarGz(src, destDir string) error { if header.Typeflag == tar.TypeReg { name := filepath.Base(header.Name) - if name == "bore" || name == "bore.exe" || name == "cloudflared" || name == "cloudflared.exe" { - destPath := filepath.Join(destDir, name) - out, err := os.Create(destPath) - if err != nil { - return err - } - defer out.Close() + if name == binaryName || name == binaryName+".exe" { + return extractFileToDir(tr, destDir, name) + } + } + } - if _, err := io.Copy(out, tr); err != nil { - return err - } + return fmt.Errorf("executable %s not found in archive", binaryName) +} - if err := os.Chmod(destPath, 0755); err != nil { - return err - } - return nil +func ExtractZip(src, destDir, binaryName string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + if !f.Mode().IsRegular() { + continue + } + name := filepath.Base(f.Name) + if name == binaryName || name == binaryName+".exe" { + rc, err := f.Open() + if err != nil { + return err } + defer rc.Close() + return extractFileToDir(rc, destDir, name) } } - return fmt.Errorf("executable not found in archive") + return fmt.Errorf("executable %s not found in archive", binaryName) +} + +func extractFileToDir(src io.Reader, destDir, filename string) error { + destPath := filepath.Join(destDir, filename) + tmpPath := destPath + ".tmp" + + out, err := os.Create(tmpPath) + if err != nil { + return err + } + + _, err = io.Copy(out, src) + out.Close() + + if err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to write %s: %w", filename, err) + } + + if err := os.Chmod(tmpPath, 0755); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to chmod %s: %w", filename, err) + } + + if err := os.Rename(tmpPath, destPath); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to rename %s: %w", filename, err) + } + + return nil +} + +func ExtractArchive(src, destDir, binaryName string) error { + src = strings.ToLower(src) + if strings.HasSuffix(src, ".zip") { + return ExtractZip(src, destDir, binaryName) + } + if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") { + return ExtractTarGz(src, destDir, binaryName) + } + return fmt.Errorf("unsupported archive format: %s", src) } From f990b0b8d6f944f700bcf0e4cc3719828d458b02 Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:41:20 -0600 Subject: [PATCH 4/9] fix(bore): update installation logic to support architecture-specific binaries for macOS, Linux, and Window --- internal/providers/bore/installer.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/internal/providers/bore/installer.go b/internal/providers/bore/installer.go index 918fc03..7b66ba5 100644 --- a/internal/providers/bore/installer.go +++ b/internal/providers/bore/installer.go @@ -48,24 +48,34 @@ func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { switch platform { case "darwin": - if arch == "arm64" { + switch arch { + case "arm64": filename = "bore-v0.6.0-aarch64-apple-darwin.tar.gz" url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename - } else { + case "amd64": filename = "bore-v0.6.0-x86_64-apple-darwin.tar.gz" url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + default: + return fmt.Errorf("unsupported architecture for macOS: %s (supported: arm64, amd64)", arch) } case "linux": - if arch == "arm64" { + switch arch { + case "arm64": filename = "bore-v0.6.0-aarch64-unknown-linux-musl.tar.gz" url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename - } else { + case "amd64": filename = "bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz" url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + default: + return fmt.Errorf("unsupported architecture for Linux: %s (supported: arm64, amd64)", arch) } case "windows": - filename = "bore-v0.6.0-x86_64-pc-windows-msvc.zip" - url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + if arch == "amd64" { + filename = "bore-v0.6.0-x86_64-pc-windows-msvc.zip" + url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + } else { + return fmt.Errorf("unsupported architecture for Windows: %s (supported: amd64)", arch) + } default: return fmt.Errorf("unsupported platform: %s", platform) } @@ -87,7 +97,7 @@ func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { defer os.Remove(destArchive) - if err := providers.ExtractTarGz(destArchive, i.BaseDir); err != nil { + if err := providers.ExtractArchive(destArchive, i.BaseDir, "bore"); err != nil { return fmt.Errorf("failed to extract bore: %w", err) } From f4edbec277dcbad7216c475bc75ef8c129961014 Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:43:56 -0600 Subject: [PATCH 5/9] refactor(cloudflared): use DownloadWithProgress and ExtractArchive --- internal/providers/cloudflared/installer.go | 48 +++------------------ 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/internal/providers/cloudflared/installer.go b/internal/providers/cloudflared/installer.go index f3fdd1c..9305365 100644 --- a/internal/providers/cloudflared/installer.go +++ b/internal/providers/cloudflared/installer.go @@ -3,7 +3,6 @@ package cloudflared import ( "fmt" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -12,15 +11,11 @@ import ( ) type Installer struct { - BaseDir string - downloader *providers.BaseDownloader + BaseDir string } func NewInstaller(baseDir string) *Installer { - return &Installer{ - BaseDir: baseDir, - downloader: providers.NewBaseDownloader(), - } + return &Installer{BaseDir: baseDir} } func (i *Installer) CloudflaredBin() string { @@ -61,18 +56,17 @@ func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { } if strings.HasSuffix(url, ".tgz") { - tmpFile := binPath + ".tgz" - if err := i.downloader.Download(url, tmpFile, progress, "cloudflared"); err != nil { - os.Remove(tmpFile) + destArchive := binPath + ".tgz" + if err := providers.DownloadWithProgress(url, destArchive, progress, "cloudflared"); err != nil { return fmt.Errorf("download failed: %w", err) } - defer os.Remove(tmpFile) + defer os.Remove(destArchive) - if err := i.extractTgz(tmpFile, binPath); err != nil { + if err := providers.ExtractArchive(destArchive, i.BaseDir, "cloudflared"); err != nil { return fmt.Errorf("extract failed: %w", err) } } else { - if err := i.downloader.Download(url, binPath, progress, "cloudflared"); err != nil { + if err := providers.DownloadWithProgress(url, binPath, progress, "cloudflared"); err != nil { return fmt.Errorf("download failed: %w", err) } } @@ -81,10 +75,6 @@ func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { return fmt.Errorf("binary not found after install: %w", err) } - if err := os.Chmod(binPath, 0755); err != nil { - return fmt.Errorf("chmod failed: %w", err) - } - if progress != nil { progress <- providers.DownloadProgress{ Percent: 100, @@ -119,27 +109,3 @@ func (i *Installer) cloudflaredURL() (string, error) { return "", fmt.Errorf("unsupported OS: %s", os) } } - -func (i *Installer) extractTgz(src, dest string) error { - cmd := exec.Command("tar", "-xzf", src, "-C", filepath.Dir(dest)) - if err := cmd.Run(); err != nil { - return err - } - - entries, err := os.ReadDir(filepath.Dir(dest)) - if err != nil { - return err - } - - for _, entry := range entries { - if !entry.IsDir() && entry.Name() == "cloudflared" { - extractedPath := filepath.Join(filepath.Dir(dest), entry.Name()) - if extractedPath != dest { - return os.Rename(extractedPath, dest) - } - return os.Chmod(dest, 0755) - } - } - - return fmt.Errorf("cloudflared binary not found in extracted archive") -} From a0ad308b7299f1dec14b0b2d10a92114b133078c Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:47:21 -0600 Subject: [PATCH 6/9] fix(cloudflared): implement AutoInstaller for download progress UI - Add IsInstalled() and Install() methods to implement AutoInstaller interface - Remove direct Install() call from Start() so UI can show download progress - Now cloudflared shows downloading screen like bore/tunnelmole --- internal/providers/cloudflared/cloudflared.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/providers/cloudflared/cloudflared.go b/internal/providers/cloudflared/cloudflared.go index 39e33b4..45f1282 100644 --- a/internal/providers/cloudflared/cloudflared.go +++ b/internal/providers/cloudflared/cloudflared.go @@ -77,10 +77,7 @@ func (p *CloudflaredProvider) FindBinary() string { func (p *CloudflaredProvider) Start(ctx context.Context, tunnel config.TunnelConfig, logWriter io.Writer) (*providers.Process, error) { binary := p.FindBinary() if binary == "" { - if err := p.installer.Install(nil); err != nil { - return nil, fmt.Errorf("failed to install cloudflared: %w", err) - } - binary = p.installer.CloudflaredBin() + return nil, fmt.Errorf("installing") } ctx, cancel := context.WithCancel(ctx) @@ -131,3 +128,11 @@ func (p *CloudflaredProvider) IsReady(line string) bool { return strings.Contains(line, "trycloudflare.com") || strings.Contains(line, "started tunnel") } + +func (p *CloudflaredProvider) IsInstalled() bool { + return p.installer.IsInstalled() +} + +func (p *CloudflaredProvider) Install(progress chan<- providers.DownloadProgress) error { + return p.installer.Install(progress) +} From 6d4448cbcc244d1ddfaa449e45d26c1cee4b8d5a Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Tue, 12 May 2026 16:50:58 -0600 Subject: [PATCH 7/9] fix(api): add bore to /api/providers endpoint --- go.mod | 2 +- internal/web/handlers_status.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index bdd4fac..3e6d1c1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v0.12.1 github.com/gorilla/websocket v1.5.3 github.com/wailsapp/wails/v2 v2.11.0 + golang.org/x/text v0.22.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -52,5 +53,4 @@ require ( golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect ) diff --git a/internal/web/handlers_status.go b/internal/web/handlers_status.go index 1a70ff2..5376105 100644 --- a/internal/web/handlers_status.go +++ b/internal/web/handlers_status.go @@ -24,6 +24,7 @@ func (h *Handlers) handleProviders(w http.ResponseWriter) { {"id": "localhostrun", "name": "localhost.run"}, {"id": "serveo", "name": "Serveo"}, {"id": "pinggy", "name": "Pinggy"}, + {"id": "bore", "name": "bore"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(providers) From 8a688fc4b847c5cf3a955c8700a624a557d904c8 Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Fri, 15 May 2026 00:27:51 -0600 Subject: [PATCH 8/9] refactor: extract bore version --- internal/providers/bore/installer.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/providers/bore/installer.go b/internal/providers/bore/installer.go index 7b66ba5..f99b463 100644 --- a/internal/providers/bore/installer.go +++ b/internal/providers/bore/installer.go @@ -9,6 +9,8 @@ import ( "github.com/sthbryan/ftm/internal/providers" ) +const boreVersion = "v0.6.0" + type Installer struct { BaseDir string } @@ -50,29 +52,29 @@ func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { case "darwin": switch arch { case "arm64": - filename = "bore-v0.6.0-aarch64-apple-darwin.tar.gz" - url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + filename = "bore-" + boreVersion + "-aarch64-apple-darwin.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/" + boreVersion + "/" + filename case "amd64": - filename = "bore-v0.6.0-x86_64-apple-darwin.tar.gz" - url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + filename = "bore-" + boreVersion + "-x86_64-apple-darwin.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/" + boreVersion + "/" + filename default: return fmt.Errorf("unsupported architecture for macOS: %s (supported: arm64, amd64)", arch) } case "linux": switch arch { case "arm64": - filename = "bore-v0.6.0-aarch64-unknown-linux-musl.tar.gz" - url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + filename = "bore-" + boreVersion + "-aarch64-unknown-linux-musl.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/" + boreVersion + "/" + filename case "amd64": - filename = "bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz" - url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + filename = "bore-" + boreVersion + "-x86_64-unknown-linux-musl.tar.gz" + url = "https://github.com/ekzhang/bore/releases/download/" + boreVersion + "/" + filename default: return fmt.Errorf("unsupported architecture for Linux: %s (supported: arm64, amd64)", arch) } case "windows": if arch == "amd64" { - filename = "bore-v0.6.0-x86_64-pc-windows-msvc.zip" - url = "https://github.com/ekzhang/bore/releases/download/v0.6.0/" + filename + filename = "bore-" + boreVersion + "-x86_64-pc-windows-msvc.zip" + url = "https://github.com/ekzhang/bore/releases/download/" + boreVersion + "/" + filename } else { return fmt.Errorf("unsupported architecture for Windows: %s (supported: amd64)", arch) } @@ -114,4 +116,4 @@ func (i *Installer) Install(progress chan<- providers.DownloadProgress) error { } return nil -} +} \ No newline at end of file From d274d86bcfb1dd58250999e5067b4ffded5884ea Mon Sep 17 00:00:00 2001 From: Bryan Villafuerte Date: Fri, 15 May 2026 00:28:14 -0600 Subject: [PATCH 9/9] refactor: handle error on closing output --- internal/providers/downloader.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/providers/downloader.go b/internal/providers/downloader.go index e3a1bcd..406ed8a 100644 --- a/internal/providers/downloader.go +++ b/internal/providers/downloader.go @@ -170,7 +170,9 @@ func extractFileToDir(src io.Reader, destDir, filename string) error { } _, err = io.Copy(out, src) - out.Close() + if closeErr := out.Close(); closeErr != nil && err == nil { + err = closeErr + } if err != nil { os.Remove(tmpPath) @@ -191,11 +193,11 @@ func extractFileToDir(src io.Reader, destDir, filename string) error { } func ExtractArchive(src, destDir, binaryName string) error { - src = strings.ToLower(src) - if strings.HasSuffix(src, ".zip") { + lowerSrc := strings.ToLower(src) + if strings.HasSuffix(lowerSrc, ".zip") { return ExtractZip(src, destDir, binaryName) } - if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") { + if strings.HasSuffix(lowerSrc, ".tar.gz") || strings.HasSuffix(lowerSrc, ".tgz") { return ExtractTarGz(src, destDir, binaryName) } return fmt.Errorf("unsupported archive format: %s", src)