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

257 lines
6.7 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 {
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")
}