home / skills / gentleman-programming / gentleman.dots / gentleman-installer

gentleman-installer skill

/skills/gentleman-installer

This skill helps you configure and expand installation steps in Gentleman.Dots TUI installer, ensuring OS-aware tooling, logging, and non-interactive support.

npx playbooks add skill gentleman-programming/gentleman.dots --skill gentleman-installer

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.3 KB
---
name: gentleman-installer
description: >
  Installation step patterns for Gentleman.Dots TUI installer.
  Trigger: When editing installer.go, adding installation steps, or modifying the installation flow.
license: Apache-2.0
metadata:
  author: gentleman-programming
  version: "1.0"
---

## 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`:

```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`:

```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`:

```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`:

```go
// 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

```go
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

```go
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

```go
// 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:

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

---

## Commands

```bash
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

Overview

This skill provides installation step patterns and conventions for the Gentleman.Dots TUI installer, focused on adding or modifying installation steps in installer.go. It documents the canonical InstallStep structure, registration and execution flows, interactive step handling, OS-specific execution, logging, and error wrapping for consistent, testable install steps.

How this skill works

The skill inspects the installer model and execution files to ensure new steps follow the InstallStep structure and are registered in SetupInstallSteps. It guides implementers to add a case in executeStep and create a dedicated step function in installer.go that handles OS variants, logs progress via SendLog, and returns wrapped StepError on failure. Interactive steps are flagged and executed with full terminal control when required.

When to use it

  • Adding a new tool installation step to the TUI installer
  • Modifying existing installation logic or step flow
  • Implementing backup/restore or config patching during install
  • Adding non-interactive CLI support for automated runs
  • Adding support for a new OS or platform like Termux

Best practices

  • Always define steps in model.SetupInstallSteps with StatusPending and correct Interactive flag
  • Put step logic in installer.go and add a switch case in executeStep
  • Handle all relevant OS variants (Mac, Arch, Debian, Termux) with system helpers
  • Stream progress with SendLog for each notable action and use log helper functions
  • Return wrapStepError for failures to preserve step context and user-friendly messages

Example use cases

  • Add a conditional InstallStep when a user opts into an optional tool
  • Create an interactive step that runs sudo commands and requires terminal control
  • Implement OS-specific package manager calls using system.RunBrewWithLogs or RunSudoWithLogs
  • Patch user config files after installing a shell or window manager
  • Skip steps when the target command already exists to make runs idempotent

FAQ

Where should I register a new install step?

Register it in SetupInstallSteps() inside the model, appending an InstallStep with ID, Name, Description, and Interactive flag as needed.

How do I run steps that require a password or full terminal?

Mark the step Interactive: true and ensure runNextStep routes to runInteractiveStep which uses tea.ExecProcess and system.RunSudoWithLogs for terminal control.