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 { Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."` } type pushCmd struct { Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."` } type commitCmd struct{} type destroyCmd struct{} type cdCmd struct{} type openCmd struct{} type execCmd struct{} type execAllCmd struct{} type rcCmd struct { Castle string `arg:"" optional:"" name:"CASTLE" help:"Castle name."` } type generateCmd struct{} func (c *pullCmd) Run(app *core.App) error { return app.Pull(defaultCastle(c.Castle)) } func (c *pushCmd) Run(app *core.App) error { return app.Push(defaultCastle(c.Castle)) } func (c *commitCmd) Run() error { return notImplemented("commit") } func (c *destroyCmd) Run() error { return notImplemented("destroy") } func (c *cdCmd) Run() error { return notImplemented("cd") } func (c *openCmd) Run() error { return notImplemented("open") } func (c *execCmd) Run() error { return notImplemented("exec") } func (c *execAllCmd) Run() error { return notImplemented("exec_all") } func (c *rcCmd) Run(app *core.App) error { return app.Rc(defaultCastle(c.Castle)) } func (c *generateCmd) Run() error { return notImplemented("generate") } 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") }