From 3899291e15845f954a6480f47cee414a01b3a275 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Tue, 15 Oct 2024 16:34:34 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + actions.go | 136 ++++++++++++++++++++++++++++++++++++++ commands.go | 109 ++++++++++++++++++++++++++++++ commands_test.go | 27 ++++++++ go.mod | 51 ++++++++++++++ go.sum | 136 ++++++++++++++++++++++++++++++++++++++ main.go | 168 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 629 insertions(+) create mode 100644 .gitignore create mode 100644 actions.go create mode 100644 commands.go create mode 100644 commands_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a101bc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +jirashell diff --git a/actions.go b/actions.go new file mode 100644 index 0000000..9375d84 --- /dev/null +++ b/actions.go @@ -0,0 +1,136 @@ +package main + +import ( + "fmt" + "github.com/andygrunwald/go-jira" + "github.com/fatih/color" + "github.com/spf13/viper" + "os" + "strconv" +) + +func printError(msg string, err error) { + if err == nil { + fmt.Printf("Error: %s\n", msg) + } else { + fmt.Printf("Error: %s: %v\n", msg, err) + } +} + +func ActionExit(_ []string) { + promptRunner.Close() + os.Exit(0) +} + +func ActionLogout(_ []string) { + viper.Set("token", nil) + err := viper.WriteConfig() + if err != nil { + printError("Failed to write config", err) + } + ensureLoggedIn() +} + +func ActionListProjects(_ []string) { + projects, _, err := jiraClient.Project.GetList() + if err != nil { + printError("Failed to list projects", err) + return + } + for _, project := range *projects { + fmt.Printf(" - %s: %s\n", project.Key, project.Name) + } +} + +func ActionUseProject(args []string) { + if len(args) != 1 { + printError("Need a project ID", nil) + return + } + project, _, err := jiraClient.Project.Get(args[0]) + if err != nil { + printError("Failed to get project", err) + return + } + fmt.Printf("Using project %s (%s)\n", project.Name, project.Key) + viper.Set("project.id", project.ID) + viper.Set("project.key", project.Key) + SaveConfig() + UpdatePrompt() +} + +func ActionListBoards(_ []string) { + project := GetProjectId() + if project == "" { + printError("Please select a project first", nil) + return + } + list, _, err := jiraClient.Board.GetAllBoards(&jira.BoardListOptions{ProjectKeyOrID: project}) + if err != nil { + printError("Failed to list boards", err) + return + } + for _, board := range list.Values { + fmt.Printf(" - %s (%d)\n", board.Name, board.ID) + } +} + +func ActionUseBoard(args []string) { + if len(args) != 1 { + printError("Need a board ID", nil) + return + } + boardId, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + printError("Invalid board ID", nil) + return + } + board, _, err := jiraClient.Board.GetBoardConfiguration(int(boardId)) + if err != nil { + printError("Failed to get project", err) + return + } + fmt.Printf("Using board %s (%d)\n", board.Name, board.ID) + viper.Set("board.id", board.ID) + viper.Set("board.name", board.Name) + SaveConfig() + UpdatePrompt() +} + +func ActionUseCurrentSprint(_ []string) { + boardId := viper.GetInt("board.id") + list, _, err := jiraClient.Board.GetAllSprintsWithOptions(boardId, &jira.GetAllSprintsOptions{ + State: "active", + }) + if err != nil { + printError("Failed to get current board", err) + return + } + if len(list.Values) != 1 { + printError("There should be only one sprint at a time", nil) + return + } + sprint := list.Values[0] + fmt.Printf("Using sprint %s (%d)\n", sprint.Name, sprint.ID) + viper.Set("sprint.id", sprint.ID) + viper.Set("sprint.name", sprint.Name) + SaveConfig() + UpdatePrompt() +} + +func ActionListIssues(_ []string) { + sprintId := GetSprintId() + if sprintId == 0 { + printError("No sprint selected", nil) + return + } + issues, _, err := jiraClient.Sprint.GetIssuesForSprint(sprintId) + if err != nil { + printError("Failed to list issues", err) + } + for _, issue := range issues { + red := color.New(color.FgRed).SprintFunc() + yellow := color.New(color.FgYellow).SprintFunc() + fmt.Printf("%s - %s (%s)\n", red(issue.Key), issue.Fields.Summary, yellow(issue.Fields.Status.Name)) + } +} diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..9768f8b --- /dev/null +++ b/commands.go @@ -0,0 +1,109 @@ +package main + +import ( + "github.com/elk-language/go-prompt" + "strings" +) + +type Action func(args []string) + +type CommandArg struct { + Name string + Action Action + Help string + Children []CommandArg +} + +var commandIssues = CommandArg{Name: "issues"} + +func option(name string, children ...CommandArg) CommandArg { + return command(name, nil, children...) +} + +func command(name string, action Action, children ...CommandArg) CommandArg { + return CommandArg{ + Name: name, + Action: action, + Children: children, + } +} + +func (arg CommandArg) WithHelp(help string) CommandArg { + arg.Help = help + return arg +} + +var CommandTree = []CommandArg{ + option("list", + command("issues", ActionListIssues).WithHelp("List all issues in the current sprint"), + command("boards", ActionListBoards).WithHelp("List all boards in the current project"), + //command("sprints", ActionListIssues).WithHelp("List all issues in the current sprint"), + //option("open", option("issues")), + //option("closed", option("issues")), + command("projects", ActionListProjects).WithHelp("List all projects"), + ).WithHelp("List things"), + option("use", + command("project", ActionUseProject).WithHelp("Use project [project key]"), + command("board", ActionUseBoard).WithHelp("Use board [board id]"), + option("current", command("sprint", ActionUseCurrentSprint).WithHelp("Use the current sprint")), + ).WithHelp("Select something for further commands"), + //option("view", + // option("issue"), + // option("sprint"), + //), + command("exit", ActionExit).WithHelp("Exit jirashell"), + command("logout", ActionLogout).WithHelp("Logout from Jira"), +} + +func getCommandFor(parts []string, tree []CommandArg) (*CommandArg, []string) { + for _, arg := range tree { + if arg.Name == parts[0] { + if len(parts) == 1 { + return &arg, []string{} + } else { + result, args := getCommandFor(parts[1:], arg.Children) + if result != nil { + return result, args + } else { + return &arg, parts[1:] + } + } + } + } + return nil, nil +} + +func GetCommand(cmdline string) (*CommandArg, []string) { + commandParts := strings.Split(cmdline, " ") + command, args := getCommandFor(commandParts, CommandTree) + if command == nil || command.Action == nil { + return nil, nil + } + return command, args +} + +func GetSuggestionsForPart(parts []string, tree []CommandArg, suggests []prompt.Suggest) []prompt.Suggest { + for _, option := range tree { + if len(parts) == 0 { + suggests = append(suggests, prompt.Suggest{ + Text: option.Name, + Description: option.Help, + }) + } else if option.Name == parts[0] { + suggests = GetSuggestionsForPart(parts[1:], option.Children, suggests) + } else if len(parts) == 1 { + if strings.HasPrefix(option.Name, parts[0]) { + suggests = append(suggests, prompt.Suggest{ + Text: option.Name, + Description: option.Help, + }) + } + } + } + return suggests +} + +func GetSuggestionsFor(command string) []prompt.Suggest { + commandParts := strings.Split(command, " ") + return GetSuggestionsForPart(commandParts, CommandTree, []prompt.Suggest{}) +} diff --git a/commands_test.go b/commands_test.go new file mode 100644 index 0000000..d41ac77 --- /dev/null +++ b/commands_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestSuggestionsOneLetter(t *testing.T) { + suggestions := GetSuggestionsFor("l") + require.Len(t, suggestions, 2) + require.Equal(t, "list", suggestions[0].Text) + require.Equal(t, "logout", suggestions[1].Text) +} + +//func TestSuggestionsRoot(t *testing.T) { +// suggestions := GetSuggestionsFor("") +// require.Len(t, suggestions, 2) +// require.Equal(t, "list", suggestions[0].Text) +// require.Equal(t, "logout", suggestions[1].Text) +//} +// +//func TestSuggestionsFullPart(t *testing.T) { +// suggestions := GetSuggestionsFor("list") +// require.Len(t, suggestions, 2) +// require.Equal(t, "issues", suggestions[0].Text) +// require.Equal(t, "projects", suggestions[1].Text) +//} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e235c19 --- /dev/null +++ b/go.mod @@ -0,0 +1,51 @@ +module gitea.seeseepuff.com/seeseemelk/jirashell/v2 + +go 1.23.2 + +require ( + github.com/abiosoft/ishell/v2 v2.0.2 + github.com/adrg/xdg v0.5.0 + github.com/andygrunwald/go-jira v1.16.0 + github.com/elk-language/go-prompt v1.1.5 + github.com/pkg/errors v0.9.1 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/abiosoft/ishell v2.0.0+incompatible // indirect + github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-tty v0.0.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/trivago/tgo v1.0.7 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cc9edd3 --- /dev/null +++ b/go.sum @@ -0,0 +1,136 @@ +github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= +github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= +github.com/abiosoft/ishell/v2 v2.0.2 h1:5qVfGiQISaYM8TkbBl7RFO6MddABoXpATrsFbVI+SNo= +github.com/abiosoft/ishell/v2 v2.0.2/go.mod h1:E4oTCXfo6QjoCart0QYa5m9w4S+deXs/P/9jA77A9Bs= +github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= +github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= +github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= +github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= +github.com/andygrunwald/go-jira v1.16.0 h1:PU7C7Fkk5L96JvPc6vDVIrd99vdPnYudHu4ju2c2ikQ= +github.com/andygrunwald/go-jira v1.16.0/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elk-language/go-prompt v1.1.5 h1:/pGHSmEICQbaJltkFYZtcQlm0fQ8WO3CgISkUnYXxYY= +github.com/elk-language/go-prompt v1.1.5/go.mod h1:iEK3nFtQZuRxpoUVZk7Tie27TyWL+RMLkv4wA39XkWc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= +github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= +github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= +github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..53805b5 --- /dev/null +++ b/main.go @@ -0,0 +1,168 @@ +package main + +import ( + "github.com/abiosoft/ishell/v2" + "github.com/adrg/xdg" + "github.com/andygrunwald/go-jira" + prompt "github.com/elk-language/go-prompt" + pstrings "github.com/elk-language/go-prompt/strings" + "github.com/pkg/errors" + "github.com/spf13/viper" + "log" + "os" +) + +var configPath string +var shell *ishell.Shell +var jiraClient *jira.Client +var promptRunner *prompt.Prompt +var prefix = "> " + +func loadConfig() { + err := viper.ReadInConfig() + if err != nil { + log.Fatal(err) + } +} + +func SaveConfig() { + err := viper.WriteConfig() + if err != nil { + log.Fatal(err) + } +} + +func GetProjectId() string { + return viper.GetString("project.id") +} + +func GetSprintId() int { + return viper.GetInt("sprint.id") +} + +func UpdatePrompt() { + prefix = "" + project := viper.GetString("project.key") + if project != "" { + prefix = prefix + "p:" + project + " " + } + board := viper.GetString("board.name") + if board != "" { + prefix = prefix + "b:" + board + " " + } + sprint := viper.GetString("sprint.name") + if sprint != "" { + prefix = prefix + "s:" + sprint + " " + } + prefix = prefix[0:len(prefix)-1] + "> " +} + +func executor(in string) { + command, args := GetCommand(in) + if command == nil { + println("Invalid command", in) + return + } + command.Action(args) +} + +func completer(d prompt.Document) ([]prompt.Suggest, pstrings.RuneNumber, pstrings.RuneNumber) { + endIndex := d.CurrentRuneIndex() + w := d.GetWordBeforeCursor() + startIndex := endIndex - pstrings.RuneCount([]byte(w)) + s := GetSuggestionsFor(d.TextBeforeCursor()) + return prompt.FilterHasPrefix(s, w, true), startIndex, endIndex +} + +func main() { + // Open config file + configPath, err := xdg.ConfigFile("jirashell/config.toml") + if err != nil { + log.Fatal(err) + } + viper.SetConfigFile(configPath) + + _, err = os.Stat(configPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatal(err) + } else if err == nil { + loadConfig() + } + + ensureLoggedIn() + UpdatePrompt() + + promptRunner = prompt.New( + executor, + prompt.WithCompleter(completer), + prompt.WithTitle("jirashell"), + prompt.WithPrefixCallback(func() string { return prefix }), + ) + promptRunner.Run() +} + +func ensureLoggedIn() { + for { + username := viper.GetString("username") + host := viper.GetString("host") + token := viper.GetString("token") + + if host != "" && token != "" && username != "" { + println("Logging in...") + err := login(host, username, token) + if err != nil { + println("Could not log in:", err) + } else { + println("Logged in!") + SaveConfig() + return + } + } + + println() + println("A host name is required.") + host = prompt.Input(prompt.WithPrefix("Host: "), prompt.WithInitialText(host)) + if host == "" { + return + } + viper.Set("host", host) + + println() + println("A username is required.") + username = prompt.Input(prompt.WithPrefix("Username: "), prompt.WithInitialText(username)) + if username == "" { + return + } + viper.Set("username", username) + + println() + println("A token is required.") + token = prompt.Input(prompt.WithPrefix("Token: "), prompt.WithInitialText(token)) + if token == "" { + return + } + viper.Set("token", token) + } +} + +func login(host, username, token string) error { + var err error + tp := jira.BasicAuthTransport{ + Username: username, + Password: token, + } + jiraClient, err = jira.NewClient(tp.Client(), host) + if err != nil { + return err + } + + _, _, err = jiraClient.User.GetSelf() + if err != nil { + return err + } + return nil +} + +func noSuggestions(doc prompt.Document) []prompt.Suggest { + return make([]prompt.Suggest, 0) +}