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
4 changes: 2 additions & 2 deletions internal/cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"devctl/internal/config"
"devctl/internal/formats"
"devctl/internal/ui"
"devctl/internal/ui/component"
"devctl/internal/ui/view"
"devctl/internal/ui/widgets"
"devctl/pkg/pkgmgr"
"devctl/pkg/pkgmgr/scoop"
"devctl/pkg/version"
Expand Down Expand Up @@ -63,7 +63,7 @@ func runImport(cfg *config.Config, filePath string) error {
}

// Show spinner during cache building phase
prepSpinner := component.NewSpinner(output)
prepSpinner := widgets.NewSpinner(output)
prepSpinner.Start("Preparing context")

managerCache := make(map[pkgmgr.ManagerType]pkgmgr.Manager)
Expand Down
95 changes: 52 additions & 43 deletions internal/cmd/init.go → internal/cmd/init/init.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package cmd
package init

import (
"context"
"devctl/internal/config"
"devctl/internal/installer"
"devctl/internal/ui"
"devctl/internal/ui/widgets"
"devctl/pkg/executil"
"devctl/pkg/pkgmgr"
"fmt"
Expand All @@ -14,7 +15,7 @@ import (
"github.com/spf13/cobra"
)

func NewCmdInit(cfg *config.Config) *cobra.Command {
func NewCmd(cfg *config.Config) *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize configuration by detecting package managers",
Expand All @@ -28,36 +29,39 @@ func NewCmdInit(cfg *config.Config) *cobra.Command {
}

func runInit(cfg *config.Config) error {
out := ui.NewDefaultOutput()
output := ui.NewDefaultOutput()

currentPlatform := pkgmgr.GetCurrent()
detectResult := detectPackageManagers(currentPlatform)
displayDetectionResults(out, detectResult, currentPlatform)
displayDetectionResults(output, detectResult, currentPlatform)

uninstalled := getUninstalledManagers(detectResult)
if len(uninstalled) == 0 {
out.Println("")
return saveConfiguration(out, cfg, detectResult)
output.Println("")
return saveConfiguration(output, cfg, detectResult)
}

out.Println("")
confirmed, err := ui.ConfirmAutoInstall(len(uninstalled))
output.Println("")
confirmed, err := widgets.NewConfirmForm(widgets.ConfirmFormConfig{
Title: fmt.Sprintf("Found %d uninstalled package manager(s). Install automatically?", len(uninstalled)),
Desc: "This will execute installation scripts on your system.",
})
if err != nil {
return fmt.Errorf("failed to get user confirmation: %w", err)
}

if !confirmed {
out.Println("\nManual installation guides:")
output.Println("\nManual installation guides:")
for _, mgr := range uninstalled {
showManualInstallGuide(out, mgr.Type, string(currentPlatform))
showManualInstallGuide(output, mgr.Type, string(currentPlatform))
}
out.Println("")
return saveConfiguration(out, cfg, detectResult)
output.Println("")
return saveConfiguration(output, cfg, detectResult)
}

for _, mgr := range uninstalled {
if err := attemptAutoInstall(out, mgr.Type, string(currentPlatform), cfg.Debug); err != nil {
out.Error(fmt.Sprintf("Failed to install %s: %v", mgr.Type, err))
if err := attemptAutoInstall(output, mgr.Type, string(currentPlatform), cfg.Debug); err != nil {
output.Error(fmt.Sprintf("Failed to install %s: %v", mgr.Type, err))
continue
}

Expand All @@ -71,8 +75,8 @@ func runInit(cfg *config.Config) error {
}
}

out.Println("")
return saveConfiguration(out, cfg, detectResult)
output.Println("")
return saveConfiguration(output, cfg, detectResult)
}

type PackageManagerInfo struct {
Expand All @@ -98,17 +102,17 @@ func detectPackageManagers(p pkgmgr.Platform) map[pkgmgr.ManagerType]PackageMana
return managers
}

func displayDetectionResults(out ui.Output, results map[pkgmgr.ManagerType]PackageManagerInfo, p pkgmgr.Platform) {
managers := make([]ui.ManagerStatus, 0, len(results))
func displayDetectionResults(output ui.Output, results map[pkgmgr.ManagerType]PackageManagerInfo, p pkgmgr.Platform) {
managers := make([]ManagerStatus, 0, len(results))
for _, mgr := range results {
managers = append(managers, ui.ManagerStatus{
managers = append(managers, ManagerStatus{
Name: string(mgr.Type),
Installed: mgr.Installed,
Path: mgr.ExecutablePath,
})
}

out.PrintDetectionResults(ui.DetectionResult{
printDetectionResults(output, DetectionResult{
Platform: string(p),
Managers: managers,
})
Expand Down Expand Up @@ -146,47 +150,52 @@ func saveConfiguration(out ui.Output, cfg *config.Config, results map[pkgmgr.Man
return nil
}

func attemptAutoInstall(out ui.Output, managerType pkgmgr.ManagerType, platformStr string, debug bool) error {
func attemptAutoInstall(output ui.Output, managerType pkgmgr.ManagerType, platformStr string, debug bool) error {
inst := installer.GetInstaller(managerType)
if inst == nil {
return fmt.Errorf("no installer available for %s", managerType)
}

canAuto, err := inst.CanAutoInstall()
if !canAuto {
out.Error(fmt.Sprintf("%s: Automatic installation not available", managerType))
output.Error(fmt.Sprintf("%s: Automatic installation not available", managerType))

failedPrereqs := getFailedPrereqs(inst.GetPrerequisites())
if len(failedPrereqs) > 0 {
out.PrintPrerequisites(failedPrereqs)
out.Println("")
printPrerequisites(output, failedPrereqs)
output.Println("")
}

showGuide, _ := ui.ConfirmShowGuide()
showGuide, _ := widgets.NewConfirmForm(widgets.ConfirmFormConfig{
Title: "Show manual installation guide?",
})
if showGuide {
showManualInstallGuide(out, managerType, platformStr)
showManualInstallGuide(output, managerType, platformStr)
}
return fmt.Errorf("automatic installation not supported: %w", err)
}

out.Info(fmt.Sprintf("Installing %s...", managerType))
output.Info(fmt.Sprintf("Installing %s...", managerType))

cmd := inst.GetInstallCommand()
slog.Debug("installer command", slog.String("manager", string(managerType)), slog.String("cmd", cmd))
if debug {
out.PrintInstallCommand(cmd)
output.Printf("\nCommand to execute:\n %s\n\n", ui.DefaultStyles.Info.Render(cmd))
}

failedPrereqs := getFailedPrereqs(inst.GetPrerequisites())
if len(failedPrereqs) > 0 {
out.Error(fmt.Sprintf("%s: prerequisites not met for automatic installation", managerType))
out.PrintPrerequisites(failedPrereqs)
out.Println("")
showManualInstallGuide(out, managerType, platformStr)
output.Error(fmt.Sprintf("%s: prerequisites not met for automatic installation", managerType))
printPrerequisites(output, failedPrereqs)
output.Println("")
showManualInstallGuide(output, managerType, platformStr)
return fmt.Errorf("prerequisites not met for %s", managerType)
}

confirmed, err := ui.ConfirmProceed(string(managerType))
confirmed, err := widgets.NewConfirmForm(widgets.ConfirmFormConfig{
Title: fmt.Sprintf("Proceed with %s installation?", string(managerType)),
Desc: "This will modify your system PATH and configuration.",
})
if err != nil || !confirmed {
return fmt.Errorf("installation cancelled by user")
}
Expand All @@ -202,26 +211,26 @@ func attemptAutoInstall(out ui.Output, managerType pkgmgr.ManagerType, platformS
}()

for progress := range progressChan {
out.PrintInstallProgress(progress.Stage, progress.Message)
output.Printf("%s [%s] %s\n", ui.DefaultStyles.Info.Render(ui.IconInfo), progress.Stage, progress.Message)
}

if err := <-errChan; err != nil {
out.Error("Installation failed")
showManualInstallGuide(out, managerType, platformStr)
output.Error("Installation failed")
showManualInstallGuide(output, managerType, platformStr)
return err
}

out.Success(fmt.Sprintf("%s installed successfully!", managerType))
output.Success(fmt.Sprintf("%s installed successfully!", managerType))
return nil
}

func getFailedPrereqs(prereqs []installer.Prerequisite) []ui.PrerequisiteResult {
failed := make([]ui.PrerequisiteResult, 0, len(prereqs))
func getFailedPrereqs(prereqs []installer.Prerequisite) []PrerequisiteResult {
failed := make([]PrerequisiteResult, 0, len(prereqs))
for _, prereq := range prereqs {
if prereq.Passed {
continue
}
failed = append(failed, ui.PrerequisiteResult{
failed = append(failed, PrerequisiteResult{
Name: prereq.Name,
Passed: prereq.Passed,
Message: prereq.Message,
Expand All @@ -230,14 +239,14 @@ func getFailedPrereqs(prereqs []installer.Prerequisite) []ui.PrerequisiteResult
return failed
}

func showManualInstallGuide(out ui.Output, managerType pkgmgr.ManagerType, platformStr string) {
func showManualInstallGuide(output ui.Output, managerType pkgmgr.ManagerType, platformStr string) {
guide := installer.GetInstallGuide(managerType, platformStr)
if guide == nil {
out.Printf("No installation guide available for %s\n", managerType)
output.Printf("No installation guide available for %s\n", managerType)
return
}

out.PrintManualGuide(ui.ManualGuide{
printManualGuide(output, ManualGuide{
ManagerName: string(managerType),
Instructions: guide.Instructions,
URL: guide.URL,
Expand Down
91 changes: 91 additions & 0 deletions internal/cmd/init/ui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package init

import (
"devctl/internal/ui"
"fmt"
)

// DetectionResult represents package manager detection results.
type DetectionResult struct {
Platform string
Managers []ManagerStatus
}

// ManagerStatus represents the status of a single package manager.
type ManagerStatus struct {
Name string
Installed bool
Path string
}

// ManualGuide represents manual installation instructions.
type ManualGuide struct {
ManagerName string
Instructions []string
URL string
VerifyCmd string
}

// PrerequisiteResult represents a prerequisite check result.
type PrerequisiteResult struct {
Name string
Passed bool
Message string
}

// printDetectionResults displays package manager detection results.
func printDetectionResults(output ui.Output, result DetectionResult) {
output.Printf("\n%s\n", ui.DefaultStyles.Title.Render(fmt.Sprintf("Package Manager Detection (%s)", result.Platform)))
output.Printf("%s\n", ui.Separator(50))

for _, mgr := range result.Managers {
if mgr.Installed {
output.Printf("%s %-10s Installed at: %s\n",
ui.DefaultStyles.Success.Render(ui.IconSuccess),
mgr.Name,
mgr.Path)
} else {
output.Printf("%s %-10s Not installed\n",
ui.DefaultStyles.Error.Render(ui.IconError),
mgr.Name)
}
}
}

// printPrerequisites displays prerequisite check results.
func printPrerequisites(output ui.Output, prereqs []PrerequisiteResult) {
if len(prereqs) == 0 {
return
}
output.Println("Prerequisites:")
for _, prereq := range prereqs {
status := ui.DefaultStyles.Success.Render(ui.IconSuccess)
if !prereq.Passed {
status = ui.DefaultStyles.Error.Render(ui.IconError)
}
output.Printf(" %s %s: %s\n", status, prereq.Name, prereq.Message)
}
}

// printManualGuide displays manual installation instructions.
func printManualGuide(output ui.Output, guide ManualGuide) {
output.Printf("\n%s Manual Installation Guide for %s\n",
ui.DefaultStyles.Title.Render("📖"),
guide.ManagerName)

output.Printf("%s\n", ui.Separator(50))

for i, instruction := range guide.Instructions {
output.Printf("%d. %s\n", i+1, instruction)
}

if guide.URL != "" {
output.Printf("\nMore info: %s\n", ui.DefaultStyles.Info.Render(guide.URL))
}

if guide.VerifyCmd != "" {
output.Printf("Verify installation: %s\n", ui.DefaultStyles.Info.Render(guide.VerifyCmd))
}

output.Println("")
}
3 changes: 2 additions & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
initcmd "devctl/internal/cmd/init"
"devctl/internal/config"
"devctl/internal/logging"
"devctl/pkg/cmdutil"
Expand Down Expand Up @@ -30,7 +31,7 @@ func NewCmdRoot() (*cobra.Command, error) {

cmd.SetFlagErrorFunc(rootFlagErrorFunc)

cmd.AddCommand(NewCmdInit(cfg))
cmd.AddCommand(initcmd.NewCmd(cfg))
cmd.AddCommand(NewCmdImport(cfg))
cmd.AddCommand(NewCmdExport(cfg))
cmd.AddCommand(NewCmdSync(cfg))
Expand Down
8 changes: 4 additions & 4 deletions internal/cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"devctl/internal/config"
"devctl/internal/ui"
"devctl/internal/ui/component"
"devctl/internal/ui/widgets"
"devctl/pkg/pkgmgr"
"devctl/pkg/pkgmgr/brew"
"devctl/pkg/pkgmgr/scoop"
Expand All @@ -29,7 +29,7 @@ func NewCmdSync(cfg *config.Config) *cobra.Command {
func runSync(cfg *config.Config) error {
out := ui.NewDefaultOutput()

detectingSpinner := component.NewSpinner(out)
detectingSpinner := widgets.NewSpinner(out)
detectingSpinner.Start("Detecting package managers")

supportedTypes := filterSupportedManagers(cfg.PackageManagers)
Expand Down Expand Up @@ -64,7 +64,7 @@ func runSyncWithManagers(cfg *config.Config, managers map[pkgmgr.ManagerType]pkg
syncCounts := make(map[pkgmgr.ManagerType]int)
var warnings []string

spinner := component.NewSpinner(out)
spinner := widgets.NewSpinner(out)
spinner.Start("Scanning installed packages")

for managerType, mgr := range managers {
Expand All @@ -88,7 +88,7 @@ func runSyncWithManagers(cfg *config.Config, managers map[pkgmgr.ManagerType]pkg

cfg.Packages = config.MergePackages(cfg.Packages, allNewPackages)

saveConfigurationSpinner := component.NewSpinner(out)
saveConfigurationSpinner := widgets.NewSpinner(out)
saveConfigurationSpinner.Start("Saving configuration")
if err := config.SaveToFile(cfg, cfg.ConfigDir); err != nil {
return fmt.Errorf("failed to save config: %w", err)
Expand Down
Loading