Files
gosick/internal/homesick/cli/cli.go
2026-03-20 18:05:07 +00:00

291 lines
7.8 KiB
Go

package cli
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"git.hrafn.xyz/aether/gosick/internal/homesick/core"
"git.hrafn.xyz/aether/gosick/internal/homesick/version"
"github.com/alecthomas/kong"
)
func Run(args []string, stdout io.Writer, stderr io.Writer) int {
app, err := core.New(stdout, stderr)
if err != nil {
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
parser, err := kong.New(
&cliModel{},
kong.Name(programName()),
kong.Description("Your home is your castle. Don't leave your precious dotfiles behind."),
kong.Writers(stdout, stderr),
kong.Exit(func(int) {}),
kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
)
if err != nil {
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
normalizedArgs := normalizeArgs(args)
ctx, err := parser.Parse(normalizedArgs)
if err != nil {
var parseErr *kong.ParseError
if errors.As(err, &parseErr) {
if parseErr.ExitCode() == 0 || isHelpRequest(normalizedArgs) {
return 0
}
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
if parseErr.Context != nil {
_ = parseErr.Context.PrintUsage(false)
}
return parseErr.ExitCode()
}
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
if err := ctx.Run(app); err != nil {
var exitErr *cliExitError
if errors.As(err, &exitErr) {
_, _ = fmt.Fprintf(stderr, "error: %v\n", exitErr.err)
return exitErr.code
}
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
return 1
}
return 0
}
type cliModel struct {
Clone cloneCmd `cmd:"" help:"Clone a castle."`
List listCmd `cmd:"" help:"List castles."`
ShowPath showPathCmd `cmd:"" name:"show_path" help:"Show the path of a castle."`
Status statusCmd `cmd:"" help:"Show git status for a castle."`
Diff diffCmd `cmd:"" help:"Show git diff for a castle."`
Link linkCmd `cmd:"" help:"Symlink all dotfiles from a castle."`
Unlink unlinkCmd `cmd:"" help:"Unsymlink all dotfiles from a castle."`
Track trackCmd `cmd:"" help:"Track a file in a castle."`
Version versionCmd `cmd:"" help:"Display the current version."`
Pull pullCmd `cmd:"" help:"Pull the specified castle."`
Push pushCmd `cmd:"" help:"Push the specified castle."`
Commit commitCmd `cmd:"" help:"Commit the specified castle's changes."`
Destroy destroyCmd `cmd:"" help:"Destroy a castle."`
Cd cdCmd `cmd:"" help:"Print the path to a castle."`
Open openCmd `cmd:"" help:"Open a castle."`
Exec execCmd `cmd:"" help:"Execute a command in a castle."`
ExecAll execAllCmd `cmd:"" name:"exec_all" help:"Execute a command in every castle."`
Rc rcCmd `cmd:"" help:"Run rc hooks for a castle."`
Generate generateCmd `cmd:"" help:"Generate a castle."`
}
type cloneCmd struct {
URI string `arg:"" name:"URI" help:"Castle URI to clone."`
Destination string `arg:"" optional:"" name:"CASTLE_NAME" help:"Optional local castle name."`
}
func (c *cloneCmd) Run(app *core.App) error {
return app.Clone(c.URI, c.Destination)
}
type listCmd struct{}
func (c *listCmd) Run(app *core.App) error {
return app.List()
}
type showPathCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *showPathCmd) Run(app *core.App) error {
return app.ShowPath(defaultCastle(c.Castle))
}
type statusCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *statusCmd) Run(app *core.App) error {
return app.Status(defaultCastle(c.Castle))
}
type diffCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *diffCmd) Run(app *core.App) error {
return app.Diff(defaultCastle(c.Castle))
}
type linkCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *linkCmd) Run(app *core.App) error {
return app.LinkCastle(defaultCastle(c.Castle))
}
type unlinkCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *unlinkCmd) Run(app *core.App) error {
return app.Unlink(defaultCastle(c.Castle))
}
type trackCmd struct {
File string `arg:"" name:"FILE" help:"File to track."`
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
func (c *trackCmd) Run(app *core.App) error {
return app.Track(c.File, defaultCastle(c.Castle))
}
type versionCmd struct{}
func (c *versionCmd) Run(app *core.App) error {
return app.Version(version.String)
}
type pullCmd struct {
All bool `help:"Pull all castles."`
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type pushCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type commitCmd struct {
Message string `short:"m" required:"" name:"MESSAGE" help:"Commit message."`
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type destroyCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type cdCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type openCmd struct {
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type execCmd struct {
Castle string `arg:"" name:"CASTLE" help:"Castle name."`
Command []string `arg:"" name:"COMMAND" help:"Shell command to execute in the castle root."`
}
type execAllCmd struct {
Command []string `arg:"" name:"COMMAND" help:"Shell command to execute in each castle root."`
}
type rcCmd struct {
Force bool `help:"Bypass legacy .homesickrc safety confirmation."`
Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."`
}
type generateCmd struct {
Path string `arg:"" name:"PATH" help:"Path to generate a homesick-ready castle repository."`
}
func (c *pullCmd) Run(app *core.App) error {
if c.All {
if strings.TrimSpace(c.Castle) != "" {
return errors.New("pull accepts either --all or CASTLE, not both")
}
return app.PullAll()
}
return app.Pull(defaultCastle(c.Castle))
}
func (c *pushCmd) Run(app *core.App) error { return app.Push(defaultCastle(c.Castle)) }
func (c *commitCmd) Run(app *core.App) error {
return app.Commit(defaultCastle(c.Castle), c.Message)
}
func (c *destroyCmd) Run(app *core.App) error { return app.Destroy(defaultCastle(c.Castle)) }
func (c *cdCmd) Run(app *core.App) error { return app.ShowPath(defaultCastle(c.Castle)) }
func (c *openCmd) Run(app *core.App) error { return app.Open(defaultCastle(c.Castle)) }
func (c *execCmd) Run(app *core.App) error { return app.Exec(c.Castle, c.Command) }
func (c *execAllCmd) Run(app *core.App) error { return app.ExecAll(c.Command) }
func (c *rcCmd) Run(app *core.App) error {
originalForce := app.Force
app.Force = c.Force
err := app.Rc(defaultCastle(c.Castle))
app.Force = originalForce
return err
}
func (c *generateCmd) Run(app *core.App) error { return app.Generate(c.Path) }
func defaultCastle(castle string) string {
if strings.TrimSpace(castle) == "" {
return "dotfiles"
}
return castle
}
func programName() string {
if len(os.Args) > 0 {
if name := strings.TrimSpace(filepath.Base(os.Args[0])); name != "" {
return name
}
}
return "gosick"
}
func normalizeArgs(args []string) []string {
if len(args) == 0 {
return []string{"--help"}
}
switch args[0] {
case "-h", "--help":
return []string{"--help"}
case "help":
if len(args) == 1 {
return []string{"--help"}
}
return append(args[1:], "--help")
case "-v", "--version":
return []string{"version"}
default:
return args
}
}
func isHelpRequest(args []string) bool {
for _, arg := range args {
if arg == "-h" || arg == "--help" {
return true
}
}
return false
}
type cliExitError struct {
code int
err error
}
func (e *cliExitError) Error() string {
return e.err.Error()
}
func notImplemented(command string) error {
return &cliExitError{code: 2, err: fmt.Errorf("%s is not implemented in Go yet", command)}
}
func init() {
_ = os.Setenv("GIT_TERMINAL_PROMPT", "0")
}