gentleman-installer

SKILL.md

When to Use

Use this skill when:

  • Adding new installation steps
  • Modifying existing tool installations
  • Working on backup/restore functionality
  • Implementing non-interactive mode support
  • Adding new OS/platform support

Critical Patterns

Pattern 1: InstallStep Structure

All steps follow this structure in model.go:

type InstallStep struct {
    ID          string      // Unique identifier: "terminal", "shell", etc.
    Name        string      // Display name: "Install Fish"
    Description string      // Short description
    Status      StepStatus  // Pending, Running, Done, Failed, Skipped
    Progress    float64     // 0.0 - 1.0
    Error       error       // Error if failed
    Interactive bool        // Needs terminal control (sudo, chsh)
}

Pattern 2: Step Registration in SetupInstallSteps

Steps MUST be registered in SetupInstallSteps() in model.go:

func (m *Model) SetupInstallSteps() {
    m.Steps = []InstallStep{}

    // Conditional step based on user choice
    if m.Choices.SomeChoice {
        m.Steps = append(m.Steps, InstallStep{
            ID:          "newstep",
            Name:        "Install Something",
            Description: "Description here",
            Status:      StatusPending,
            Interactive: false, // true if needs sudo/password
        })
    }
}

Pattern 3: Step Execution in executeStep

All step logic goes in installer.go:

func executeStep(stepID string, m *Model) error {
    switch stepID {
    case "newstep":
        return stepNewStep(m)
    // ... other cases
    default:
        return fmt.Errorf("unknown step: %s", stepID)
    }
}

func stepNewStep(m *Model) error {
    stepID := "newstep"

    SendLog(stepID, "Starting installation...")

    // Check if already installed
    if system.CommandExists("newtool") {
        SendLog(stepID, "Already installed, skipping...")
        return nil
    }

    // Install based on OS
    var result *system.ExecResult
    if m.SystemInfo.IsTermux {
        result = system.RunPkgInstall("newtool", nil, func(line string) {
            SendLog(stepID, line)
        })
    } else {
        result = system.RunBrewWithLogs("install newtool", nil, func(line string) {
            SendLog(stepID, line)
        })
    }

    if result.Error != nil {
        return wrapStepError("newstep", "Install NewTool",
            "Failed to install NewTool",
            result.Error)
    }

    SendLog(stepID, "✓ NewTool installed")
    return nil
}

Pattern 4: Interactive Steps (sudo/password required)

Mark step as Interactive and use runInteractiveStep:

// In SetupInstallSteps:
m.Steps = append(m.Steps, InstallStep{
    ID:          "interactive_step",
    Name:        "Configure System",
    Description: "Requires password",
    Status:      StatusPending,
    Interactive: true,  // KEY: marks as interactive
})

// In runNextStep (update.go):
if step.Interactive {
    return runInteractiveStep(step.ID, &m)
}

Decision Tree

Adding new tool installation?
├── Add step to SetupInstallSteps() with conditions
├── Add case in executeStep() switch
├── Create step{Name}() function in installer.go
├── Handle all OS variants (Mac, Linux, Arch, Debian, Termux)
├── Use SendLog() for progress updates
└── Return wrapStepError() on failure

Step needs password/sudo?
├── Set Interactive: true in InstallStep
├── Use system.RunSudo() or system.RunSudoWithLogs()
└── Use tea.ExecProcess for full terminal control

Step should be conditional?
├── Check m.Choices.{option} before appending
├── Check m.SystemInfo for OS-specific logic
└── Use StatusSkipped if conditions not met

Code Examples

Example 1: OS-Specific Installation

func stepInstallTool(m *Model) error {
    stepID := "tool"

    if !system.CommandExists("tool") {
        SendLog(stepID, "Installing tool...")

        var result *system.ExecResult
        switch {
        case m.SystemInfo.IsTermux:
            result = system.RunPkgInstall("tool", nil, logFunc(stepID))
        case m.SystemInfo.OS == system.OSArch:
            result = system.RunSudoWithLogs("pacman -S --noconfirm tool", nil, logFunc(stepID))
        case m.SystemInfo.OS == system.OSMac:
            result = system.RunBrewWithLogs("install tool", nil, logFunc(stepID))
        default: // Debian/Ubuntu
            result = system.RunBrewWithLogs("install tool", nil, logFunc(stepID))
        }

        if result.Error != nil {
            return wrapStepError("tool", "Install Tool",
                "Failed to install tool",
                result.Error)
        }
    }

    // Copy configuration
    SendLog(stepID, "Copying configuration...")
    homeDir := os.Getenv("HOME")
    if err := system.CopyDir(filepath.Join("Gentleman.Dots", "ToolConfig/*"),
        filepath.Join(homeDir, ".config/tool/")); err != nil {
        return wrapStepError("tool", "Install Tool",
            "Failed to copy configuration",
            err)
    }

    SendLog(stepID, "✓ Tool configured")
    return nil
}

func logFunc(stepID string) func(string) {
    return func(line string) {
        SendLog(stepID, line)
    }
}

Example 2: Error Wrapping Pattern

func wrapStepError(stepID, stepName, description string, cause error) error {
    return &StepError{
        StepID:      stepID,
        StepName:    stepName,
        Description: description,
        Cause:       cause,
    }
}

// Usage:
if result.Error != nil {
    return wrapStepError("terminal", "Install Alacritty",
        "Failed to install Alacritty. Check your internet connection.",
        result.Error)
}

Example 3: Config Patching

// Patch config based on user choices
func stepInstallShell(m *Model) error {
    // ... install shell ...

    // Patch config for window manager choice
    configPath := filepath.Join(homeDir, ".config/fish/config.fish")
    if err := system.PatchFishForWM(configPath, m.Choices.WindowMgr, m.Choices.InstallNvim); err != nil {
        return wrapStepError("shell", "Install Fish",
            "Failed to configure window manager in shell",
            err)
    }

    return nil
}

Logging Pattern

Always use SendLog for step progress:

SendLog(stepID, "Starting...")           // Start
SendLog(stepID, "Downloading...")        // Progress
SendLog(stepID, "  → file.txt")          // Sub-item
SendLog(stepID, "✓ Step completed")      // Success

Commands

cd installer && go build ./cmd/gentleman-installer           # Build
./gentleman-installer --help                                  # Show help
./gentleman-installer --non-interactive --shell=fish         # Non-interactive
GENTLEMAN_VERBOSE=1 ./gentleman-installer --non-interactive  # Verbose logs

Resources

  • Steps: See installer/internal/tui/installer.go for step implementations
  • Model: See installer/internal/tui/model.go for SetupInstallSteps
  • System: See installer/internal/system/exec.go for command execution
  • Non-interactive: See installer/internal/tui/non_interactive.go for CLI mode
Weekly Installs
1
Installed on
opencode1
codex1
github-copilot1
claude-code1