diff --git a/go.mod b/go.mod index 8782b1d..084fb9b 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,45 @@ module github.com/open-cli-collective/hubspot-cli -go 1.22 +go 1.23.0 require ( + github.com/charmbracelet/huh v0.8.0 github.com/fatih/color v1.18.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 ) require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // 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.16.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.9 // indirect - golang.org/x/sys v0.25.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 87e7f92..65d1ce0 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,78 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +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/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +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.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 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= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.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-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.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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= @@ -19,11 +80,20 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/cmd/configcmd/configcmd.go b/internal/cmd/configcmd/configcmd.go index 3347df6..d69e203 100644 --- a/internal/cmd/configcmd/configcmd.go +++ b/internal/cmd/configcmd/configcmd.go @@ -1,7 +1,9 @@ package configcmd import ( + "fmt" "os" + "strings" "github.com/spf13/cobra" @@ -18,7 +20,6 @@ func Register(parent *cobra.Command, opts *root.Options) { Long: "Commands for managing hspt configuration and credentials.", } - cmd.AddCommand(newSetCmd(opts)) cmd.AddCommand(newShowCmd(opts)) cmd.AddCommand(newClearCmd(opts)) cmd.AddCommand(newTestCmd(opts)) @@ -26,44 +27,6 @@ func Register(parent *cobra.Command, opts *root.Options) { parent.AddCommand(cmd) } -func newSetCmd(opts *root.Options) *cobra.Command { - var token string - - cmd := &cobra.Command{ - Use: "set", - Short: "Set configuration values", - Long: "Set HubSpot credentials.", - Example: ` # Set access token - hspt config set --token YOUR_ACCESS_TOKEN - - # Using environment variable instead - export HUBSPOT_ACCESS_TOKEN=YOUR_ACCESS_TOKEN`, - RunE: func(cmd *cobra.Command, args []string) error { - v := opts.View() - - cfg, err := config.Load() - if err != nil { - return err - } - - if token != "" { - cfg.AccessToken = token - } - - if err := config.Save(cfg); err != nil { - return err - } - - v.Success("Configuration saved to %s", config.Path()) - return nil - }, - } - - cmd.Flags().StringVar(&token, "token", "", "HubSpot access token") - - return cmd -} - func newShowCmd(opts *root.Options) *cobra.Command { return &cobra.Command{ Use: "show", @@ -75,14 +38,7 @@ func newShowCmd(opts *root.Options) *cobra.Command { token := config.GetAccessToken() // Mask the token - maskedToken := "" - if token != "" { - if len(token) > 8 { - maskedToken = token[:4] + "..." + token[len(token)-4:] - } else { - maskedToken = "****" - } - } + maskedToken := maskToken(token) headers := []string{"KEY", "VALUE", "SOURCE"} rows := [][]string{ @@ -104,14 +60,42 @@ func newShowCmd(opts *root.Options) *cobra.Command { } } +func maskToken(token string) string { + if token == "" { + return "" + } + if len(token) <= 8 { + return "********" + } + return token[:4] + "********" + token[len(token)-4:] +} + func newClearCmd(opts *root.Options) *cobra.Command { - return &cobra.Command{ + var force bool + + cmd := &cobra.Command{ Use: "clear", Short: "Clear stored configuration", Long: "Remove the stored configuration file. Environment variables will still work.", + Example: ` # Clear with confirmation prompt + hspt config clear + + # Clear without confirmation + hspt config clear --force`, RunE: func(cmd *cobra.Command, args []string) error { v := opts.View() + if !force { + fmt.Print("This will remove all stored credentials. Continue? [y/N]: ") + var response string + _, _ = fmt.Scanln(&response) + response = strings.TrimSpace(strings.ToLower(response)) + if response != "y" && response != "yes" { + v.Info("Clear cancelled") + return nil + } + } + if err := config.Clear(); err != nil { return err } @@ -120,6 +104,10 @@ func newClearCmd(opts *root.Options) *cobra.Command { return nil }, } + + cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt") + + return cmd } func getTokenSource() string { diff --git a/internal/cmd/configcmd/configcmd_test.go b/internal/cmd/configcmd/configcmd_test.go new file mode 100644 index 0000000..43bf14a --- /dev/null +++ b/internal/cmd/configcmd/configcmd_test.go @@ -0,0 +1,48 @@ +package configcmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMaskToken(t *testing.T) { + tests := []struct { + name string + token string + expected string + }{ + { + name: "empty token", + token: "", + expected: "", + }, + { + name: "short token", + token: "abc", + expected: "********", + }, + { + name: "exactly 8 chars", + token: "12345678", + expected: "********", + }, + { + name: "9 chars", + token: "123456789", + expected: "1234********6789", + }, + { + name: "long token", + token: "abcd1234567890efghijklmnopqrstuv", + expected: "abcd********stuv", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := maskToken(tt.token) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/internal/cmd/initcmd/initcmd.go b/internal/cmd/initcmd/initcmd.go index 01aa2c1..93b70d6 100644 --- a/internal/cmd/initcmd/initcmd.go +++ b/internal/cmd/initcmd/initcmd.go @@ -1,10 +1,10 @@ package initcmd import ( - "bufio" "fmt" - "strings" + "os" + "github.com/charmbracelet/huh" "github.com/spf13/cobra" "github.com/open-cli-collective/hubspot-cli/api" @@ -45,124 +45,103 @@ Get your access token from: HubSpot Settings > Integrations > Private Apps`, parent.AddCommand(cmd) } -func runInit(opts *root.Options, token string, noVerify bool) error { - v := opts.View() - reader := bufio.NewReader(opts.Stdin) +func runInit(opts *root.Options, prefillToken string, noVerify bool) error { + configPath := config.Path() - v.Println("HubSpot CLI Setup") - v.Println("") - - // Check for existing config + // Load existing config for pre-population existingCfg, _ := config.Load() - if existingCfg.AccessToken != "" { - v.Warning("Existing configuration found at %s", config.Path()) - v.Println("") + if existingCfg == nil { + existingCfg = &config.Config{} + } - overwrite, err := promptYesNo(reader, "Overwrite existing configuration?", false) + // Check if config already exists + if _, err := os.Stat(configPath); err == nil { + var overwrite bool + err := huh.NewConfirm(). + Title("Configuration already exists"). + Description(fmt.Sprintf("Overwrite %s?", configPath)). + Value(&overwrite). + Run() if err != nil { return err } if !overwrite { - v.Info("Setup cancelled") + fmt.Println("Initialization cancelled.") return nil } - v.Println("") } - // Prompt for token if not provided - if token == "" { - v.Println("Enter your HubSpot access token") - v.Println(" Get one from: HubSpot Settings > Integrations > Private Apps") - v.Println("") + cfg := &config.Config{} - var err error - token, err = promptRequired(reader, "Access Token") - if err != nil { - return err - } + // Pre-fill from existing config, then override with CLI flags + // Priority: CLI flag > existing config value + if prefillToken != "" { + cfg.AccessToken = prefillToken + } else if existingCfg.AccessToken != "" { + cfg.AccessToken = existingCfg.AccessToken } - v.Println("") + // Build the form + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Access Token"). + Description("Get one from: HubSpot Settings > Integrations > Private Apps"). + EchoMode(huh.EchoModePassword). + Value(&cfg.AccessToken). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("access token is required") + } + return nil + }), + ), + ) + + if err := form.Run(); err != nil { + return err + } // Verify connection unless --no-verify if !noVerify { - v.Println("Testing connection...") + fmt.Print("Verifying connection... ") client, err := api.New(api.ClientConfig{ - AccessToken: token, + AccessToken: cfg.AccessToken, Verbose: opts.Verbose, }) if err != nil { + fmt.Println("failed!") return fmt.Errorf("failed to create client: %w", err) } owners, err := client.GetOwners() if err != nil { - if api.IsUnauthorized(err) { - v.Error("Authentication failed: invalid access token") - v.Println("") - v.Info("Check your token at: HubSpot Settings > Integrations > Private Apps") - return nil - } - if api.IsForbidden(err) { - v.Error("Authentication failed: missing required scopes") - v.Println("") - v.Info("Ensure your private app has the required scopes enabled") - return nil - } - return fmt.Errorf("connection test failed: %w", err) + fmt.Println("failed!") + fmt.Println() + fmt.Println("Troubleshooting:") + fmt.Println(" - Check your access token is correct") + fmt.Println(" - Ensure your private app has the required scopes") + fmt.Println() + fmt.Println("To get a new access token:") + fmt.Println(" HubSpot Settings > Integrations > Private Apps") + return fmt.Errorf("connection verification failed: %w", err) + } + fmt.Println("success!") + fmt.Println() + fmt.Printf("HubSpot account has %d owners\n", len(owners)) + if len(owners) > 0 { + fmt.Printf("First owner: %s (%s)\n", owners[0].FullName(), owners[0].Email) } - - v.Success("Connection verified (%d owners found)", len(owners)) - v.Println("") } // Save configuration - cfg := &config.Config{ - AccessToken: token, - } - if err := config.Save(cfg); err != nil { return fmt.Errorf("failed to save configuration: %w", err) } - v.Success("Configuration saved to %s", config.Path()) - v.Println("") - v.Println("Try it out:") - v.Println(" hspt config show") + fmt.Printf("\nConfiguration saved to %s\n", configPath) + fmt.Println("\nYou're all set! Try running:") + fmt.Println(" hspt config show") return nil } - -func promptRequired(reader *bufio.Reader, label string) (string, error) { - for { - fmt.Printf("%s: ", label) - input, err := reader.ReadString('\n') - if err != nil { - return "", err - } - input = strings.TrimSpace(input) - if input != "" { - return input, nil - } - fmt.Printf(" %s is required\n", label) - } -} - -func promptYesNo(reader *bufio.Reader, question string, defaultYes bool) (bool, error) { - suffix := " [y/N]: " - if defaultYes { - suffix = " [Y/n]: " - } - - fmt.Print(question + suffix) - input, err := reader.ReadString('\n') - if err != nil { - return false, err - } - input = strings.TrimSpace(strings.ToLower(input)) - - if input == "" { - return defaultYes, nil - } - return input == "y" || input == "yes", nil -}