cli/cmd/doctor/doctor.go

284 lines
7 KiB
Go
Raw Normal View History

// Package doctor provides environment check commands.
package doctor
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir"
)
// Style aliases
var (
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
)
// AddDoctorCommand adds the 'doctor' command to the given parent command.
func AddDoctorCommand(parent *clir.Cli) {
var verbose bool
doctorCmd := parent.NewSubCommand("doctor", "Check development environment")
doctorCmd.LongDescription("Checks that all required tools are installed and configured.\n" +
"Run this before `core setup` to ensure your environment is ready.")
doctorCmd.BoolFlag("verbose", "Show detailed version information", &verbose)
doctorCmd.Action(func() error {
return runDoctor(verbose)
})
}
type check struct {
name string
description string
command string
args []string
required bool
versionFlag string
}
func runDoctor(verbose bool) error {
fmt.Println("Checking development environment...")
fmt.Println()
checks := []check{
// Required tools
{
name: "Git",
description: "Version control",
command: "git",
args: []string{"--version"},
required: true,
versionFlag: "--version",
},
{
name: "GitHub CLI",
description: "GitHub integration (issues, PRs, CI)",
command: "gh",
args: []string{"--version"},
required: true,
versionFlag: "--version",
},
{
name: "PHP",
description: "Laravel packages",
command: "php",
args: []string{"-v"},
required: true,
versionFlag: "-v",
},
{
name: "Composer",
description: "PHP dependencies",
command: "composer",
args: []string{"--version"},
required: true,
versionFlag: "--version",
},
{
name: "Node.js",
description: "Frontend builds",
command: "node",
args: []string{"--version"},
required: true,
versionFlag: "--version",
},
// Optional tools
{
name: "pnpm",
description: "Fast package manager",
command: "pnpm",
args: []string{"--version"},
required: false,
versionFlag: "--version",
},
{
name: "Claude Code",
description: "AI-assisted development",
command: "claude",
args: []string{"--version"},
required: false,
versionFlag: "--version",
},
{
name: "Docker",
description: "Container runtime",
command: "docker",
args: []string{"--version"},
required: false,
versionFlag: "--version",
},
}
var passed, failed, optional int
fmt.Println("Required:")
for _, c := range checks {
if !c.required {
continue
}
ok, version := runCheck(c)
if ok {
if verbose && version != "" {
fmt.Printf(" %s %s %s\n", successStyle.Render("✓"), c.name, dimStyle.Render(version))
} else {
fmt.Printf(" %s %s\n", successStyle.Render("✓"), c.name)
}
passed++
} else {
fmt.Printf(" %s %s - %s\n", errorStyle.Render("✗"), c.name, c.description)
failed++
}
}
fmt.Println("\nOptional:")
for _, c := range checks {
if c.required {
continue
}
ok, version := runCheck(c)
if ok {
if verbose && version != "" {
fmt.Printf(" %s %s %s\n", successStyle.Render("✓"), c.name, dimStyle.Render(version))
} else {
fmt.Printf(" %s %s\n", successStyle.Render("✓"), c.name)
}
passed++
} else {
fmt.Printf(" %s %s - %s\n", dimStyle.Render("○"), c.name, dimStyle.Render(c.description))
optional++
}
}
// Check SSH
fmt.Println("\nGitHub Access:")
if checkGitHubSSH() {
fmt.Printf(" %s SSH key found\n", successStyle.Render("✓"))
} else {
fmt.Printf(" %s SSH key missing - run: ssh-keygen && gh ssh-key add\n", errorStyle.Render("✗"))
failed++
}
if checkGitHubCLI() {
fmt.Printf(" %s CLI authenticated\n", successStyle.Render("✓"))
} else {
fmt.Printf(" %s CLI authentication - run: gh auth login\n", errorStyle.Render("✗"))
failed++
}
// Check workspace
fmt.Println("\nWorkspace:")
registryPath, err := repos.FindRegistry()
if err == nil {
fmt.Printf(" %s Found repos.yaml at %s\n", successStyle.Render("✓"), registryPath)
reg, err := repos.LoadRegistry(registryPath)
if err == nil {
basePath := reg.BasePath
if basePath == "" {
basePath = "./packages"
}
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(filepath.Dir(registryPath), basePath)
}
if strings.HasPrefix(basePath, "~/") {
home, _ := os.UserHomeDir()
basePath = filepath.Join(home, basePath[2:])
}
// Count existing repos
allRepos := reg.List()
var cloned int
for _, repo := range allRepos {
repoPath := filepath.Join(basePath, repo.Name)
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
cloned++
}
}
fmt.Printf(" %s %d/%d repos cloned\n", successStyle.Render("✓"), cloned, len(allRepos))
}
} else {
fmt.Printf(" %s No repos.yaml found (run from workspace directory)\n", dimStyle.Render("○"))
}
// Summary
fmt.Println()
if failed > 0 {
fmt.Printf("%s %d issues found\n", errorStyle.Render("Doctor:"), failed)
fmt.Println("\nInstall missing tools:")
printInstallInstructions()
return fmt.Errorf("%d required tools missing", failed)
}
fmt.Printf("%s Environment ready\n", successStyle.Render("Doctor:"))
return nil
}
func runCheck(c check) (bool, string) {
cmd := exec.Command(c.command, c.args...)
output, err := cmd.CombinedOutput()
if err != nil {
return false, ""
}
// Extract first line as version
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) > 0 {
return true, strings.TrimSpace(lines[0])
}
return true, ""
}
func checkGitHubSSH() bool {
// Just check if SSH keys exist - don't try to authenticate
// (key might be locked/passphrase protected)
home, err := os.UserHomeDir()
if err != nil {
return false
}
sshDir := filepath.Join(home, ".ssh")
keyPatterns := []string{"id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"}
for _, key := range keyPatterns {
keyPath := filepath.Join(sshDir, key)
if _, err := os.Stat(keyPath); err == nil {
return true
}
}
return false
}
func checkGitHubCLI() bool {
cmd := exec.Command("gh", "auth", "status")
output, _ := cmd.CombinedOutput()
// Check for any successful login (even if there's also a failing token)
return strings.Contains(string(output), "Logged in to")
}
func printInstallInstructions() {
switch runtime.GOOS {
case "darwin":
fmt.Println(" brew install git gh php composer node pnpm docker")
fmt.Println(" brew install --cask claude")
case "linux":
fmt.Println(" # Install via your package manager or:")
fmt.Println(" # Git: apt install git")
fmt.Println(" # GitHub CLI: https://cli.github.com/")
fmt.Println(" # PHP: apt install php8.3-cli")
fmt.Println(" # Node: https://nodejs.org/")
fmt.Println(" # pnpm: npm install -g pnpm")
default:
fmt.Println(" See documentation for your OS")
}
}