main 6af0a61

bootstrap_handler.go RawHistoryFolder

1package handlers
2
3import (
4	"context"
5	"fmt"
6	"os/exec"
7	"path/filepath"
8
9	"github.com/bnema/archup/internal/application/dto"
10	"github.com/bnema/archup/internal/config"
11	"github.com/bnema/archup/internal/domain/ports"
12)
13
14// BootstrapHandler handles downloading/cloning install files
15type BootstrapHandler struct {
16	fs         ports.FileSystem
17	httpClient ports.HTTPClient
18	logger     ports.Logger
19	repoURL    string
20	rawURL     string
21	branch     string
22}
23
24// NewBootstrapHandler creates a new bootstrap handler
25func NewBootstrapHandler(fs ports.FileSystem, httpClient ports.HTTPClient, logger ports.Logger, repoURL, rawURL, branch string) *BootstrapHandler {
26	return &BootstrapHandler{
27		fs:         fs,
28		httpClient: httpClient,
29		logger:     logger,
30		repoURL:    repoURL,
31		rawURL:     rawURL,
32		branch:     branch,
33	}
34}
35
36// Handle prepares install files by cloning repo or downloading
37func (h *BootstrapHandler) Handle(ctx context.Context) (*dto.BootstrapResult, error) {
38	h.logger.Info("Starting bootstrap")
39
40	result := &dto.BootstrapResult{
41		Success:     false,
42		InstallDir:  config.DefaultInstallDir,
43		ErrorDetail: "",
44	}
45
46	// Create install directory
47	if err := h.fs.MkdirAll(config.DefaultInstallDir, 0755); err != nil {
48		result.ErrorDetail = fmt.Sprintf("Failed to create install directory: %v", err)
49		return result, err
50	}
51
52	// Try to clone repo first
53	if err := h.cloneRepo(ctx); err == nil {
54		if err := h.copyRepoFiles(); err != nil {
55			h.logger.Warn("Failed to copy repo files", "error", err)
56			result.ErrorDetail = fmt.Sprintf("Failed to copy repo files: %v", err)
57			return result, err
58		}
59		h.logger.Info("Bootstrap complete via repo clone")
60		result.Success = true
61		result.Method = "clone"
62		return result, nil
63	} else {
64		h.logger.Warn("Repo clone failed, falling back to downloads", "error", err)
65	}
66
67	// Fallback: download files
68	if err := h.downloadFiles(ctx); err != nil {
69		result.ErrorDetail = fmt.Sprintf("Failed to download files: %v", err)
70		return result, err
71	}
72
73	h.logger.Info("Bootstrap complete via downloads")
74	result.Success = true
75	result.Method = "download"
76	return result, nil
77}
78
79// cloneRepo clones the archup repository at the configured git ref.
80func (h *BootstrapHandler) cloneRepo(ctx context.Context) error {
81	repoDir := config.DefaultInstallRepoDir
82
83	// Check if already cloned
84	if _, err := h.fs.Stat(filepath.Join(repoDir, ".git")); err == nil {
85		h.logger.Info("Repository already cloned")
86		return nil
87	}
88
89	branch := h.branch
90	if branch == "" {
91		branch = "main"
92	}
93
94	h.logger.Info("Cloning repository", "url", h.repoURL, "branch", branch)
95
96	cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", branch, h.repoURL, repoDir)
97	if output, err := cmd.CombinedOutput(); err != nil {
98		return fmt.Errorf("git clone failed: %w (output: %s)", err, string(output))
99	}
100
101	return nil
102}
103
104// copyRepoFiles copies install files from cloned repo to install directory
105func (h *BootstrapHandler) copyRepoFiles() error {
106	repoDir := config.DefaultInstallRepoDir
107	installDir := config.DefaultInstallDir
108
109	// Files to copy from repo
110	files := []struct {
111		src  string
112		dest string
113	}{
114		{"install/base.packages", "base.packages"},
115		{"install/extra.packages", "extra.packages"},
116		{"install/configs/limine.conf.template", "configs/limine.conf.template"},
117		{"install/configs/chaotic-aur.conf", "configs/chaotic-aur.conf"},
118		{"install/configs/shell/shell", "configs/shell/shell"},
119		{"install/configs/shell/starship.toml", "configs/shell/starship.toml"},
120		{"install/configs/shell/init", "configs/shell/init"},
121		{"install/configs/shell/aliases", "configs/shell/aliases"},
122		{"install/configs/shell/envs", "configs/shell/envs"},
123		{"install/configs/shell/rc", "configs/shell/rc"},
124		{"install/configs/shell/bashrc", "configs/shell/bashrc"},
125	}
126
127	for _, f := range files {
128		srcPath := filepath.Join(repoDir, f.src)
129		destPath := filepath.Join(installDir, f.dest)
130
131		// Create destination directory
132		if err := h.fs.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
133			return fmt.Errorf("failed to create directory for %s: %w", f.dest, err)
134		}
135
136		// Read and copy file
137		content, err := h.fs.ReadFile(srcPath)
138		if err != nil {
139			h.logger.Warn("File not found in repo", "file", f.src)
140			continue // Skip missing files
141		}
142
143		if err := h.fs.WriteFile(destPath, content, 0644); err != nil {
144			return fmt.Errorf("failed to write %s: %w", f.dest, err)
145		}
146
147		h.logger.Info("Copied file", "file", f.dest)
148	}
149
150	return nil
151}
152
153// downloadFiles downloads install files from GitHub
154func (h *BootstrapHandler) downloadFiles(_ context.Context) error {
155	files := []struct {
156		urlPath  string
157		destPath string
158	}{
159		{"install/base.packages", "base.packages"},
160		{"install/extra.packages", "extra.packages"},
161		{"install/configs/limine.conf.template", "configs/limine.conf.template"},
162	}
163
164	for _, f := range files {
165		url := fmt.Sprintf("%s/%s", h.rawURL, f.urlPath)
166		destPath := filepath.Join(config.DefaultInstallDir, f.destPath)
167
168		// Create destination directory
169		if err := h.fs.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
170			return fmt.Errorf("failed to create directory for %s: %w", f.destPath, err)
171		}
172
173		h.logger.Info("Downloading file", "url", url)
174
175		resp, err := h.httpClient.Get(url)
176		if err != nil {
177			return fmt.Errorf("failed to download %s: %w", f.urlPath, err)
178		}
179
180		if resp.StatusCode() < 200 || resp.StatusCode() > 299 {
181			if closeErr := resp.Close(); closeErr != nil {
182				h.logger.Warn("Failed to close HTTP response", "error", closeErr)
183			}
184			return fmt.Errorf("unexpected status %d from %s", resp.StatusCode(), url)
185		}
186
187		if err := h.fs.WriteFile(destPath, resp.Body(), 0644); err != nil {
188			if closeErr := resp.Close(); closeErr != nil {
189				h.logger.Warn("Failed to close HTTP response", "error", closeErr)
190			}
191			return fmt.Errorf("failed to write %s: %w", f.destPath, err)
192		}
193
194		if closeErr := resp.Close(); closeErr != nil {
195			h.logger.Warn("Failed to close HTTP response", "error", closeErr)
196		}
197	}
198
199	return nil
200}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close