diff --git a/cmd/scheduledTasks.go b/cmd/scheduledTasks.go index 0f4f396..b379589 100644 --- a/cmd/scheduledTasks.go +++ b/cmd/scheduledTasks.go @@ -20,7 +20,7 @@ import ( "strconv" "strings" - "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" "github.com/apppackio/apppack/app" "github.com/apppackio/apppack/ui" "github.com/logrusorgru/aurora" @@ -123,20 +123,26 @@ If no index is provided, an interactive prompt will be provided to choose the ta for _, t := range tasks { taskList = append(taskList, fmt.Sprintf("%s %s", t.Schedule, t.Command)) } - questions := []*survey.Question{{ - Name: "task", - Prompt: &survey.Select{ - Message: "Scheduled task to delete:", - Options: taskList, - FilterMessage: "", - }, - }} - answers := make(map[string]int) + var selectedTask string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Scheduled task to delete:"). + Options(huh.NewOptions(taskList...)...). + Value(&selectedTask), + ), + ) ui.Spinner.Stop() - if err := survey.Ask(questions, &answers); err != nil { + if err := form.Run(); err != nil { checkErr(err) } - idx = answers["task"] + // Find index of selected task + for i, task := range taskList { + if task == selectedTask { + idx = i + break + } + } } task, err = a.DeleteScheduledTask(idx) checkErr(err) diff --git a/cmd/shell.go b/cmd/shell.go index a30fcb3..8556c51 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" "github.com/apppackio/apppack/app" "github.com/apppackio/apppack/ui" "github.com/aws/aws-sdk-go/aws" @@ -147,23 +147,30 @@ func interactiveCmd(a *app.App, cmd string) { arnParts := strings.Split(*t.TaskArn, "/") taskList = append(taskList, fmt.Sprintf("%s: %s", *tag, arnParts[len(arnParts)-1])) } - answers := make(map[string]interface{}) - questions := []*survey.Question{ - { - Name: "task", - Prompt: &survey.Select{ - Message: "Select task to connect to", - Options: taskList, - }, - }, - } + var selectedTask string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Select task to connect to"). + Options(huh.NewOptions(taskList...)...). + Value(&selectedTask), + ), + ) ui.Spinner.Stop() - if err := survey.Ask(questions, &answers); err != nil { + if err := form.Run(); err != nil { checkErr(err) } + // Find index of selected task + var selectedIndex int + for i, task := range taskList { + if task == selectedTask { + selectedIndex = i + break + } + } ui.StartSpinner() ecsSession, err := a.CreateEcsSession( - tasks[answers["task"].(survey.OptionAnswer).Index], + tasks[selectedIndex], exec, ) checkErr(err) diff --git a/go.mod b/go.mod index ef3a6cd..6fde6a2 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/apppackio/apppack go 1.22 require ( - github.com/AlecAivazis/survey/v2 v2.3.7 github.com/TylerBrock/saw v0.2.2 github.com/apparentlymart/go-cidr v1.1.0 github.com/aws/aws-sdk-go v1.51.7 github.com/briandowns/spinner v1.23.0 + github.com/charmbracelet/huh v0.6.0 github.com/dustin/go-humanize v1.0.1 github.com/getsentry/sentry-go v0.27.0 github.com/google/uuid v1.6.0 @@ -30,13 +30,28 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/twinj/uuid v0.0.0-20151029044442-89173bcdda19 // indirect github.com/xtaci/smux v1.5.24 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.8.0 // indirect ) require ( @@ -51,21 +66,19 @@ require ( github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 + golang.org/x/text v0.18.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 994df63..795c3cd 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/TylerBrock/colorjson v0.0.0-20180527164720-95ec53f28296/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= @@ -9,14 +7,34 @@ github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4t github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apppackio/saw v0.2.3-0.20210507180802-f6559c287e6f h1:4qSROTO6FceKFgKaoYmALA953QpYHRrQhcG1v2uqusU= github.com/apppackio/saw v0.2.3-0.20210507180802-f6559c287e6f/go.mod h1:GjKNeaxQeBkAudVlPmb2el62OMm4rjtY7Uzz1OmByAs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.13.56/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= github.com/aws/aws-sdk-go v1.35.7/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go v1.51.7 h1:RRjxHhx9RCjw5AhgpmmShq3F4JDlleSkyhYMQ2xUAe8= github.com/aws/aws-sdk-go v1.51.7/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/session-manager-plugin v0.0.0-20240103212942-e12e3d7a44af h1:mq6Swz3HVR1ZV9zkxgEj4ywg2R9s3MAAZXPVjPtz0U4= github.com/aws/session-manager-plugin v0.0.0-20240103212942-e12e3d7a44af/go.mod h1:7n17tunRPUsniNBu5Ja9C7WwJWTdOzaLqr/H0Ns3uuI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/cli/cli/v2 v2.25.1 h1:4xwJfPeo/uNMSrL2aeFbzOUHVB6N6XJSuQyHEF0Dn9E= @@ -27,9 +45,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -37,6 +52,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -61,8 +78,6 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hokaccha/go-prettyjson v0.0.0-20180920040306-f579f869bbfe/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e h1:0aewS5NTyxftZHSnFaJmWE5oCCrj4DyEXkAiMa1iZJM= github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= @@ -79,8 +94,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg= github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -96,22 +109,28 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.10/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.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/mum4k/termdash v0.20.0 h1:g6yZvE7VJmuefJmDrSrv5Az8IFTTSCqG0x8xiOMPbyM= github.com/mum4k/termdash v0.20.0/go.mod h1:/kPwGKcOhLawc2OmWJPLQ5nzR5PmcbiKMcVv9/413b4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -143,7 +162,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= @@ -171,15 +189,16 @@ golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -188,8 +207,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -199,10 +218,10 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/stacks/account.go b/stacks/account.go index ede71b2..ee90d63 100644 --- a/stacks/account.go +++ b/stacks/account.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" "github.com/apppackio/apppack/ui" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -69,17 +69,30 @@ func (a *AccountStack) UpdateFromFlags(flags *pflag.FlagSet) error { } func (a *AccountStack) AskQuestions(_ *session.Session) error { - return ui.AskQuestions([]*ui.QuestionExtra{ + var administrators = strings.Join(a.Parameters.Administrators, "\n") + err := ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: "Who can administer this account?", HelpText: "A list of email addresses (one per line) who have access to manage this AppPack account. These users will be assigned a permissive IAM policy in your AWS account and should be fully trusted with any resources within ", - Question: &survey.Question{ - Name: "Administrators", - Prompt: &survey.Multiline{Message: "Administrators"}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewText(). + Title("Administrators"). + Value(&administrators), + ), + ), }, }, a.Parameters) + if err != nil { + return err + } + // Convert administrators text back to slice + if administrators != "" { + a.Parameters.Administrators = strings.Split(administrators, "\n") + } else { + a.Parameters.Administrators = []string{} + } + return nil } func (*AccountStack) StackName(_ *string) *string { diff --git a/stacks/app_pipeline.go b/stacks/app_pipeline.go index 5fc5e4b..884f88f 100644 --- a/stacks/app_pipeline.go +++ b/stacks/app_pipeline.go @@ -1,7 +1,6 @@ package stacks import ( - "errors" "fmt" "math/rand" "os" @@ -9,8 +8,7 @@ import ( "strings" "time" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/core" + "github.com/charmbracelet/huh" "github.com/apppackio/apppack/auth" "github.com/apppackio/apppack/bridge" "github.com/apppackio/apppack/ddb" @@ -179,24 +177,27 @@ func (a *AppStack) AskForDatabase(sess *session.Session) error { "Answering yes will create a user and database and provide the credentials to the app as a config variable. " + "See https://docs.apppack.io/how-to/using-databases/ for more info." } + defaultValue := ui.BooleanAsYesNo(enable) + var selected = defaultValue err := ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: fmt.Sprintf("Should a database be created for this %s?", a.StackType()), HelpText: helpText, - WriteTo: &ui.BooleanOptionProxy{Value: &enable}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Database", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(enable), - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Database"). + Options(huh.NewOptions("yes", "no")...). + Value(&selected), + ), + ), }, }, a.Parameters) if err != nil { return err } + // Convert selection back to boolean + enable = (selected == "yes") if enable { canChange, err := a.CanChangeParameter("DatabaseStackName") @@ -213,16 +214,12 @@ func (a *AppStack) AskForDatabase(sess *session.Session) error { } // DatabaseStackParameters converts `{name} ({Engine})` -> `{stackName}` -func databaseSelectTransform(ans interface{}) interface{} { - o, ok := ans.(core.OptionAnswer) - if !ok { - return ans - } - if o.Value != "" { - parts := strings.Split(o.Value, " ") - o.Value = fmt.Sprintf(databaseStackNameTmpl, parts[0]) +func databaseSelectTransform(value string) string { + if value != "" { + parts := strings.Split(value, " ") + return fmt.Sprintf(databaseStackNameTmpl, parts[0]) } - return o + return value } // AskForDatabaseStack gives the user a choice of available database stacks @@ -255,20 +252,23 @@ func (a *AppStack) AskForDatabaseStack(sess *session.Session) error { } else { verbose = "Which database cluster should this app's database be setup on?" } + var selectedDatabase = databases[defaultDatabaseIdx] err = ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: verbose, - Question: &survey.Question{ - Name: "DatabaseStackName", - Prompt: &survey.Select{ - Message: "Database Cluster", - Options: databases, - Default: databases[defaultDatabaseIdx], - }, - Transform: databaseSelectTransform, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Database Cluster"). + Options(huh.NewOptions(databases...)...). + Value(&selectedDatabase), + ), + ), }, }, a.Parameters) + if err == nil { + a.Parameters.DatabaseStackName = databaseSelectTransform(selectedDatabase) + } if err != nil { return err } @@ -276,15 +276,11 @@ func (a *AppStack) AskForDatabaseStack(sess *session.Session) error { } // RedisStackParameters converts `{name}` -> `{stackName}` -func redisSelectTransform(ans interface{}) interface{} { - o, ok := ans.(core.OptionAnswer) - if !ok { - return ans - } - if o.Value != "" { - o.Value = fmt.Sprintf(redisStackNameTmpl, o.Value) +func redisSelectTransform(value string) string { + if value != "" { + return fmt.Sprintf(redisStackNameTmpl, value) } - return o + return value } func (a *AppStack) AskForRedis(sess *session.Session) error { @@ -301,24 +297,26 @@ func (a *AppStack) AskForRedis(sess *session.Session) error { "Answering yes will create a user and provide the credentials to the app as a config variable. " + "See https://docs.apppack.io/how-to/using-redis/ for more info." } + var redisSel = ui.BooleanAsYesNo(enable) err := ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: verbose, HelpText: helpText, - WriteTo: &ui.BooleanOptionProxy{Value: &enable}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Redis", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(enable), - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Redis"). + Options(huh.NewOptions("yes", "no")...). + Value(&redisSel), + ), + ), }, }, a.Parameters) if err != nil { return err } + // Convert selection back to boolean + enable = (redisSel == "yes") if enable { canChange, err := a.CanChangeParameter("RedisStackName") if err != nil { @@ -361,20 +359,23 @@ func (a *AppStack) AskForRedisStack(sess *session.Session) error { } else { verbose = "Which Redis instance should this app's user be setup on?" } + var selectedRedis = redises[defaultRedisIdx] err = ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: verbose, - Question: &survey.Question{ - Name: "RedisStackName", - Prompt: &survey.Select{ - Message: "Redis Cluster", - Options: redises, - Default: redises[defaultRedisIdx], - }, - Transform: redisSelectTransform, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Redis Cluster"). + Options(huh.NewOptions(redises...)...). + Value(&selectedRedis), + ), + ), }, }, a.Parameters) + if err == nil { + a.Parameters.RedisStackName = redisSelectTransform(selectedRedis) + } if err != nil { return err } @@ -392,24 +393,26 @@ func (a *AppStack) AskForSES() error { verbose = "Should this app be allowed to send email via Amazon SES?" helpText = "Allow this app to send email via SES. See https://docs.apppack.io/how-to/sending-email/ for more info." } + var sesSel = ui.BooleanAsYesNo(enable) err := ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: verbose, HelpText: helpText, - WriteTo: &ui.BooleanOptionProxy{Value: &enable}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "SES (email)", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(enable), - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("SES (email)"). + Options(huh.NewOptions("yes", "no")...). + Value(&sesSel), + ), + ), }, }, a.Parameters) if err != nil { return err } + // Convert selection back to boolean + enable = (sesSel == "yes") if enable { return a.AskForSESDomain() } @@ -429,11 +432,14 @@ func (a *AppStack) AskForSESDomain() error { { Verbose: verbose, HelpText: "Only allow outbound email via SES from a specific domain (e.g., example.com). Use `*` to allow sending on any domain approved for sending in SES.", - Question: &survey.Question{ - Name: "SesDomain", - Prompt: &survey.Input{Message: "SES Approved Domain", Default: a.Parameters.SesDomain}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("SES Approved Domain"). + Placeholder(a.Parameters.SesDomain). + Value(&a.Parameters.SesDomain), + ), + ), }, }, a.Parameters) if err != nil { @@ -474,11 +480,14 @@ func (a *AppStack) AskQuestions(sess *session.Session) error { // skipcq: GO-R10 questions = append(questions, &ui.QuestionExtra{ Verbose: fmt.Sprintf("What code repository should this %s build from?", a.StackType()), HelpText: "Use the HTTP URL (e.g., https://github.com/{org}/{repo}.git). BitBucket and Github repositories are supported.", - Question: &survey.Question{ - Name: "RepositoryUrl", - Prompt: &survey.Input{Message: "Repository URL", Default: a.Parameters.RepositoryUrl}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Repository URL"). + Placeholder(a.Parameters.RepositoryUrl). + Value(&a.Parameters.RepositoryUrl), + ), + ), }) if err = ui.AskQuestions(questions, a.Parameters); err != nil { return err @@ -491,35 +500,39 @@ func (a *AppStack) AskQuestions(sess *session.Session) error { // skipcq: GO-R10 return err } if !a.Pipeline { + var domainText = strings.Join(a.Parameters.Domains, "\n") questions = append(questions, []*ui.QuestionExtra{ { Verbose: "What branch should this app build from?", HelpText: "The deployment pipeline will be triggered on new pushes to this branch.", - Question: &survey.Question{ - Name: "Branch", - Prompt: &survey.Input{Message: "Branch", Default: a.Parameters.Branch}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Branch"). + Placeholder(a.Parameters.Branch). + Value(&a.Parameters.Branch), + ), + ), }, { Verbose: "Should the app be served on a custom domain? (Optional)", HelpText: "By default, the app will automatically be assigned a domain within the cluster. If you'd like it to respond on other domain(s), enter them here (one-per-line). See https://docs.apppack.io/how-to/custom-domains/ for more info.", - WriteTo: &ui.MultiLineValueProxy{Value: &a.Parameters.Domains}, - Question: &survey.Question{ - Prompt: &survey.Multiline{ - Message: "Custom Domain(s)", - Default: strings.Join(a.Parameters.Domains, "\n"), - }, - Validate: func(val interface{}) error { - domains := strings.Split(val.(string), "\n") - if len(domains) > 4 { - return errors.New("limit of 4 custom domains exceeded") - } - return nil - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewText(). + Title("Custom Domain(s)"). + Placeholder(domainText). + Value(&domainText), + ), + ), }, }...) + // Convert domainText back to slice + if domainText != "" { + a.Parameters.Domains = strings.Split(domainText, "\n") + } else { + a.Parameters.Domains = []string{} + } } var sqsVerbose string var sqsHelpText string @@ -534,59 +547,68 @@ func (a *AppStack) AskQuestions(sess *session.Session) error { // skipcq: GO-R10 bucketHelpTextApp = "the app" } + // Variables for boolean selections + var privateS3Sel = ui.BooleanAsYesNo(a.Parameters.PrivateS3BucketEnabled) + var publicS3Sel = ui.BooleanAsYesNo(a.Parameters.PublicS3BucketEnabled) + var sqsSel = ui.BooleanAsYesNo(a.Parameters.SQSQueueEnabled) + questions = append(questions, []*ui.QuestionExtra{ { Verbose: "What path should be used for healthchecks?", HelpText: "Enter a path (e.g., `/-/alive/`) that will always serve a 200 status code when the application is healthy.", - Question: &survey.Question{ - Name: "HealthCheckPath", - Prompt: &survey.Input{Message: "Healthcheck Path", Default: a.Parameters.HealthCheckPath}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Healthcheck Path"). + Placeholder(a.Parameters.HealthCheckPath). + Value(&a.Parameters.HealthCheckPath), + ), + ), }, { Verbose: fmt.Sprintf("Should a private S3 Bucket be created for this %s?", a.StackType()), HelpText: fmt.Sprintf("The S3 Bucket can be used to store files that should not be publicly accessible. Answering yes will create the bucket and provide its name to %s as a config variable. See https://docs.apppack.io/how-to/using-s3/ for more info.", bucketHelpTextApp), - WriteTo: &ui.BooleanOptionProxy{Value: &a.Parameters.PrivateS3BucketEnabled}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Private S3 Bucket", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(a.Parameters.PrivateS3BucketEnabled), - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Private S3 Bucket"). + Options(huh.NewOptions("yes", "no")...). + Value(&privateS3Sel), + ), + ), }, { Verbose: fmt.Sprintf("Should a public S3 Bucket be created for this %s?", a.StackType()), - HelpText: fmt.Sprintf("The S3 Bucket can be used to store files that should be publicly accessible. Answering yes will create the bucket and provide its name to %s as a config variable. See https://docs.apppack.io/how-to/using-s3/ for more info.", bucketHelpTextApp), - WriteTo: &ui.BooleanOptionProxy{Value: &a.Parameters.PublicS3BucketEnabled}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Public S3 Bucket", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(a.Parameters.PublicS3BucketEnabled), - }, - }, + HelpText: fmt.Sprintf("The S3 Bucket can be used to store files that should not be publicly accessible. Answering yes will create the bucket and provide its name to %s as a config variable. See https://docs.apppack.io/how-to/using-s3/ for more info.", bucketHelpTextApp), + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Public S3 Bucket"). + Options(huh.NewOptions("yes", "no")...). + Value(&publicS3Sel), + ), + ), }, { Verbose: sqsVerbose, HelpText: sqsHelpText, - WriteTo: &ui.BooleanOptionProxy{Value: &a.Parameters.SQSQueueEnabled}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "SQS Queue", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(a.Parameters.SQSQueueEnabled), - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("SQS Queue"). + Options(huh.NewOptions("yes", "no")...). + Value(&sqsSel), + ), + ), }, }...) if err = ui.AskQuestions(questions, a.Parameters); err != nil { return err } + // Convert selections back to booleans + a.Parameters.PrivateS3BucketEnabled = (privateS3Sel == "yes") + a.Parameters.PublicS3BucketEnabled = (publicS3Sel == "yes") + a.Parameters.SQSQueueEnabled = (sqsSel == "yes") if err := a.AskForDatabase(sess); err != nil { return err } @@ -597,20 +619,29 @@ func (a *AppStack) AskQuestions(sess *session.Session) error { // skipcq: GO-R10 return err } if a.Stack == nil { + var usersText = strings.Join(a.Parameters.AllowedUsers, "\n") err = ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: fmt.Sprintf("Who can manage this %s?", a.StackType()), HelpText: fmt.Sprintf("A list of email addresses (one per line) who have access to manage this %s via AppPack.", a.StackType()), - WriteTo: &ui.MultiLineValueProxy{Value: &a.Parameters.AllowedUsers}, - Question: &survey.Question{ - Prompt: &survey.Multiline{Message: "Users"}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewText(). + Title("Users"). + Value(&usersText), + ), + ), }, }, a.Parameters) if err != nil { return err } + // Convert usersText back to slice + if usersText != "" { + a.Parameters.AllowedUsers = strings.Split(usersText, "\n") + } else { + a.Parameters.AllowedUsers = []string{} + } } else if err = a.WarnIfDataLoss(); err != nil { return err } @@ -649,14 +680,16 @@ func (a *AppStack) WarnIfDataLoss() error { ui.PrintWarning("The current Redis database will no longer be accessible to the application.") } if privateS3BucketDestroy || publicS3BucketDestroy || databaseDestroy || redisDestroy { - var verify string - err := survey.AskOne(&survey.Select{ - Message: "Are you sure you want to continue?", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: "no", - }, &verify, nil) - if err != nil { + var verify = "no" + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Are you sure you want to continue?"). + Options(huh.NewOptions("yes", "no")...). + Value(&verify), + ), + ) + if err := form.Run(); err != nil { return err } if verify != "yes" { diff --git a/stacks/cluster.go b/stacks/cluster.go index 8a5fe46..27cd7cd 100644 --- a/stacks/cluster.go +++ b/stacks/cluster.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" "github.com/apparentlymart/go-cidr/cidr" "github.com/apppackio/apppack/bridge" "github.com/apppackio/apppack/ui" @@ -225,11 +225,14 @@ func (a *ClusterStack) AskQuestions(_ *session.Session) error { { Verbose: "What domain should be associated with this cluster?", HelpText: "Apps installed to this cluster will automatically get assigned a subdomain on the provided domain. The domain or a parent domain must already be setup as a Route53 Hosted Zone. See https://docs.apppack.io/how-to/bring-your-own-cluster-domain/ for more info.", - Question: &survey.Question{ - Name: "Domain", - Prompt: &survey.Input{Message: "Cluster Domain", Default: a.Parameters.Domain}, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Cluster Domain"). + Placeholder(a.Parameters.Domain). + Value(&a.Parameters.Domain), + ), + ), }, }...) if err = ui.AskQuestions(questions, a.Parameters); err != nil { diff --git a/stacks/database.go b/stacks/database.go index 021ecac..b4a0550 100644 --- a/stacks/database.go +++ b/stacks/database.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" "github.com/apppackio/apppack/bridge" "github.com/apppackio/apppack/ui" "github.com/aws/aws-sdk-go/aws" @@ -255,6 +255,7 @@ func (a *DatabaseStack) AskQuestions(sess *session.Session) error { var questions []*ui.QuestionExtra var err error var aurora bool + var auroraSel = ui.BooleanAsYesNo(aurora) if a.Stack == nil { err = AskForCluster( sess, @@ -270,35 +271,33 @@ func (a *DatabaseStack) AskQuestions(sess *session.Session) error { { Verbose: "What engine should this Database use?", HelpText: "", - Question: &survey.Question{ - Name: "Engine", - Prompt: &survey.Select{ - Message: "Type", - Options: []string{"postgres", "mysql"}, - FilterMessage: "", - Default: "postgres", - }, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Type"). + Options(huh.NewOptions("postgres", "mysql")...). + Value(&a.Parameters.Engine), + ), + ), }, { Verbose: "Should this Database use the Aurora engine variant?", HelpText: "Aurora provides many benefits over the standard engines, but is not available on very small instance sizes. For more info see https://aws.amazon.com/rds/aurora/.", - WriteTo: &ui.BooleanOptionProxy{Value: &aurora}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Aurora", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(aurora), - }, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Aurora"). + Options(huh.NewOptions("yes", "no")...). + Value(&auroraSel), + ), + ), }, }...) if err = ui.AskQuestions(questions, a.Parameters); err != nil { return err } + // Convert aurora selection back to boolean + aurora = (auroraSel == "yes") ui.StartSpinner() if aurora { a.Parameters.Engine, err = auroraEngineName(&a.Parameters.Engine) @@ -322,39 +321,42 @@ func (a *DatabaseStack) AskQuestions(sess *session.Session) error { } ui.Spinner.Stop() ui.Spinner.Suffix = "" + var multiAZSel = ui.BooleanAsYesNo(a.Parameters.MultiAZ) questions = append(questions, []*ui.QuestionExtra{ { Verbose: "What instance class should be used for this Database?", HelpText: "Enter the Database instance class. For more info see https://aws.amazon.com/rds/pricing/.", - Question: &survey.Question{ - Name: "InstanceClass", - Prompt: &survey.Select{ - Message: "Instance Class", - Options: instanceClasses, - FilterMessage: "", - Default: a.Parameters.InstanceClass, - }, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Instance Class"). + Options(huh.NewOptions(instanceClasses...)...). + Value(&a.Parameters.InstanceClass), + ), + ), }, { Verbose: "Should this Database be setup in multiple availability zones?", HelpText: "Multiple availability zones (AZs) provide more resilience in the case of an AZ outage, " + "but double the cost at AWS. In the case of Aurora databases, enabling multiple availability zones will give you access to a read-replica." + "For more info see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html.", - WriteTo: &ui.BooleanOptionProxy{Value: &a.Parameters.MultiAZ}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Multi AZ", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(a.Parameters.MultiAZ), - }, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Multi AZ"). + Options(huh.NewOptions("yes", "no")...). + Value(&multiAZSel), + ), + ), }, }...) - return ui.AskQuestions(questions, a.Parameters) + err = ui.AskQuestions(questions, a.Parameters) + if err != nil { + return err + } + // Convert multiAZ selection back to boolean + a.Parameters.MultiAZ = (multiAZSel == "yes") + return nil } func (*DatabaseStack) StackName(name *string) *string { diff --git a/stacks/redis.go b/stacks/redis.go index 3645d40..919d639 100644 --- a/stacks/redis.go +++ b/stacks/redis.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" "github.com/apppackio/apppack/bridge" "github.com/apppackio/apppack/ui" "github.com/aws/aws-sdk-go/aws" @@ -168,26 +168,28 @@ func (a *RedisStack) AskQuestions(sess *session.Session) error { a.Parameters.InstanceClass = DefaultRedisStackParameters.InstanceClass } + var multiAZSel = ui.BooleanAsYesNo(a.Parameters.MultiAZ) questions = append(questions, []*ui.QuestionExtra{ { Verbose: "Should this Redis instance be setup in multiple availability zones?", HelpText: "Multiple availability zones (AZs) provide more resilience in the case of an AZ outage, " + "but double the cost at AWS. For more info see " + "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/AutoFailover.html.", - WriteTo: &ui.BooleanOptionProxy{Value: &a.Parameters.MultiAZ}, - Question: &survey.Question{ - Prompt: &survey.Select{ - Message: "Multi AZ", - Options: []string{"yes", "no"}, - FilterMessage: "", - Default: ui.BooleanAsYesNo(a.Parameters.MultiAZ), - }, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Multi AZ"). + Options(huh.NewOptions("yes", "no")...). + Value(&multiAZSel), + ), + ), }, }...) if err = ui.AskQuestions(questions, a.Parameters); err != nil { return err } + // Convert multiAZ selection back to boolean + a.Parameters.MultiAZ = (multiAZSel == "yes") // Clear the questions slice so we can reuse it questions = questions[:0] @@ -204,16 +206,14 @@ func (a *RedisStack) AskQuestions(sess *session.Session) error { { Verbose: "What instance class should be used for this Redis instance?", HelpText: "Enter the Redis instance class. For more info see https://aws.amazon.com/elasticache/pricing/.", - Question: &survey.Question{ - Name: "InstanceClass", - Prompt: &survey.Select{ - Message: "Instance Class", - Options: instanceClasses, - FilterMessage: "", - Default: a.Parameters.InstanceClass, - }, - Validate: survey.Required, - }, + Form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Instance Class"). + Options(huh.NewOptions(instanceClasses...)...). + Value(&a.Parameters.InstanceClass), + ), + ), }, }...) return ui.AskQuestions(questions, a.Parameters) diff --git a/stacks/stacks.go b/stacks/stacks.go index 7897f31..917367e 100644 --- a/stacks/stacks.go +++ b/stacks/stacks.go @@ -7,8 +7,6 @@ import ( "strings" "time" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/core" "github.com/apppackio/apppack/ddb" "github.com/apppackio/apppack/ui" "github.com/aws/aws-sdk-go/aws/session" @@ -32,18 +30,12 @@ func AskForCluster(sess *session.Session, verbose, helpText string, response int if len(clusters) == 0 { return fmt.Errorf("no AppPack clusters are setup") } + var selectedCluster string return ui.AskQuestions([]*ui.QuestionExtra{ { Verbose: verbose, HelpText: helpText, - Question: &survey.Question{ - Name: "ClusterStackName", - Prompt: &survey.Select{ - Message: "Cluster", - Options: clusters, - }, - Transform: clusterSelectTransform, - }, + Form: ui.CreateSelectForm("Cluster", "ClusterStackName", clusters, &selectedCluster), }, }, response) } @@ -220,14 +212,3 @@ func DeleteStackAndWait(sess *session.Session, stack Stack) (*cloudformation.Sta return cfnStack, err } -// clusterSelectTransform converts `{name}` -> `{stackName}` -func clusterSelectTransform(ans interface{}) interface{} { - o, ok := ans.(core.OptionAnswer) - if !ok { - return ans - } - if o.Value != "" { - o.Value = fmt.Sprintf(clusterStackNameTmpl, o.Value) - } - return o -} diff --git a/ui/questions.go b/ui/questions.go index f58c8a6..729f7c0 100644 --- a/ui/questions.go +++ b/ui/questions.go @@ -4,16 +4,14 @@ import ( "fmt" "strings" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/core" + "github.com/charmbracelet/huh" "github.com/logrusorgru/aurora" ) type QuestionExtra struct { - Question *survey.Question + Form *huh.Form Verbose string HelpText string - WriteTo core.Settable } func BooleanAsYesNo(defaultValue bool) string { @@ -23,41 +21,9 @@ func BooleanAsYesNo(defaultValue bool) string { return "no" } -// BooleanOptionProxy allows setting a boolean value from a survey.Select question -type BooleanOptionProxy struct { - Value *bool -} - -func (b *BooleanOptionProxy) WriteAnswer(_ string, value interface{}) error { - ans, ok := value.(core.OptionAnswer) - if !ok { - return fmt.Errorf("unable to convert value to OptionAnswer") - } - - if ans.Value == "yes" { - *b.Value = true - } else { - *b.Value = false - } - return nil -} -// MultiLineValueProxy allows setting a []string value from a survey.Multiline question -type MultiLineValueProxy struct { - Value *[]string -} - -func (m *MultiLineValueProxy) WriteAnswer(_ string, value interface{}) error { - ans, ok := value.(string) - if !ok { - return fmt.Errorf("unable to convert value to string") - } - *m.Value = strings.Split(ans, "\n") - return nil -} - -// AskQuestions tweaks survey.Ask (and AskOne) to format things the way we want -func AskQuestions(questions []*QuestionExtra, response interface{}) error { +// AskQuestions migrated from survey to huh - provides formatted questions with help text +func AskQuestions(questions []*QuestionExtra, _ interface{}) error { for _, q := range questions { fmt.Println() fmt.Println(aurora.Bold(aurora.White(q.Verbose))) @@ -66,39 +32,34 @@ func AskQuestions(questions []*QuestionExtra, response interface{}) error { fmt.Println(q.HelpText) } fmt.Println() - if q.WriteTo == nil { - if err := survey.Ask([]*survey.Question{q.Question}, response, survey.WithShowCursor(true)); err != nil { - return err - } - } else { - if q.Question.Validate != nil { - if err := survey.AskOne(q.Question.Prompt, q.WriteTo, survey.WithShowCursor(true), survey.WithValidator(q.Question.Validate)); err != nil { - return err - } - } else { - if err := survey.AskOne(q.Question.Prompt, q.WriteTo, survey.WithShowCursor(true)); err != nil { - return err - } - } - } - var underline int - if p, ok := q.Question.Prompt.(*survey.Input); ok { - underline = len(p.Message) - } else if p, ok := q.Question.Prompt.(*survey.Select); ok { - underline = len(p.Message) - } else if p, ok := q.Question.Prompt.(*survey.Multiline); ok { - underline = len(p.Message) - } else if p, ok := q.Question.Prompt.(*survey.Password); ok { - underline = len(p.Message) + if err := q.Form.Run(); err != nil { + return err } + + // Forms directly update their target variables via Value() pointers + + // Get the underline length - simplified for now + var underline = 10 // Default underline length fmt.Println(aurora.Faint(strings.Repeat("─", 2+underline))) } return nil } +// CreateSelectForm creates a select form for single choice +func CreateSelectForm(title, _ string, options []string, target interface{}) *huh.Form { + return huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title(title). + Options(huh.NewOptions(options...)...). + Value(target.(*string)), + ), + ) +} + // PauseUntilEnter waits for the user to press enter func PauseUntilEnter(msg string) { fmt.Println(aurora.Bold(aurora.White(msg))) fmt.Scanln() -} +} \ No newline at end of file diff --git a/utils/utils.go b/utils/utils.go index a2ad5cf..f556e5d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,3 +1,3 @@ package utils -var AccountFlagHelpText string = "AWS account ID or alias (not needed if you are only the administrator of one account)" +var AccountFlagHelpText = "AWS account ID or alias (not needed if you are only the administrator of one account)"