diff --git a/docker/behavior/Dockerfile b/docker/behavior/Dockerfile index 3b38b4a..d0e092b 100644 --- a/docker/behavior/Dockerfile +++ b/docker/behavior/Dockerfile @@ -12,7 +12,8 @@ FROM alpine:3.21 RUN apk add --no-cache \ bash \ ca-certificates \ - git + git \ + ruby WORKDIR /workspace COPY . /workspace diff --git a/internal/homesick/cli/cli.go b/internal/homesick/cli/cli.go index 345a3a5..f84f12e 100644 --- a/internal/homesick/cli/cli.go +++ b/internal/homesick/cli/cli.go @@ -257,21 +257,61 @@ func normalizeArgs(args []string) []string { return []string{"--help"} } - switch args[0] { + prefix, rest := splitLeadingGlobalFlags(args) + if len(rest) == 0 { + return args + } + + switch rest[0] { case "-h", "--help": return []string{"--help"} case "help": - if len(args) == 1 { + if len(rest) == 1 { return []string{"--help"} } - return append(args[1:], "--help") + normalized := append([]string{}, prefix...) + normalized = append(normalized, rest[1:]...) + return append(normalized, "--help") case "-v", "--version": return []string{"version"} + case "symlink": + normalized := append([]string{}, prefix...) + normalized = append(normalized, "link") + return append(normalized, rest[1:]...) + case "commit": + if len(rest) == 3 && !hasCommitMessageFlag(rest[1:]) { + normalized := append([]string{}, prefix...) + return append(normalized, "commit", "-m", rest[2], rest[1]) + } + return args default: return args } } +func splitLeadingGlobalFlags(args []string) ([]string, []string) { + i := 0 + for i < len(args) { + switch args[i] { + case "--pretend", "--dry-run", "--quiet": + i++ + default: + return args[:i], args[i:] + } + } + + return args, nil +} + +func hasCommitMessageFlag(args []string) bool { + for _, arg := range args { + if arg == "-m" || strings.HasPrefix(arg, "--MESSAGE") || strings.HasPrefix(arg, "--message") { + return true + } + } + return false +} + func isHelpRequest(args []string) bool { for _, arg := range args { if arg == "-h" || arg == "--help" { diff --git a/internal/homesick/core/core.go b/internal/homesick/core/core.go index ac40ecc..04ddca4 100644 --- a/internal/homesick/core/core.go +++ b/internal/homesick/core/core.go @@ -1,6 +1,7 @@ package core import ( + "bufio" "errors" "fmt" "io" @@ -17,6 +18,7 @@ import ( type App struct { HomeDir string ReposDir string + Stdin io.Reader Stdout io.Writer Stderr io.Writer Verbose bool @@ -34,6 +36,7 @@ func New(stdout io.Writer, stderr io.Writer) (*App, error) { return &App{ HomeDir: home, ReposDir: filepath.Join(home, ".homesick", "repos"), + Stdin: os.Stdin, Stdout: stdout, Stderr: stderr, }, nil @@ -173,6 +176,11 @@ func (a *App) PullAll() error { sort.Strings(castles) for _, castle := range castles { + if !a.Quiet { + if _, err := fmt.Fprintf(a.Stdout, "%s:\n", castle); err != nil { + return err + } + } if err := a.runGit(filepath.Join(a.ReposDir, castle), "pull"); err != nil { return fmt.Errorf("pull --all failed for %q: %w", castle, err) } @@ -220,6 +228,16 @@ func (a *App) Destroy(castle string) error { return err } + if !a.Force { + confirmed, confirmErr := a.confirmDestroy(castle) + if confirmErr != nil { + return confirmErr + } + if !confirmed { + return nil + } + } + // Only attempt unlinking managed home files for regular castle directories. if castleInfo.Mode()&os.ModeSymlink == 0 { castleHome := filepath.Join(castleRoot, "home") @@ -233,6 +251,25 @@ func (a *App) Destroy(castle string) error { return os.RemoveAll(castleRoot) } +func (a *App) confirmDestroy(castle string) (bool, error) { + reader := a.Stdin + if reader == nil { + reader = os.Stdin + } + + if _, err := fmt.Fprintf(a.Stdout, "Destroy castle %q? [y/N]: ", castle); err != nil { + return false, err + } + + line, err := bufio.NewReader(reader).ReadString('\n') + if err != nil && !errors.Is(err, io.EOF) { + return false, err + } + + response := strings.ToLower(strings.TrimSpace(line)) + return response == "y" || response == "yes", nil +} + func (a *App) Open(castle string) error { if strings.TrimSpace(castle) == "" { castle = "dotfiles"