detached 5248904

refactor(phases): integrate shell, postboot, and verify packages into PostInstallPhase

commit
5248904b484c109f1b7330f484335646eea41ecd
author
bnema · · 2025-10-28 22:21 UTC
committer
bnema · · 2025-10-28 22:42 UTC
parent
935f0cf
signature
unsigned
2 files · +58 -311
M internal/phases/phase.go +6 -0
@@ -178,6 +178,12 @@ func (b *BasePhase) SendError(progressChan chan<- ProgressUpdate, err error) {
178178
179179 // SendOutput is a helper to send command output line
180180 func (b *BasePhase) SendOutput(progressChan chan<- ProgressUpdate, line string) {
181+ // Log the output
182+ if b.logger != nil {
183+ b.logger.Info(line)
184+ }
185+
186+ // Send to progress channel for TUI display
181187 if progressChan != nil {
182188 progressChan <- ProgressUpdate{
183189 PhaseName: b.name,
@@ -178,6 +178,12 @@ func (b *BasePhase) SendError(progressChan chan<- ProgressUpdate, err error) {
178 178
179 // SendOutput is a helper to send command output line179 // SendOutput is a helper to send command output line
180 func (b *BasePhase) SendOutput(progressChan chan<- ProgressUpdate, line string) {180 func (b *BasePhase) SendOutput(progressChan chan<- ProgressUpdate, line string) {
181+ // Log the output
182+ if b.logger != nil {
183+ b.logger.Info(line)
184+ }
185+
186+ // Send to progress channel for TUI display
181 if progressChan != nil {187 if progressChan != nil {
182 progressChan <- ProgressUpdate{188 progressChan <- ProgressUpdate{
183 PhaseName: b.name,189 PhaseName: b.name,
M internal/phases/postinstall.go +52 -311
@@ -12,7 +12,10 @@ import (
1212 "github.com/bnema/archup/internal/config"
1313 "github.com/bnema/archup/internal/interfaces"
1414 "github.com/bnema/archup/internal/logger"
15+ "github.com/bnema/archup/internal/postboot"
16+ "github.com/bnema/archup/internal/shell"
1517 "github.com/bnema/archup/internal/system"
18+ "github.com/bnema/archup/internal/verify"
1619 )
1720
1821 // PostInstallPhase handles post-installation tasks
@@ -53,7 +56,7 @@ func (p *PostInstallPhase) PreCheck() error {
5356
5457 // Execute runs the post-install phase
5558 func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
56- totalSteps := 9
59+ totalSteps := 10
5760 currentStep := 0
5861
5962 // Step 1: Boot logo
@@ -91,10 +94,18 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu
9194 // Step 5: Post-boot setup
9295 currentStep++
9396 p.SendProgress(progressChan, "Setting up post-boot scripts...", currentStep, totalSteps)
94- if err := p.setupPostBoot(progressChan); err != nil {
97+ postbootSetup := postboot.NewSetup(p.fs, p.http, p.chrExec, p.config, p.logger)
98+ pbResult, err := postbootSetup.Configure()
99+ if err != nil {
95100 p.SendError(progressChan, err)
96101 return PhaseResult{Success: false, Error: err}
97102 }
103+ if pbResult.ScriptsDownloaded > 0 {
104+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Post-boot scripts downloaded (%d files)", pbResult.ScriptsDownloaded))
105+ }
106+ if pbResult.ServiceEnabled {
107+ p.SendOutput(progressChan, "[OK] First-boot service enabled")
108+ }
98109
99110 // Step 6: Pacman hooks
100111 currentStep++
@@ -107,12 +118,49 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu
107118 // Step 7: Shell configuration
108119 currentStep++
109120 p.SendProgress(progressChan, "Configuring shell environment...", currentStep, totalSteps)
110- if err := p.configureShell(progressChan); err != nil {
121+ shellConfig := shell.NewConfigurator(p.fs, p.http, p.chrExec, p.config, p.logger)
122+
123+ // Get username from /mnt/home
124+ homeDir := filepath.Join(config.PathMnt, "home")
125+ entries, err := os.ReadDir(homeDir)
126+ switch {
127+ case err != nil:
128+ p.SendError(progressChan, err)
129+ return PhaseResult{Success: false, Error: fmt.Errorf("failed to read home directory: %w", err)}
130+ case len(entries) == 0:
131+ p.SendError(progressChan, err)
132+ return PhaseResult{Success: false, Error: fmt.Errorf("no user found in /home")}
133+ }
134+
135+ username := entries[0].Name()
136+ userHome := filepath.Join(config.PathMnt, "home", username)
137+
138+ shellResult, err := shellConfig.Configure(username, userHome)
139+ if err != nil {
111140 p.SendError(progressChan, err)
112141 return PhaseResult{Success: false, Error: err}
113142 }
143+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Shell environment configured (%d themes applied)", shellResult.ThemesApplied))
144+ if len(shellResult.ThemesFailed) > 0 {
145+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to apply themes: %v", shellResult.ThemesFailed))
146+ }
147+ for _, w := range shellResult.Warnings {
148+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] %s", w))
149+ }
150+
151+ // Step 7a: Shell configuration verification (non-fatal)
152+ currentStep++
153+ p.SendProgress(progressChan, "Verifying shell configuration...", currentStep, totalSteps)
154+ shellVerification := verify.ValidateShellConfigs(p.fs, p.chrExec, p.logger.LogPath(), userHome)
155+ if warnings := verify.GetShellConfigWarnings(shellVerification); warnings != "" {
156+ p.SendOutput(progressChan, warnings)
157+ }
158+
159+ // Step 7b: Theme verification (non-fatal)
160+ themeFiles := verify.ValidateThemeFiles(p.fs, userHome)
161+ p.SendOutput(progressChan, verify.GetThemeVerificationSummary(themeFiles))
114162
115- // Step 8: Verification
163+ // Step 8: Installation verification
116164 currentStep++
117165 p.SendProgress(progressChan, "Verifying installation...", currentStep, totalSteps)
118166 if err := p.verifyInstallation(progressChan); err != nil {
@@ -382,206 +430,6 @@ Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/limine/
382430 return nil
383431 }
384432
385-// configureShell sets up bash shell with ArchUp defaults
386-func (p *PostInstallPhase) configureShell(progressChan chan<- ProgressUpdate) error {
387- p.SendOutput(progressChan, "Configuring shell environment...")
388-
389- // Get username from /mnt/home
390- homeDir := filepath.Join(config.PathMnt, "home")
391- entries, err := os.ReadDir(homeDir)
392- switch {
393- case err != nil:
394- return fmt.Errorf("failed to read home directory: %w", err)
395- case len(entries) == 0:
396- return fmt.Errorf("no user found in /home")
397- }
398-
399- username := entries[0].Name()
400- userHome := filepath.Join(config.PathMnt, "home", username)
401- archupDefault := filepath.Join(userHome, ".local", "share", "archup", "default")
402- archupDefaultBash := filepath.Join(archupDefault, "bash")
403-
404- p.SendOutput(progressChan, fmt.Sprintf("Configuring shell for user: %s", username))
405-
406- // Create directory structure
407- if err := p.fs.MkdirAll(archupDefaultBash, 0755); err != nil {
408- return fmt.Errorf("failed to create shell config directory: %w", err)
409- }
410-
411- // Copy shell configuration files from templates
412- if err := p.copyShellTemplates(archupDefault, archupDefaultBash); err != nil {
413- return fmt.Errorf("failed to copy shell templates: %w", err)
414- }
415-
416- // Create .bashrc
417- bashrcContent, err := p.readTemplate(config.BashrcTemplate)
418- if err != nil {
419- return fmt.Errorf("failed to read bashrc template: %w", err)
420- }
421-
422- bashrcPath := filepath.Join(userHome, ".bashrc")
423- if err := p.fs.WriteFile(bashrcPath, bashrcContent, 0644); err != nil {
424- return fmt.Errorf("failed to write .bashrc: %w", err)
425- }
426-
427- // Set ownership to user
428- if err := p.setShellOwnership(userHome, username); err != nil {
429- return fmt.Errorf("failed to set ownership: %w", err)
430- }
431-
432- // Clone bleu-theme repository
433- p.SendOutput(progressChan, "Cloning bleu-theme repository...")
434- themesDir := filepath.Join(userHome, ".local", "share", "archup", "themes")
435- bleuThemeDir := filepath.Join(themesDir, "bleu")
436- currentDir := filepath.Join(userHome, ".local", "share", "archup", "current")
437-
438- cloneCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && git clone https://github.com/bnema/bleu-theme.git %s'",
439- username, themesDir, bleuThemeDir)
440- switch err := p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cloneCmd); {
441- case err != nil:
442- p.SendOutput(progressChan, "[WARN] Failed to clone bleu-theme repository")
443- default:
444- p.SendOutput(progressChan, "[OK] Bleu-theme repository cloned")
445- }
446-
447- // Create current theme symlink
448- symlinkCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && ln -snf %s %s/theme'",
449- username, currentDir, bleuThemeDir, currentDir)
450- if err := p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, symlinkCmd); err != nil {
451- p.SendOutput(progressChan, "[WARN] Failed to create theme symlink")
452- }
453-
454- // Apply themes to CLI tools
455- p.SendOutput(progressChan, "Applying themes to CLI tools...")
456- if err := p.applyThemes(progressChan, username, userHome); err != nil {
457- // Non-fatal, log and continue
458- p.SendOutput(progressChan, "[WARN] Some themes failed to apply")
459- }
460-
461- // Configure git delta
462- gitCommands := []string{
463- fmt.Sprintf("su - %s -c 'git config --global core.pager delta'", username),
464- fmt.Sprintf("su - %s -c 'git config --global interactive.diffFilter \"delta --color-only\"'", username),
465- fmt.Sprintf("su - %s -c 'git config --global delta.navigate true'", username),
466- fmt.Sprintf("su - %s -c 'git config --global delta.light false'", username),
467- fmt.Sprintf("su - %s -c 'git config --global delta.side-by-side true'", username),
468- fmt.Sprintf("su - %s -c 'git config --global merge.conflictstyle diff3'", username),
469- fmt.Sprintf("su - %s -c 'git config --global diff.colorMoved default'", username),
470- }
471-
472- for _, gitCmd := range gitCommands {
473- if err := p.chrExec.ChrootExec(p.logger.LogPath(),config.PathMnt, gitCmd); err != nil {
474- // Non-fatal
475- p.SendOutput(progressChan, "[WARN] Failed to configure git delta")
476- break
477- }
478- }
479-
480- p.SendOutput(progressChan, "[OK] Shell environment configured")
481- return nil
482-}
483-
484-// copyShellTemplates copies shell config templates to user directory
485-func (p *PostInstallPhase) copyShellTemplates(archupDefault, archupDefaultBash string) error {
486- templates := map[string]string{
487- config.ShellConfigTemplate: filepath.Join(archupDefaultBash, "shell"),
488- config.ShellInitTemplate: filepath.Join(archupDefaultBash, "init"),
489- config.ShellAliasesTemplate: filepath.Join(archupDefaultBash, "aliases"),
490- config.ShellEnvsTemplate: filepath.Join(archupDefaultBash, "envs"),
491- config.ShellRcTemplate: filepath.Join(archupDefaultBash, "rc"),
492- // Note: starship.toml is now copied from bleu-theme in applyThemes()
493- }
494-
495- for template, dest := range templates {
496- content, err := p.readTemplate(template)
497- if err != nil {
498- return fmt.Errorf("failed to read template %s: %w", template, err)
499- }
500-
501- if err := p.fs.WriteFile(dest, content, 0644); err != nil {
502- return fmt.Errorf("failed to write %s: %w", dest, err)
503- }
504- }
505-
506- return nil
507-}
508-
509-// readTemplate reads a template file from install path
510-func (p *PostInstallPhase) readTemplate(filename string) ([]byte, error) {
511- // Use DefaultInstallDir directly to match where bootstrap downloads files
512- templatePath := filepath.Join(config.DefaultInstallDir, filename)
513- return p.fs.ReadFile(templatePath)
514-}
515-
516-// setShellOwnership sets ownership of shell config files to user
517-func (p *PostInstallPhase) setShellOwnership(userHome, username string) error {
518- // Use chroot to run chown
519- relativeHome := strings.TrimPrefix(userHome, config.PathMnt)
520- chownCmd := fmt.Sprintf("chown -R %s:%s %s/.local %s/.bashrc",
521- username, username, relativeHome, relativeHome)
522- return p.chrExec.ChrootExec(p.logger.LogPath(),config.PathMnt, chownCmd)
523-}
524-
525-// applyThemes applies bleu-theme to CLI tools
526-func (p *PostInstallPhase) applyThemes(progressChan chan<- ProgressUpdate, username, userHome string) error {
527- themeDir := filepath.Join(userHome, ".local", "share", "archup", "current", "theme")
528- archupDefault := filepath.Join(userHome, ".local", "share", "archup", "default")
529-
530- // Starship theme - copy to archup default location
531- p.SendOutput(progressChan, "Configuring starship theme...")
532- starshipCmd := fmt.Sprintf("su - %s -c 'cp %s/starship/bleu.toml %s/starship.toml'",
533- username, themeDir, archupDefault)
534- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, starshipCmd)
535-
536- // Eza theme
537- p.SendOutput(progressChan, "Configuring eza theme...")
538- ezaConfigDir := fmt.Sprintf("%s/.config/eza", userHome)
539- ezaCommands := []string{
540- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, ezaConfigDir),
541- fmt.Sprintf("su - %s -c 'cp %s/eza/theme.yml %s/'", username, themeDir, ezaConfigDir),
542- }
543- for _, cmd := range ezaCommands {
544- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
545- }
546-
547- // Bat theme
548- p.SendOutput(progressChan, "Configuring bat theme...")
549- batThemeDir := fmt.Sprintf("%s/.config/bat/themes", userHome)
550- batCommands := []string{
551- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, batThemeDir),
552- fmt.Sprintf("su - %s -c 'cp %s/bat/bleu.tmTheme %s/'", username, themeDir, batThemeDir),
553- fmt.Sprintf("su - %s -c 'bat cache --build'", username),
554- }
555- for _, cmd := range batCommands {
556- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
557- }
558-
559- // Btop theme
560- p.SendOutput(progressChan, "Configuring btop theme...")
561- btopThemeDir := fmt.Sprintf("%s/.config/btop/themes", userHome)
562- btopCommands := []string{
563- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, btopThemeDir),
564- fmt.Sprintf("su - %s -c 'cp %s/btop/bleu.theme %s/'", username, themeDir, btopThemeDir),
565- }
566- for _, cmd := range btopCommands {
567- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
568- }
569-
570- // Yazi theme
571- p.SendOutput(progressChan, "Configuring yazi theme...")
572- yaziFlavorDir := fmt.Sprintf("%s/.config/yazi/flavors/bleu", userHome)
573- yaziCommands := []string{
574- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, yaziFlavorDir),
575- fmt.Sprintf("su - %s -c 'cp %s/yazi/bleu.toml %s/flavor.toml'", username, themeDir, yaziFlavorDir),
576- }
577- for _, cmd := range yaziCommands {
578- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
579- }
580-
581- p.SendOutput(progressChan, "[OK] CLI themes configured")
582- return nil
583-}
584-
585433 // verifyInstallation performs comprehensive verification
586434 func (p *PostInstallPhase) verifyInstallation(progressChan chan<- ProgressUpdate) error {
587435 p.SendOutput(progressChan, "Verifying installation components...")
@@ -702,110 +550,3 @@ func (p *PostInstallPhase) CanSkip() bool {
702550 return false
703551 }
704552
705-// setupPostBoot downloads and configures first-boot service
706-func (p *PostInstallPhase) setupPostBoot(progressChan chan<- ProgressUpdate) error {
707- p.SendOutput(progressChan, "Configuring first-boot service...")
708-
709- if err := p.fs.MkdirAll(config.PathMntPostBoot, 0755); err != nil {
710- return fmt.Errorf("failed to create post-boot directory: %w", err)
711- }
712-
713- // Download logo.txt
714- p.SendOutput(progressChan, "Downloading logo.txt...")
715- logoURL := fmt.Sprintf("%s/logo.txt", p.config.RawURL)
716- resp, err := p.http.Get(logoURL)
717- switch {
718- case err != nil:
719- return fmt.Errorf("failed to download logo.txt: %w", err)
720- case resp.StatusCode != http.StatusOK:
721- resp.Body.Close()
722- return fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
723- }
724-
725- logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
726- logoFile, err := p.fs.Create(logoPath)
727- if err != nil {
728- resp.Body.Close()
729- return fmt.Errorf("failed to create logo.txt: %w", err)
730- }
731-
732- _, err = io.Copy(logoFile, resp.Body)
733- logoFile.Close()
734- resp.Body.Close()
735- if err != nil {
736- return fmt.Errorf("failed to save logo.txt: %w", err)
737- }
738-
739- // Download all post-boot scripts
740- for _, script := range config.PostBootScripts {
741- p.SendOutput(progressChan, fmt.Sprintf("Downloading %s...", script))
742-
743- scriptURL := fmt.Sprintf("%s/install/post-boot/%s", p.config.RawURL, script)
744- resp, err := p.http.Get(scriptURL)
745- switch {
746- case err != nil:
747- return fmt.Errorf("failed to download %s: %w", script, err)
748- case resp.StatusCode != http.StatusOK:
749- resp.Body.Close()
750- return fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
751- }
752-
753- scriptPath := filepath.Join(config.PathMntPostBoot, script)
754- scriptFile, err := p.fs.Create(scriptPath)
755- if err != nil {
756- resp.Body.Close()
757- return fmt.Errorf("failed to create %s: %w", script, err)
758- }
759-
760- _, err = io.Copy(scriptFile, resp.Body)
761- scriptFile.Close()
762- resp.Body.Close()
763-
764- if err != nil {
765- return fmt.Errorf("failed to save %s: %w", script, err)
766- }
767-
768- if err := p.fs.Chmod(scriptPath, 0755); err != nil {
769- return fmt.Errorf("failed to set permissions on %s: %w", script, err)
770- }
771- }
772-
773- p.SendOutput(progressChan, "[OK] Post-boot scripts downloaded")
774-
775- // Download service template
776- p.SendOutput(progressChan, "Creating systemd service...")
777- serviceURL := fmt.Sprintf("%s/%s", p.config.RawURL, config.PostBootServiceTemplate)
778- resp, err = p.http.Get(serviceURL)
779- switch {
780- case err != nil:
781- return fmt.Errorf("failed to download service template: %w", err)
782- case resp.StatusCode != http.StatusOK:
783- resp.Body.Close()
784- return fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
785- }
786-
787- templateBytes, err := io.ReadAll(resp.Body)
788- resp.Body.Close()
789- if err != nil {
790- return fmt.Errorf("failed to read service template: %w", err)
791- }
792-
793- serviceContent := string(templateBytes)
794- serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_EMAIL__", p.config.Email)
795- serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_USERNAME__", p.config.Username)
796-
797- servicePath := filepath.Join(config.PathMntSystemdSystem, config.PostBootServiceName)
798- if err := p.fs.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
799- return fmt.Errorf("failed to write service file: %w", err)
800- }
801-
802- p.SendOutput(progressChan, "[OK] Service file created")
803-
804- p.SendOutput(progressChan, "Enabling first-boot service...")
805- if err := p.chrExec.ChrootSystemctl(p.logger.LogPath(), config.PathMnt, "enable", config.PostBootServiceName); err != nil {
806- return fmt.Errorf("failed to enable service: %w", err)
807- }
808-
809- p.SendOutput(progressChan, "[OK] First-boot service enabled")
810- return nil
811-}
@@ -12,7 +12,10 @@ import (
12 "github.com/bnema/archup/internal/config"12 "github.com/bnema/archup/internal/config"
13 "github.com/bnema/archup/internal/interfaces"13 "github.com/bnema/archup/internal/interfaces"
14 "github.com/bnema/archup/internal/logger"14 "github.com/bnema/archup/internal/logger"
15+ "github.com/bnema/archup/internal/postboot"
16+ "github.com/bnema/archup/internal/shell"
15 "github.com/bnema/archup/internal/system"17 "github.com/bnema/archup/internal/system"
18+ "github.com/bnema/archup/internal/verify"
16 )19 )
17 20
18 // PostInstallPhase handles post-installation tasks21 // PostInstallPhase handles post-installation tasks
@@ -53,7 +56,7 @@ func (p *PostInstallPhase) PreCheck() error {
53 56
54 // Execute runs the post-install phase57 // Execute runs the post-install phase
55 func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {58 func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
56- totalSteps := 959+ totalSteps := 10
57 currentStep := 060 currentStep := 0
58 61
59 // Step 1: Boot logo62 // Step 1: Boot logo
@@ -91,10 +94,18 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu
91 // Step 5: Post-boot setup94 // Step 5: Post-boot setup
92 currentStep++95 currentStep++
93 p.SendProgress(progressChan, "Setting up post-boot scripts...", currentStep, totalSteps)96 p.SendProgress(progressChan, "Setting up post-boot scripts...", currentStep, totalSteps)
94- if err := p.setupPostBoot(progressChan); err != nil {97+ postbootSetup := postboot.NewSetup(p.fs, p.http, p.chrExec, p.config, p.logger)
98+ pbResult, err := postbootSetup.Configure()
99+ if err != nil {
95 p.SendError(progressChan, err)100 p.SendError(progressChan, err)
96 return PhaseResult{Success: false, Error: err}101 return PhaseResult{Success: false, Error: err}
97 }102 }
103+ if pbResult.ScriptsDownloaded > 0 {
104+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Post-boot scripts downloaded (%d files)", pbResult.ScriptsDownloaded))
105+ }
106+ if pbResult.ServiceEnabled {
107+ p.SendOutput(progressChan, "[OK] First-boot service enabled")
108+ }
98 109
99 // Step 6: Pacman hooks110 // Step 6: Pacman hooks
100 currentStep++111 currentStep++
@@ -107,12 +118,49 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu
107 // Step 7: Shell configuration118 // Step 7: Shell configuration
108 currentStep++119 currentStep++
109 p.SendProgress(progressChan, "Configuring shell environment...", currentStep, totalSteps)120 p.SendProgress(progressChan, "Configuring shell environment...", currentStep, totalSteps)
110- if err := p.configureShell(progressChan); err != nil {121+ shellConfig := shell.NewConfigurator(p.fs, p.http, p.chrExec, p.config, p.logger)
122+
123+ // Get username from /mnt/home
124+ homeDir := filepath.Join(config.PathMnt, "home")
125+ entries, err := os.ReadDir(homeDir)
126+ switch {
127+ case err != nil:
128+ p.SendError(progressChan, err)
129+ return PhaseResult{Success: false, Error: fmt.Errorf("failed to read home directory: %w", err)}
130+ case len(entries) == 0:
131+ p.SendError(progressChan, err)
132+ return PhaseResult{Success: false, Error: fmt.Errorf("no user found in /home")}
133+ }
134+
135+ username := entries[0].Name()
136+ userHome := filepath.Join(config.PathMnt, "home", username)
137+
138+ shellResult, err := shellConfig.Configure(username, userHome)
139+ if err != nil {
111 p.SendError(progressChan, err)140 p.SendError(progressChan, err)
112 return PhaseResult{Success: false, Error: err}141 return PhaseResult{Success: false, Error: err}
113 }142 }
143+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Shell environment configured (%d themes applied)", shellResult.ThemesApplied))
144+ if len(shellResult.ThemesFailed) > 0 {
145+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to apply themes: %v", shellResult.ThemesFailed))
146+ }
147+ for _, w := range shellResult.Warnings {
148+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] %s", w))
149+ }
150+
151+ // Step 7a: Shell configuration verification (non-fatal)
152+ currentStep++
153+ p.SendProgress(progressChan, "Verifying shell configuration...", currentStep, totalSteps)
154+ shellVerification := verify.ValidateShellConfigs(p.fs, p.chrExec, p.logger.LogPath(), userHome)
155+ if warnings := verify.GetShellConfigWarnings(shellVerification); warnings != "" {
156+ p.SendOutput(progressChan, warnings)
157+ }
158+
159+ // Step 7b: Theme verification (non-fatal)
160+ themeFiles := verify.ValidateThemeFiles(p.fs, userHome)
161+ p.SendOutput(progressChan, verify.GetThemeVerificationSummary(themeFiles))
114 162
115- // Step 8: Verification163+ // Step 8: Installation verification
116 currentStep++164 currentStep++
117 p.SendProgress(progressChan, "Verifying installation...", currentStep, totalSteps)165 p.SendProgress(progressChan, "Verifying installation...", currentStep, totalSteps)
118 if err := p.verifyInstallation(progressChan); err != nil {166 if err := p.verifyInstallation(progressChan); err != nil {
@@ -382,206 +430,6 @@ Exec = /usr/bin/cp /usr/share/limine/BOOTX64.EFI /boot/EFI/limine/
382 return nil430 return nil
383 }431 }
384 432
385-// configureShell sets up bash shell with ArchUp defaults
386-func (p *PostInstallPhase) configureShell(progressChan chan<- ProgressUpdate) error {
387- p.SendOutput(progressChan, "Configuring shell environment...")
388-
389- // Get username from /mnt/home
390- homeDir := filepath.Join(config.PathMnt, "home")
391- entries, err := os.ReadDir(homeDir)
392- switch {
393- case err != nil:
394- return fmt.Errorf("failed to read home directory: %w", err)
395- case len(entries) == 0:
396- return fmt.Errorf("no user found in /home")
397- }
398-
399- username := entries[0].Name()
400- userHome := filepath.Join(config.PathMnt, "home", username)
401- archupDefault := filepath.Join(userHome, ".local", "share", "archup", "default")
402- archupDefaultBash := filepath.Join(archupDefault, "bash")
403-
404- p.SendOutput(progressChan, fmt.Sprintf("Configuring shell for user: %s", username))
405-
406- // Create directory structure
407- if err := p.fs.MkdirAll(archupDefaultBash, 0755); err != nil {
408- return fmt.Errorf("failed to create shell config directory: %w", err)
409- }
410-
411- // Copy shell configuration files from templates
412- if err := p.copyShellTemplates(archupDefault, archupDefaultBash); err != nil {
413- return fmt.Errorf("failed to copy shell templates: %w", err)
414- }
415-
416- // Create .bashrc
417- bashrcContent, err := p.readTemplate(config.BashrcTemplate)
418- if err != nil {
419- return fmt.Errorf("failed to read bashrc template: %w", err)
420- }
421-
422- bashrcPath := filepath.Join(userHome, ".bashrc")
423- if err := p.fs.WriteFile(bashrcPath, bashrcContent, 0644); err != nil {
424- return fmt.Errorf("failed to write .bashrc: %w", err)
425- }
426-
427- // Set ownership to user
428- if err := p.setShellOwnership(userHome, username); err != nil {
429- return fmt.Errorf("failed to set ownership: %w", err)
430- }
431-
432- // Clone bleu-theme repository
433- p.SendOutput(progressChan, "Cloning bleu-theme repository...")
434- themesDir := filepath.Join(userHome, ".local", "share", "archup", "themes")
435- bleuThemeDir := filepath.Join(themesDir, "bleu")
436- currentDir := filepath.Join(userHome, ".local", "share", "archup", "current")
437-
438- cloneCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && git clone https://github.com/bnema/bleu-theme.git %s'",
439- username, themesDir, bleuThemeDir)
440- switch err := p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cloneCmd); {
441- case err != nil:
442- p.SendOutput(progressChan, "[WARN] Failed to clone bleu-theme repository")
443- default:
444- p.SendOutput(progressChan, "[OK] Bleu-theme repository cloned")
445- }
446-
447- // Create current theme symlink
448- symlinkCmd := fmt.Sprintf("su - %s -c 'mkdir -p %s && ln -snf %s %s/theme'",
449- username, currentDir, bleuThemeDir, currentDir)
450- if err := p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, symlinkCmd); err != nil {
451- p.SendOutput(progressChan, "[WARN] Failed to create theme symlink")
452- }
453-
454- // Apply themes to CLI tools
455- p.SendOutput(progressChan, "Applying themes to CLI tools...")
456- if err := p.applyThemes(progressChan, username, userHome); err != nil {
457- // Non-fatal, log and continue
458- p.SendOutput(progressChan, "[WARN] Some themes failed to apply")
459- }
460-
461- // Configure git delta
462- gitCommands := []string{
463- fmt.Sprintf("su - %s -c 'git config --global core.pager delta'", username),
464- fmt.Sprintf("su - %s -c 'git config --global interactive.diffFilter \"delta --color-only\"'", username),
465- fmt.Sprintf("su - %s -c 'git config --global delta.navigate true'", username),
466- fmt.Sprintf("su - %s -c 'git config --global delta.light false'", username),
467- fmt.Sprintf("su - %s -c 'git config --global delta.side-by-side true'", username),
468- fmt.Sprintf("su - %s -c 'git config --global merge.conflictstyle diff3'", username),
469- fmt.Sprintf("su - %s -c 'git config --global diff.colorMoved default'", username),
470- }
471-
472- for _, gitCmd := range gitCommands {
473- if err := p.chrExec.ChrootExec(p.logger.LogPath(),config.PathMnt, gitCmd); err != nil {
474- // Non-fatal
475- p.SendOutput(progressChan, "[WARN] Failed to configure git delta")
476- break
477- }
478- }
479-
480- p.SendOutput(progressChan, "[OK] Shell environment configured")
481- return nil
482-}
483-
484-// copyShellTemplates copies shell config templates to user directory
485-func (p *PostInstallPhase) copyShellTemplates(archupDefault, archupDefaultBash string) error {
486- templates := map[string]string{
487- config.ShellConfigTemplate: filepath.Join(archupDefaultBash, "shell"),
488- config.ShellInitTemplate: filepath.Join(archupDefaultBash, "init"),
489- config.ShellAliasesTemplate: filepath.Join(archupDefaultBash, "aliases"),
490- config.ShellEnvsTemplate: filepath.Join(archupDefaultBash, "envs"),
491- config.ShellRcTemplate: filepath.Join(archupDefaultBash, "rc"),
492- // Note: starship.toml is now copied from bleu-theme in applyThemes()
493- }
494-
495- for template, dest := range templates {
496- content, err := p.readTemplate(template)
497- if err != nil {
498- return fmt.Errorf("failed to read template %s: %w", template, err)
499- }
500-
501- if err := p.fs.WriteFile(dest, content, 0644); err != nil {
502- return fmt.Errorf("failed to write %s: %w", dest, err)
503- }
504- }
505-
506- return nil
507-}
508-
509-// readTemplate reads a template file from install path
510-func (p *PostInstallPhase) readTemplate(filename string) ([]byte, error) {
511- // Use DefaultInstallDir directly to match where bootstrap downloads files
512- templatePath := filepath.Join(config.DefaultInstallDir, filename)
513- return p.fs.ReadFile(templatePath)
514-}
515-
516-// setShellOwnership sets ownership of shell config files to user
517-func (p *PostInstallPhase) setShellOwnership(userHome, username string) error {
518- // Use chroot to run chown
519- relativeHome := strings.TrimPrefix(userHome, config.PathMnt)
520- chownCmd := fmt.Sprintf("chown -R %s:%s %s/.local %s/.bashrc",
521- username, username, relativeHome, relativeHome)
522- return p.chrExec.ChrootExec(p.logger.LogPath(),config.PathMnt, chownCmd)
523-}
524-
525-// applyThemes applies bleu-theme to CLI tools
526-func (p *PostInstallPhase) applyThemes(progressChan chan<- ProgressUpdate, username, userHome string) error {
527- themeDir := filepath.Join(userHome, ".local", "share", "archup", "current", "theme")
528- archupDefault := filepath.Join(userHome, ".local", "share", "archup", "default")
529-
530- // Starship theme - copy to archup default location
531- p.SendOutput(progressChan, "Configuring starship theme...")
532- starshipCmd := fmt.Sprintf("su - %s -c 'cp %s/starship/bleu.toml %s/starship.toml'",
533- username, themeDir, archupDefault)
534- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, starshipCmd)
535-
536- // Eza theme
537- p.SendOutput(progressChan, "Configuring eza theme...")
538- ezaConfigDir := fmt.Sprintf("%s/.config/eza", userHome)
539- ezaCommands := []string{
540- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, ezaConfigDir),
541- fmt.Sprintf("su - %s -c 'cp %s/eza/theme.yml %s/'", username, themeDir, ezaConfigDir),
542- }
543- for _, cmd := range ezaCommands {
544- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
545- }
546-
547- // Bat theme
548- p.SendOutput(progressChan, "Configuring bat theme...")
549- batThemeDir := fmt.Sprintf("%s/.config/bat/themes", userHome)
550- batCommands := []string{
551- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, batThemeDir),
552- fmt.Sprintf("su - %s -c 'cp %s/bat/bleu.tmTheme %s/'", username, themeDir, batThemeDir),
553- fmt.Sprintf("su - %s -c 'bat cache --build'", username),
554- }
555- for _, cmd := range batCommands {
556- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
557- }
558-
559- // Btop theme
560- p.SendOutput(progressChan, "Configuring btop theme...")
561- btopThemeDir := fmt.Sprintf("%s/.config/btop/themes", userHome)
562- btopCommands := []string{
563- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, btopThemeDir),
564- fmt.Sprintf("su - %s -c 'cp %s/btop/bleu.theme %s/'", username, themeDir, btopThemeDir),
565- }
566- for _, cmd := range btopCommands {
567- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
568- }
569-
570- // Yazi theme
571- p.SendOutput(progressChan, "Configuring yazi theme...")
572- yaziFlavorDir := fmt.Sprintf("%s/.config/yazi/flavors/bleu", userHome)
573- yaziCommands := []string{
574- fmt.Sprintf("su - %s -c 'mkdir -p %s'", username, yaziFlavorDir),
575- fmt.Sprintf("su - %s -c 'cp %s/yazi/bleu.toml %s/flavor.toml'", username, themeDir, yaziFlavorDir),
576- }
577- for _, cmd := range yaziCommands {
578- p.chrExec.ChrootExec(p.logger.LogPath(), config.PathMnt, cmd)
579- }
580-
581- p.SendOutput(progressChan, "[OK] CLI themes configured")
582- return nil
583-}
584-
585 // verifyInstallation performs comprehensive verification433 // verifyInstallation performs comprehensive verification
586 func (p *PostInstallPhase) verifyInstallation(progressChan chan<- ProgressUpdate) error {434 func (p *PostInstallPhase) verifyInstallation(progressChan chan<- ProgressUpdate) error {
587 p.SendOutput(progressChan, "Verifying installation components...")435 p.SendOutput(progressChan, "Verifying installation components...")
@@ -702,110 +550,3 @@ func (p *PostInstallPhase) CanSkip() bool {
702 return false550 return false
703 }551 }
704 552
705-// setupPostBoot downloads and configures first-boot service
706-func (p *PostInstallPhase) setupPostBoot(progressChan chan<- ProgressUpdate) error {
707- p.SendOutput(progressChan, "Configuring first-boot service...")
708-
709- if err := p.fs.MkdirAll(config.PathMntPostBoot, 0755); err != nil {
710- return fmt.Errorf("failed to create post-boot directory: %w", err)
711- }
712-
713- // Download logo.txt
714- p.SendOutput(progressChan, "Downloading logo.txt...")
715- logoURL := fmt.Sprintf("%s/logo.txt", p.config.RawURL)
716- resp, err := p.http.Get(logoURL)
717- switch {
718- case err != nil:
719- return fmt.Errorf("failed to download logo.txt: %w", err)
720- case resp.StatusCode != http.StatusOK:
721- resp.Body.Close()
722- return fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
723- }
724-
725- logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
726- logoFile, err := p.fs.Create(logoPath)
727- if err != nil {
728- resp.Body.Close()
729- return fmt.Errorf("failed to create logo.txt: %w", err)
730- }
731-
732- _, err = io.Copy(logoFile, resp.Body)
733- logoFile.Close()
734- resp.Body.Close()
735- if err != nil {
736- return fmt.Errorf("failed to save logo.txt: %w", err)
737- }
738-
739- // Download all post-boot scripts
740- for _, script := range config.PostBootScripts {
741- p.SendOutput(progressChan, fmt.Sprintf("Downloading %s...", script))
742-
743- scriptURL := fmt.Sprintf("%s/install/post-boot/%s", p.config.RawURL, script)
744- resp, err := p.http.Get(scriptURL)
745- switch {
746- case err != nil:
747- return fmt.Errorf("failed to download %s: %w", script, err)
748- case resp.StatusCode != http.StatusOK:
749- resp.Body.Close()
750- return fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
751- }
752-
753- scriptPath := filepath.Join(config.PathMntPostBoot, script)
754- scriptFile, err := p.fs.Create(scriptPath)
755- if err != nil {
756- resp.Body.Close()
757- return fmt.Errorf("failed to create %s: %w", script, err)
758- }
759-
760- _, err = io.Copy(scriptFile, resp.Body)
761- scriptFile.Close()
762- resp.Body.Close()
763-
764- if err != nil {
765- return fmt.Errorf("failed to save %s: %w", script, err)
766- }
767-
768- if err := p.fs.Chmod(scriptPath, 0755); err != nil {
769- return fmt.Errorf("failed to set permissions on %s: %w", script, err)
770- }
771- }
772-
773- p.SendOutput(progressChan, "[OK] Post-boot scripts downloaded")
774-
775- // Download service template
776- p.SendOutput(progressChan, "Creating systemd service...")
777- serviceURL := fmt.Sprintf("%s/%s", p.config.RawURL, config.PostBootServiceTemplate)
778- resp, err = p.http.Get(serviceURL)
779- switch {
780- case err != nil:
781- return fmt.Errorf("failed to download service template: %w", err)
782- case resp.StatusCode != http.StatusOK:
783- resp.Body.Close()
784- return fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
785- }
786-
787- templateBytes, err := io.ReadAll(resp.Body)
788- resp.Body.Close()
789- if err != nil {
790- return fmt.Errorf("failed to read service template: %w", err)
791- }
792-
793- serviceContent := string(templateBytes)
794- serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_EMAIL__", p.config.Email)
795- serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_USERNAME__", p.config.Username)
796-
797- servicePath := filepath.Join(config.PathMntSystemdSystem, config.PostBootServiceName)
798- if err := p.fs.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
799- return fmt.Errorf("failed to write service file: %w", err)
800- }
801-
802- p.SendOutput(progressChan, "[OK] Service file created")
803-
804- p.SendOutput(progressChan, "Enabling first-boot service...")
805- if err := p.chrExec.ChrootSystemctl(p.logger.LogPath(), config.PathMnt, "enable", config.PostBootServiceName); err != nil {
806- return fmt.Errorf("failed to enable service: %w", err)
807- }
808-
809- p.SendOutput(progressChan, "[OK] First-boot service enabled")
810- return nil
811-}

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