340 lines
9.0 KiB
Go
340 lines
9.0 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 {
|
|
model := &cliModel{}
|
|
|
|
app, err := core.New(stdout, stderr)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(stderr, "error: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
parser, err := kong.New(
|
|
model,
|
|
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
|
|
}
|
|
|
|
app.Quiet = model.Quiet
|
|
app.Pretend = model.Pretend || model.DryRun
|
|
|
|
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 {
|
|
Pretend bool `help:"Preview actions without executing commands."`
|
|
DryRun bool `name:"dry-run" help:"Alias for --pretend."`
|
|
Quiet bool `help:"Suppress status output."`
|
|
|
|
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"}
|
|
}
|
|
|
|
prefix, rest := splitLeadingGlobalFlags(args)
|
|
if len(rest) == 0 {
|
|
return args
|
|
}
|
|
|
|
switch rest[0] {
|
|
case "-h", "--help":
|
|
return []string{"--help"}
|
|
case "help":
|
|
if len(rest) == 1 {
|
|
return []string{"--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" {
|
|
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")
|
|
}
|