From 6678624422466f0bee67633b5ed8d79c28e76017 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Sat, 12 Jul 2025 00:45:56 +1000 Subject: [PATCH 01/14] Add barebones implementation --- cmd/project.go | 66 ++++++--- go.mod | 23 ++++ go.sum | 66 +++++++++ internal/util/shared.go | 10 ++ internal/wizard/project/create.go | 217 ++++++++++++++++++++++++++++++ 5 files changed, 366 insertions(+), 16 deletions(-) create mode 100644 internal/util/shared.go create mode 100644 internal/wizard/project/create.go diff --git a/cmd/project.go b/cmd/project.go index 1b640282..4ba87ae7 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/uselagoon/lagoon-cli/internal/wizard/project" "strconv" "strings" @@ -143,11 +144,17 @@ var addProjectCmd = &cobra.Command{ if err != nil { return err } - - if err := requiredInputCheck("Project name", cmdProjectName, "git-url", gitUrl, "Production environment", productionEnvironment, "Deploytarget", strconv.Itoa(int(deploytarget))); err != nil { + interactive, err := cmd.Flags().GetBool("interactive") + if err != nil { return err } + if !interactive { + if err := requiredInputCheck("Project name", cmdProjectName, "git-url", gitUrl, "Production environment", productionEnvironment, "Deploytarget", strconv.Itoa(int(deploytarget))); err != nil { + return err + } + } + current := lagoonCLIConfig.Current token := lagoonCLIConfig.Lagoons[current].Token lc := lclient.New( @@ -157,19 +164,45 @@ var addProjectCmd = &cobra.Command{ &token, debug) - projectInput := schema.AddProjectInput{ - Name: cmdProjectName, - GitURL: gitUrl, - ProductionEnvironment: productionEnvironment, - StandbyProductionEnvironment: standbyProductionEnvironment, - Branches: branches, - PullRequests: pullrequests, - OpenshiftProjectPattern: deploytargetProjectPattern, - Openshift: deploytarget, - Subfolder: subfolder, - PrivateKey: privateKey, - BuildImage: buildImage, - RouterPattern: routerPattern, + projectInput := schema.AddProjectInput{} + if interactive { + config, err := project.RunCreateWizard(lc) + if err != nil { + return err + } + projectInput = config.Input + if config.OrganizationName != "" { + organizationName = config.OrganizationName + } + if config.AutoIdleProvided { + autoIdle = config.AutoIdle + autoIdleProvided = config.AutoIdleProvided + } + if config.StorageCalcProvided { + storageCalc = config.StorageCalc + storageCalcProvided = config.StorageCalcProvided + } + if config.DevEnvLimit != 0 { + developmentEnvironmentsLimit = config.DevEnvLimit + developmentEnvironmentsLimitProvided = true + } + } + + if !interactive { + projectInput = schema.AddProjectInput{ + Name: cmdProjectName, + GitURL: gitUrl, + ProductionEnvironment: productionEnvironment, + StandbyProductionEnvironment: standbyProductionEnvironment, + Branches: branches, + PullRequests: pullrequests, + OpenshiftProjectPattern: deploytargetProjectPattern, + Openshift: deploytarget, + Subfolder: subfolder, + PrivateKey: privateKey, + BuildImage: buildImage, + RouterPattern: routerPattern, + } } if orgOwnerProvided { projectInput.AddOrgOwner = &orgOwner @@ -210,7 +243,7 @@ var addProjectCmd = &cobra.Command{ Result: "success", ResultData: map[string]interface{}{ "Project Name": project.Name, - "GitURL": gitUrl, + "GitURL": projectInput.GitURL, }, } if organizationName != "" { @@ -765,6 +798,7 @@ func init() { addProjectCmd.Flags().Bool("owner", false, "Add the user as an owner of the project") addProjectCmd.Flags().StringP("organization-name", "O", "", "Name of the Organization to add the project to") addProjectCmd.Flags().UintP("organization-id", "", 0, "ID of the Organization to add the project to") + addProjectCmd.Flags().Bool("interactive", false, "Set Interactive mode for the project creation wizard.") listCmd.AddCommand(listProjectByMetadata) listProjectByMetadata.Flags().StringP("key", "K", "", "The key name of the metadata value you are querying on") diff --git a/go.mod b/go.mod index 753508ef..eb9e1e18 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/Masterminds/semver/v3 v3.3.1 + github.com/charmbracelet/huh v0.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-github/v66 v66.0.0 github.com/guregu/null v4.0.0+incompatible @@ -23,21 +24,43 @@ 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.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.4 // 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.8.0 // 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/chzyer/readline v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // 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/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 // 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/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 7679516c..5da4be92 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,43 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +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.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +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.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +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.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= +github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +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.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -12,8 +50,14 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -45,14 +89,28 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +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/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 h1:guHWmqIKr4G+gQ4uYU5vcZjsUhhklRA2uOcGVfcfqis= github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +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/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -77,11 +135,19 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/uselagoon/machinery v0.0.34 h1:5DsvXEyMeXmzQhjt11YH7+kZJueabovrwKTv0x7jQV8= github.com/uselagoon/machinery v0.0.34/go.mod h1:G0ujppuNR0BrtAnlmH8xDb9TDfayb4A36aeo0DYg7fQ= +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= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= diff --git a/internal/util/shared.go b/internal/util/shared.go new file mode 100644 index 00000000..3e1af48f --- /dev/null +++ b/internal/util/shared.go @@ -0,0 +1,10 @@ +package util + +import "regexp" + +func IsValidGitURL(url string) bool { + const pattern = `^((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)?$` + + re := regexp.MustCompile(pattern) + return re.MatchString(url) +} diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go new file mode 100644 index 00000000..2d1038b7 --- /dev/null +++ b/internal/wizard/project/create.go @@ -0,0 +1,217 @@ +package project + +import ( + "context" + "errors" + "fmt" + "github.com/charmbracelet/huh" + "github.com/uselagoon/lagoon-cli/internal/util" + "github.com/uselagoon/machinery/api/lagoon" + "github.com/uselagoon/machinery/api/lagoon/client" + "github.com/uselagoon/machinery/api/schema" + "log" + "strconv" +) + +type CreateConfig struct { + Input schema.AddProjectInput + OrganizationName string // need to update current schema in Machinery to utilize organizationDetails + AutoIdle bool + AutoIdleProvided bool + StorageCalc bool + StorageCalcProvided bool + DevEnvLimit uint +} + +func RunCreateWizard(lc *client.Client) (*CreateConfig, error) { + config := &CreateConfig{} + var organizationConfirm bool + initForm := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Enter the project name"). + Value(&config.Input.Name). + Validate(func(str string) error { + if str == "" { + return errors.New("Project name is required") + } + return nil + }), + huh.NewConfirm(). + Title("Do you want to create this project in an Organization?"). + Value(&organizationConfirm), + ), + ).WithTheme(huh.ThemeDracula()) + + formErr := initForm.Run() + if formErr != nil { + log.Fatal(formErr) + } + if organizationConfirm { + organizations, err := lagoon.AllOrganizations(context.TODO(), lc) + if err != nil { + return nil, err + } + form2 := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Select an organization"). + OptionsFunc(func() []huh.Option[string] { + options := make([]huh.Option[string], len(*organizations)) + for i, org := range *organizations { + options[i] = huh.NewOption(org.Name, org.Name) + } + + return options + }, &config.OrganizationName). + Value(&config.OrganizationName), + ), + ).WithTheme(huh.ThemeDracula()) + + err = form2.Run() + if err != nil { + fmt.Println("Error:", err) + } + } + + deploytargets, err := lagoon.ListDeployTargets(context.TODO(), lc) + var additionalFields bool + form3 := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Enter the Git URL for your project"). + Value(&config.Input.GitURL). + Validate(func(str string) error { + if !util.IsValidGitURL(config.Input.GitURL) { + return errors.New("Invalid Git URL") + } + return nil + }), + huh.NewInput(). + Title("Enter the branch name for the production environment"). + Value(&config.Input.ProductionEnvironment), + huh.NewSelect[uint](). + Title("Select a deploy target"). + OptionsFunc(func() []huh.Option[uint] { + options := make([]huh.Option[uint], len(*deploytargets)) + for i, target := range *deploytargets { + options[i] = huh.NewOption(target.Name, target.ID) + } + + return options + }, &config.Input.Openshift). + Value(&config.Input.Openshift), + huh.NewConfirm(). + Title("Do you want to define any other fields?"). + Value(&additionalFields), + ), + ).WithTheme(huh.ThemeDracula()) + + err = form3.Run() + if err != nil { + fmt.Println("Error:", err) + } + + if additionalFields { + var fields []string + form4 := huh.NewForm( + huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Select which fields you want to define"). + Options( + huh.NewOption("standby-production-environment", "StandbyProductionEnvironment"), + huh.NewOption("branches", "Branches"), + huh.NewOption("pullrequests", "PullRequests"), + huh.NewOption("deploytarget-project-pattern", "OpenshiftProjectPattern"), + huh.NewOption("development-environments-limit", "DevelopmentEnvironmentsLimit"), + huh.NewOption("auto-idle", "AutoIdle"), + huh.NewOption("subfolder", "Subfolder"), + huh.NewOption("private-key", "PrivateKey"), + huh.NewOption("build-image", "BuildImage"), + huh.NewOption("router-pattern", "RouterPattern"), + huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner"), + huh.NewOption("storage-calc", "StorageCalc "), + ). + Value(&fields), + ), + ).WithTheme(huh.ThemeDracula()) + + err = form4.Run() + if err != nil { + fmt.Println("Error:", err) + } + + if len(fields) == 0 { + fmt.Println("No fields selected.") + } + + var inputs []huh.Field + var devEnvLimit string + for _, field := range fields { + switch field { + case "StandbyProductionEnvironment": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.StandbyProductionEnvironment)) + case "Branches": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.Branches)) + case "PullRequests": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.PullRequests)) + case "OpenshiftProjectPattern": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.OpenshiftProjectPattern)) + case "Subfolder": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.Subfolder)) + case "PrivateKey": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.PrivateKey)) + case "BuildImage": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.BuildImage)) + case "RouterPattern": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.RouterPattern)) + + case "DevelopmentEnvironmentsLimit": + inputs = append(inputs, huh.NewInput(). + Title(fmt.Sprintf("Enter value for '%s'", field)). + Value(&devEnvLimit). + Validate(func(s string) error { + if s == "" { + return nil + } + _, err := strconv.ParseUint(s, 10, 64) + return err + }), + ) + + case "StorageCalc": + config.StorageCalcProvided = true + inputs = append(inputs, huh.NewConfirm(). + Title(field). + Value(&config.StorageCalc)) + case "AutoIdle": + config.AutoIdleProvided = true + inputs = append(inputs, huh.NewConfirm(). + Title(field). + Value(&config.AutoIdle)) + case "AddOrgOwner": + inputs = append(inputs, huh.NewConfirm().Title(field).Value(config.Input.AddOrgOwner)) + } + } + + form5 := huh.NewForm( + huh.NewGroup(inputs...), + ).WithTheme(huh.ThemeDracula()) + + err = form5.Run() + if err != nil { + fmt.Println("Error:", err) + } + + if devEnvLimit != "" { + developmentEnvLimit, err := strconv.Atoi(devEnvLimit) + if err != nil { + fmt.Println("Error:", err) + } + config.DevEnvLimit = uint(developmentEnvLimit) + } + + } + + return config, nil +} From 343c5883c0f325ac6fa41056449b99955d7d8f64 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Fri, 18 Jul 2025 17:39:22 +1000 Subject: [PATCH 02/14] Adds additional details around org selection --- cmd/project.go | 15 +++- go.mod | 4 + internal/util/shared.go | 123 +++++++++++++++++++++++++++++- internal/wizard/project/create.go | 118 ++++++++++++++++------------ 4 files changed, 208 insertions(+), 52 deletions(-) diff --git a/cmd/project.go b/cmd/project.go index 4ba87ae7..afcdbf5a 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -3,7 +3,10 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" + "github.com/charmbracelet/huh" + "github.com/uselagoon/lagoon-cli/internal/util" "github.com/uselagoon/lagoon-cli/internal/wizard/project" "strconv" @@ -148,6 +151,7 @@ var addProjectCmd = &cobra.Command{ if err != nil { return err } + generatedCommand := "" if !interactive { if err := requiredInputCheck("Project name", cmdProjectName, "git-url", gitUrl, "Production environment", productionEnvironment, "Deploytarget", strconv.Itoa(int(deploytarget))); err != nil { @@ -168,11 +172,14 @@ var addProjectCmd = &cobra.Command{ if interactive { config, err := project.RunCreateWizard(lc) if err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return nil + } return err } projectInput = config.Input - if config.OrganizationName != "" { - organizationName = config.OrganizationName + if config.OrganizationDetails.Name != "" { + organizationName = config.OrganizationDetails.Name } if config.AutoIdleProvided { autoIdle = config.AutoIdle @@ -186,6 +193,7 @@ var addProjectCmd = &cobra.Command{ developmentEnvironmentsLimit = config.DevEnvLimit developmentEnvironmentsLimitProvided = true } + generatedCommand = util.GenerateCLICommand(config) } if !interactive { @@ -249,6 +257,9 @@ var addProjectCmd = &cobra.Command{ if organizationName != "" { resultData.ResultData["Organization"] = organizationName } + if interactive { + resultData.ResultData["Generated Command"] = "lagoon add project" + generatedCommand + } r := output.RenderResult(resultData, outputOptions) fmt.Fprintf(cmd.OutOrStdout(), "%s", r) return nil diff --git a/go.mod b/go.mod index eb9e1e18..3aed9d2a 100644 --- a/go.mod +++ b/go.mod @@ -65,3 +65,7 @@ require ( golang.org/x/text v0.24.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) + +replace ( + github.com/uselagoon/machinery => ../machinery +) \ No newline at end of file diff --git a/internal/util/shared.go b/internal/util/shared.go index 3e1af48f..b5e426b8 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -1,6 +1,49 @@ package util -import "regexp" +import ( + "fmt" + "github.com/uselagoon/machinery/api/schema" + "reflect" + "regexp" + "slices" + "strconv" +) + +type CreateConfig struct { + Input schema.AddProjectInput + OrganizationDetails schema.Organization + AutoIdle bool + AutoIdleProvided bool + StorageCalc bool + StorageCalcProvided bool + DevEnvLimit uint +} + +var fieldCmdMap = map[string]string{ + "Name": "--project", + "GitURL": "--git-url", + "Subfolder": "--subfolder", + "Openshift": "--deploytarget", + "OpenshiftProjectPattern": "--deploytarget-project-pattern", + "Branches": "--branches", + "PullRequests": "--pullrequests", + "ProductionEnvironment": "--production-environment", + "StandbyProductionEnvironment": "--standby-production-environment", + "Availability": "--availability", + "AutoIdle": "--auto-idle", + "StorageCalc": "--storage-calc", + "DevelopmentEnvironmentsLimit": "--development-environments-limit", + "PrivateKey": "--private-key", + "BuildImage": "--build-image", + "Organization": "--organization-id", + "AddOrgOwner": "--owner", + "RouterPattern": "--router-pattern", + "ProblemsUI": "--problems-ui", + "FactsUI": "--facts-ui", + "ProductionBuildPriority": "--production-build-priority", + "DevelopmentBuildPriority": "--development-build-priority", + "DeploymentsDisabled": "--deployments-disabled", +} func IsValidGitURL(url string) bool { const pattern = `^((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)?$` @@ -8,3 +51,81 @@ func IsValidGitURL(url string) bool { re := regexp.MustCompile(pattern) return re.MatchString(url) } + +func QuotaCheck(quota int) string { + quotaRoute := strconv.Itoa(quota) + if quota < 0 { + quotaRoute = "∞" + } + return quotaRoute +} + +func IsValidProjectName(name string) (bool, string) { + casePattern := regexp.MustCompile("[^0-9a-z-]") + dashPattern := regexp.MustCompile("--") + + if name == "" { + return false, "Project name is required" + } + if casePattern.MatchString(name) { + return false, "Project name is invalid, only lowercase characters, numbers and dashes allowed for name" + } + if dashPattern.MatchString(name) { + return false, "Multiple consecutive dashes are not allowed for name" + } + return true, "" +} + +func GenerateCLICommand(config *CreateConfig) string { + cmd := "" + configFields := config.Input + configType := reflect.TypeOf(configFields) + configValue := reflect.ValueOf(configFields) + boolFields := []string{ + "--auto-idle", + "--storage-calc", + } + + for i := 0; i < configType.NumField(); i++ { + fieldName := configType.Field(i).Name + fieldValue := configValue.Field(i) + + if !fieldValue.IsZero() { + if flag, exists := fieldCmdMap[fieldName]; exists { + if flag == "--owner" { + cmd += " " + fmt.Sprintf("%s=%v", flag, *(fieldValue.Interface().(*bool))) + } else { + cmd += " " + fmt.Sprintf("%s %v", flag, fieldValue.Interface()) + } + } + } + } + + configBoolType := reflect.TypeOf(*config) + configBoolValue := reflect.ValueOf(*config) + + for i := 0; i < configBoolType.NumField(); i++ { + fieldName := configBoolType.Field(i).Name + fieldValue := configBoolValue.Field(i) + + var fieldProvided bool + + switch fieldName { + case "AutoIdle": + fieldProvided = config.AutoIdleProvided + case "StorageCalc": + fieldProvided = config.StorageCalcProvided + } + + if fieldProvided { + if flag, exists := fieldCmdMap[fieldName]; exists { + if slices.Contains(boolFields, flag) { + boolVal := fieldValue.Interface().(bool) + cmd += " " + fmt.Sprintf("%s=%v", flag, boolVal) + } + } + } + } + + return cmd +} diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index 2d1038b7..8e3271e5 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -5,35 +5,25 @@ import ( "errors" "fmt" "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" "github.com/uselagoon/lagoon-cli/internal/util" "github.com/uselagoon/machinery/api/lagoon" "github.com/uselagoon/machinery/api/lagoon/client" - "github.com/uselagoon/machinery/api/schema" - "log" "strconv" ) -type CreateConfig struct { - Input schema.AddProjectInput - OrganizationName string // need to update current schema in Machinery to utilize organizationDetails - AutoIdle bool - AutoIdleProvided bool - StorageCalc bool - StorageCalcProvided bool - DevEnvLimit uint -} - -func RunCreateWizard(lc *client.Client) (*CreateConfig, error) { - config := &CreateConfig{} +func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { + config := &util.CreateConfig{} var organizationConfirm bool initForm := huh.NewForm( huh.NewGroup( huh.NewInput(). Title("Enter the project name"). Value(&config.Input.Name). - Validate(func(str string) error { - if str == "" { - return errors.New("Project name is required") + Validate(func(name string) error { + valid, errMsg := util.IsValidProjectName(name) + if !valid { + return errors.New(errMsg) } return nil }), @@ -41,14 +31,14 @@ func RunCreateWizard(lc *client.Client) (*CreateConfig, error) { Title("Do you want to create this project in an Organization?"). Value(&organizationConfirm), ), - ).WithTheme(huh.ThemeDracula()) + ).WithTheme(huh.ThemeCatppuccin()) formErr := initForm.Run() if formErr != nil { - log.Fatal(formErr) + return nil, formErr } if organizationConfirm { - organizations, err := lagoon.AllOrganizations(context.TODO(), lc) + organizations, err := lagoon.AllOrganizationsExtended(context.TODO(), lc) // requires machinery changes current - todo: change to raw if err != nil { return nil, err } @@ -59,18 +49,35 @@ func RunCreateWizard(lc *client.Client) (*CreateConfig, error) { OptionsFunc(func() []huh.Option[string] { options := make([]huh.Option[string], len(*organizations)) for i, org := range *organizations { - options[i] = huh.NewOption(org.Name, org.Name) + orgProjectCount := len(org.Projects) + if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { + quotaFullLabel := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(fmt.Sprintf("%s | Project Limit: %v/%v | ⚠️ Project Quota full, cannot assigned project to this organization.", org.Name, orgProjectCount, org.QuotaProject)) + options[i] = huh.NewOption(quotaFullLabel, org.Name) + } else { + options[i] = huh.NewOption(fmt.Sprintf("%s | Project Limit: %v/%v", org.Name, orgProjectCount, util.QuotaCheck(org.QuotaProject)), org.Name) + } } return options - }, &config.OrganizationName). - Value(&config.OrganizationName), + }, &config.OrganizationDetails.Name). + Value(&config.OrganizationDetails.Name). + Validate(func(selectedOrg string) error { + for _, org := range *organizations { + if org.Name == selectedOrg { + orgProjectCount := len(org.Projects) + if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { + return fmt.Errorf("Organization %s has reached its project quota (%d/%d)", org.Name, orgProjectCount, org.QuotaProject) + } + } + } + return nil + }), ), - ).WithTheme(huh.ThemeDracula()) + ).WithTheme(huh.ThemeCatppuccin()) err = form2.Run() if err != nil { - fmt.Println("Error:", err) + return nil, err } } @@ -105,40 +112,50 @@ func RunCreateWizard(lc *client.Client) (*CreateConfig, error) { Title("Do you want to define any other fields?"). Value(&additionalFields), ), - ).WithTheme(huh.ThemeDracula()) + ).WithTheme(huh.ThemeCatppuccin()) err = form3.Run() if err != nil { - fmt.Println("Error:", err) + return nil, err } if additionalFields { var fields []string + additionalFieldsOptions := []huh.Option[string]{ + huh.NewOption("branches: Which branches should be deployed", "Branches"), + huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), + huh.NewOption("standby-production-environment: Which environment(the name) should be marked as the standby production environment", "StandbyProductionEnvironment"), + huh.NewOption("subfolder: Set if the .lagoon.yml should be found in a subfolder useful if you have multiple Lagoon projects per Git Repository", "Subfolder"), + } + if organizationConfirm { + additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner")) + } form4 := huh.NewForm( huh.NewGroup( huh.NewMultiSelect[string](). Title("Select which fields you want to define"). - Options( - huh.NewOption("standby-production-environment", "StandbyProductionEnvironment"), - huh.NewOption("branches", "Branches"), - huh.NewOption("pullrequests", "PullRequests"), - huh.NewOption("deploytarget-project-pattern", "OpenshiftProjectPattern"), - huh.NewOption("development-environments-limit", "DevelopmentEnvironmentsLimit"), - huh.NewOption("auto-idle", "AutoIdle"), - huh.NewOption("subfolder", "Subfolder"), - huh.NewOption("private-key", "PrivateKey"), - huh.NewOption("build-image", "BuildImage"), - huh.NewOption("router-pattern", "RouterPattern"), - huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner"), - huh.NewOption("storage-calc", "StorageCalc "), - ). + //Options( + // //huh.NewOption("auto-idle: Auto idle setting of the project.", "AutoIdle"), + // huh.NewOption("branches: Which branches should be deployed", "Branches"), + // //huh.NewOption("build-image: Build Image for the project", "BuildImage"), + // //huh.NewOption("deploytarget-project-pattern: Pattern of Deploytarget(Kubernetes) Project/Namespace that should be generated", "OpenshiftProjectPattern"), + // //huh.NewOption("development-environments-limit: How many environments can be deployed at one time", "DevelopmentEnvironmentsLimit"), + // huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner"), + // //huh.NewOption("private-key: Private key to use for the project", "PrivateKey"), + // huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), + // //huh.NewOption("router-pattern: Router pattern of the project, e.g. '${service}-${environment}-${project}.lagoon.example.com'", "RouterPattern"), + // huh.NewOption("standby-production-environment: Which environment(the name) should be marked as the standby production environment", "StandbyProductionEnvironment"), + // //huh.NewOption("storage-calc: Should storage for this environment be calculated.", "StorageCalc"), + // huh.NewOption("subfolder: Set if the .lagoon.yml should be found in a subfolder useful if you have multiple Lagoon projects per Git Repository", "Subfolder"), + //). + Options(additionalFieldsOptions...). Value(&fields), ), - ).WithTheme(huh.ThemeDracula()) + ).WithTheme(huh.ThemeCatppuccin()) err = form4.Run() if err != nil { - fmt.Println("Error:", err) + return nil, err } if len(fields) == 0 { @@ -182,36 +199,39 @@ func RunCreateWizard(lc *client.Client) (*CreateConfig, error) { case "StorageCalc": config.StorageCalcProvided = true inputs = append(inputs, huh.NewConfirm(). - Title(field). + Title(fmt.Sprintf("Enable '%s'?", field)). Value(&config.StorageCalc)) case "AutoIdle": config.AutoIdleProvided = true inputs = append(inputs, huh.NewConfirm(). - Title(field). + Title(fmt.Sprintf("Enable '%s'?", field)). Value(&config.AutoIdle)) case "AddOrgOwner": - inputs = append(inputs, huh.NewConfirm().Title(field).Value(config.Input.AddOrgOwner)) + config.Input.AddOrgOwner = new(bool) + inputs = append(inputs, huh.NewConfirm(). + Title(fmt.Sprintf("Enable '%s'?", field)). + Value(config.Input.AddOrgOwner)) } } form5 := huh.NewForm( huh.NewGroup(inputs...), - ).WithTheme(huh.ThemeDracula()) + ).WithTheme(huh.ThemeCatppuccin()) err = form5.Run() if err != nil { - fmt.Println("Error:", err) + return nil, err } if devEnvLimit != "" { developmentEnvLimit, err := strconv.Atoi(devEnvLimit) if err != nil { fmt.Println("Error:", err) + return nil, err } config.DevEnvLimit = uint(developmentEnvLimit) } } - return config, nil } From 27e284fbce656c176be68789c79615f8119500ab Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Mon, 21 Jul 2025 17:22:37 +1000 Subject: [PATCH 03/14] Cleans up multiSelect & adds project check --- internal/wizard/project/create.go | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index 8e3271e5..c8cebebe 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -25,13 +25,22 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { if !valid { return errors.New(errMsg) } + if valid { + proj, err := lagoon.GetMinimalProjectByName(context.TODO(), name, lc) + if err != nil { + return err + } + if proj.Name != "" { + return errors.New(fmt.Sprintf("project: %s already exists", name)) + } + } return nil }), huh.NewConfirm(). Title("Do you want to create this project in an Organization?"). Value(&organizationConfirm), ), - ).WithTheme(huh.ThemeCatppuccin()) + ).WithTheme(huh.ThemeCharm()) formErr := initForm.Run() if formErr != nil { @@ -73,7 +82,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return nil }), ), - ).WithTheme(huh.ThemeCatppuccin()) + ).WithTheme(huh.ThemeCharm()) err = form2.Run() if err != nil { @@ -83,6 +92,10 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { deploytargets, err := lagoon.ListDeployTargets(context.TODO(), lc) var additionalFields bool + options := make([]huh.Option[uint], len(*deploytargets)) + for i, target := range *deploytargets { + options[i] = huh.NewOption(target.Name, target.ID) + } form3 := huh.NewForm( huh.NewGroup( huh.NewInput(). @@ -99,20 +112,13 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { Value(&config.Input.ProductionEnvironment), huh.NewSelect[uint](). Title("Select a deploy target"). - OptionsFunc(func() []huh.Option[uint] { - options := make([]huh.Option[uint], len(*deploytargets)) - for i, target := range *deploytargets { - options[i] = huh.NewOption(target.Name, target.ID) - } - - return options - }, &config.Input.Openshift). + Options(options...). Value(&config.Input.Openshift), huh.NewConfirm(). Title("Do you want to define any other fields?"). Value(&additionalFields), ), - ).WithTheme(huh.ThemeCatppuccin()) + ).WithTheme(huh.ThemeCharm()) err = form3.Run() if err != nil { @@ -124,8 +130,6 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { additionalFieldsOptions := []huh.Option[string]{ huh.NewOption("branches: Which branches should be deployed", "Branches"), huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), - huh.NewOption("standby-production-environment: Which environment(the name) should be marked as the standby production environment", "StandbyProductionEnvironment"), - huh.NewOption("subfolder: Set if the .lagoon.yml should be found in a subfolder useful if you have multiple Lagoon projects per Git Repository", "Subfolder"), } if organizationConfirm { additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner")) @@ -151,7 +155,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { Options(additionalFieldsOptions...). Value(&fields), ), - ).WithTheme(huh.ThemeCatppuccin()) + ).WithTheme(huh.ThemeCharm()) err = form4.Run() if err != nil { @@ -159,7 +163,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { } if len(fields) == 0 { - fmt.Println("No fields selected.") + return config, nil } var inputs []huh.Field @@ -216,7 +220,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { form5 := huh.NewForm( huh.NewGroup(inputs...), - ).WithTheme(huh.ThemeCatppuccin()) + ).WithTheme(huh.ThemeCharm()) err = form5.Run() if err != nil { From c15930f9e68d20e592b01082df912c2f26b8671d Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Mon, 21 Jul 2025 21:23:26 +1000 Subject: [PATCH 04/14] Adds raw and validation ux --- go.mod | 6 +-- internal/wizard/project/create.go | 72 ++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 3aed9d2a..5993fac1 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,6 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) -replace ( - github.com/uselagoon/machinery => ../machinery -) \ No newline at end of file +//replace ( +// github.com/uselagoon/machinery => ../machinery +//) \ No newline at end of file diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index c8cebebe..77c5d174 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -2,6 +2,7 @@ package project import ( "context" + "encoding/json" "errors" "fmt" "github.com/charmbracelet/huh" @@ -9,6 +10,7 @@ import ( "github.com/uselagoon/lagoon-cli/internal/util" "github.com/uselagoon/machinery/api/lagoon" "github.com/uselagoon/machinery/api/lagoon/client" + "github.com/uselagoon/machinery/api/schema" "strconv" ) @@ -47,17 +49,48 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return nil, formErr } if organizationConfirm { - organizations, err := lagoon.AllOrganizationsExtended(context.TODO(), lc) // requires machinery changes current - todo: change to raw + raw := `query allOrgsWithProjects { + allOrganizations { + id + name + description + friendlyName + quotaProject + quotaGroup + quotaNotification + quotaEnvironment + quotaRoute + projects { + id + name + } + } + }` + resp, err := lc.ProcessRaw(context.TODO(), raw, map[string]interface{}{}) + if err != nil { + return nil, err + } + + allOrgs := resp.(map[string]interface{})["allOrganizations"] + o, err := json.Marshal(allOrgs) + if err != nil { + return nil, err + } + + var organizations []schema.Organization + err = json.Unmarshal(o, &organizations) if err != nil { return nil, err } + + orgValidationErr := false form2 := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Select an organization"). OptionsFunc(func() []huh.Option[string] { - options := make([]huh.Option[string], len(*organizations)) - for i, org := range *organizations { + options := make([]huh.Option[string], len(organizations)) + for i, org := range organizations { orgProjectCount := len(org.Projects) if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { quotaFullLabel := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(fmt.Sprintf("%s | Project Limit: %v/%v | ⚠️ Project Quota full, cannot assigned project to this organization.", org.Name, orgProjectCount, org.QuotaProject)) @@ -71,11 +104,13 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { }, &config.OrganizationDetails.Name). Value(&config.OrganizationDetails.Name). Validate(func(selectedOrg string) error { - for _, org := range *organizations { + for _, org := range organizations { if org.Name == selectedOrg { orgProjectCount := len(org.Projects) if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { - return fmt.Errorf("Organization %s has reached its project quota (%d/%d)", org.Name, orgProjectCount, org.QuotaProject) + orgValidationErr = true + return nil + //return fmt.Errorf("Organization %s has reached its project quota (%d/%d)", org.Name, orgProjectCount, org.QuotaProject) } } } @@ -88,6 +123,33 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { if err != nil { return nil, err } + + if orgValidationErr { + var orgRetryResp string + orgRetryRespForm := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Organization quota exceeded. What would you like to do?"). + Options( + huh.NewOption("Go back to previous form", "back"), + huh.NewOption("Cancel wizard", "cancel"), + ). + Value(&orgRetryResp), + ), + ).WithTheme(huh.ThemeCharm()) + + err := orgRetryRespForm.Run() + if err != nil { + return nil, err + } + + switch orgRetryResp { + case "back": + return RunCreateWizard(lc) + case "cancel": + return nil, huh.ErrUserAborted + } + } } deploytargets, err := lagoon.ListDeployTargets(context.TODO(), lc) From b3d8ee926f427467bc227974409e9ce2c911140e Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Wed, 6 Aug 2025 11:45:48 +1000 Subject: [PATCH 05/14] Resolves lint errors & updates deployTargets to only list valid options if org is selected --- internal/util/shared.go | 46 +++++++++++++++++++++++++++++++ internal/wizard/project/create.go | 46 +++++++++++++++---------------- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/internal/util/shared.go b/internal/util/shared.go index b5e426b8..a69d2687 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -1,7 +1,11 @@ package util import ( + "context" + "encoding/json" + "errors" "fmt" + "github.com/uselagoon/machinery/api/lagoon/client" "github.com/uselagoon/machinery/api/schema" "reflect" "regexp" @@ -76,6 +80,48 @@ func IsValidProjectName(name string) (bool, string) { return true, "" } +func GetOrgDeployTargets(lc *client.Client, orgName string) ([]schema.DeployTarget, error) { + var orgDeployTargets []schema.DeployTarget + rawOrgByName := `query organizationByNameWithDeployTargets($name: String!) { + organizationByName(name: $name) { + id + name + deployTargets{ + id + name + } + } + }` + + orgResp, err := lc.ProcessRaw(context.TODO(), rawOrgByName, map[string]interface{}{ + "name": orgName, + }) + if err != nil { + return nil, err + } + + orgData, ok := orgResp.(map[string]interface{})["organizationByName"] + if !ok { + return nil, errors.New("organization not found") + } + + orgDT, exists := orgData.(map[string]interface{})["deployTargets"] + if !exists { + return nil, errors.New("no deployTargets found in organization") + } + + odt, err := json.Marshal(orgDT) + if err != nil { + return nil, err + } + + err = json.Unmarshal(odt, &orgDeployTargets) + if err != nil { + return nil, err + } + return orgDeployTargets, nil +} + func GenerateCLICommand(config *CreateConfig) string { cmd := "" configFields := config.Input diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index 77c5d174..cd050789 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -33,7 +33,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return err } if proj.Name != "" { - return errors.New(fmt.Sprintf("project: %s already exists", name)) + return fmt.Errorf("project: %s already exists", name) } } return nil @@ -48,6 +48,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { if formErr != nil { return nil, formErr } + var organizations []schema.Organization if organizationConfirm { raw := `query allOrgsWithProjects { allOrganizations { @@ -77,7 +78,6 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return nil, err } - var organizations []schema.Organization err = json.Unmarshal(o, &organizations) if err != nil { return nil, err @@ -99,7 +99,6 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { options[i] = huh.NewOption(fmt.Sprintf("%s | Project Limit: %v/%v", org.Name, orgProjectCount, util.QuotaCheck(org.QuotaProject)), org.Name) } } - return options }, &config.OrganizationDetails.Name). Value(&config.OrganizationDetails.Name). @@ -110,7 +109,6 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { orgValidationErr = true return nil - //return fmt.Errorf("Organization %s has reached its project quota (%d/%d)", org.Name, orgProjectCount, org.QuotaProject) } } } @@ -152,10 +150,26 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { } } - deploytargets, err := lagoon.ListDeployTargets(context.TODO(), lc) + var deployTargets *[]schema.DeployTarget + var err error + if organizationConfirm { + orgDeploytargets, err := util.GetOrgDeployTargets(lc, config.OrganizationDetails.Name) + if err != nil { + return nil, err + } + deployTargets = &orgDeploytargets + } else { + deployTargets, err = lagoon.ListDeployTargets(context.TODO(), lc) + if err != nil { + return nil, err + } + if deployTargets == nil { + return nil, errors.New("no deployTargets found for user") + } + } var additionalFields bool - options := make([]huh.Option[uint], len(*deploytargets)) - for i, target := range *deploytargets { + options := make([]huh.Option[uint], len(*deployTargets)) + for i, target := range *deployTargets { options[i] = huh.NewOption(target.Name, target.ID) } form3 := huh.NewForm( @@ -165,7 +179,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { Value(&config.Input.GitURL). Validate(func(str string) error { if !util.IsValidGitURL(config.Input.GitURL) { - return errors.New("Invalid Git URL") + return errors.New("invalid Git URL") } return nil }), @@ -194,26 +208,12 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), } if organizationConfirm { - additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner")) + additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner", "AddOrgOwner")) } form4 := huh.NewForm( huh.NewGroup( huh.NewMultiSelect[string](). Title("Select which fields you want to define"). - //Options( - // //huh.NewOption("auto-idle: Auto idle setting of the project.", "AutoIdle"), - // huh.NewOption("branches: Which branches should be deployed", "Branches"), - // //huh.NewOption("build-image: Build Image for the project", "BuildImage"), - // //huh.NewOption("deploytarget-project-pattern: Pattern of Deploytarget(Kubernetes) Project/Namespace that should be generated", "OpenshiftProjectPattern"), - // //huh.NewOption("development-environments-limit: How many environments can be deployed at one time", "DevelopmentEnvironmentsLimit"), - // huh.NewOption("owner (Only select if adding to an Organization)", "AddOrgOwner"), - // //huh.NewOption("private-key: Private key to use for the project", "PrivateKey"), - // huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), - // //huh.NewOption("router-pattern: Router pattern of the project, e.g. '${service}-${environment}-${project}.lagoon.example.com'", "RouterPattern"), - // huh.NewOption("standby-production-environment: Which environment(the name) should be marked as the standby production environment", "StandbyProductionEnvironment"), - // //huh.NewOption("storage-calc: Should storage for this environment be calculated.", "StorageCalc"), - // huh.NewOption("subfolder: Set if the .lagoon.yml should be found in a subfolder useful if you have multiple Lagoon projects per Git Repository", "Subfolder"), - //). Options(additionalFieldsOptions...). Value(&fields), ), From 1caef3a765060bf3390e4937bbdcab2bda24c441 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Thu, 7 Aug 2025 17:31:20 +1000 Subject: [PATCH 06/14] Cleanup & minor refactor --- cmd/project.go | 12 ---- docs/commands/lagoon_add_project.md | 1 + go.mod | 6 +- internal/util/shared.go | 88 +++++++++++++---------------- internal/wizard/project/create.go | 36 ++++++------ 5 files changed, 60 insertions(+), 83 deletions(-) diff --git a/cmd/project.go b/cmd/project.go index afcdbf5a..f01872f6 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -181,18 +181,6 @@ var addProjectCmd = &cobra.Command{ if config.OrganizationDetails.Name != "" { organizationName = config.OrganizationDetails.Name } - if config.AutoIdleProvided { - autoIdle = config.AutoIdle - autoIdleProvided = config.AutoIdleProvided - } - if config.StorageCalcProvided { - storageCalc = config.StorageCalc - storageCalcProvided = config.StorageCalcProvided - } - if config.DevEnvLimit != 0 { - developmentEnvironmentsLimit = config.DevEnvLimit - developmentEnvironmentsLimitProvided = true - } generatedCommand = util.GenerateCLICommand(config) } diff --git a/docs/commands/lagoon_add_project.md b/docs/commands/lagoon_add_project.md index 768678d8..77171bec 100644 --- a/docs/commands/lagoon_add_project.md +++ b/docs/commands/lagoon_add_project.md @@ -22,6 +22,7 @@ lagoon add project [flags] -L, --development-environments-limit uint How many environments can be deployed at one time -g, --git-url string GitURL of the project -h, --help help for project + --interactive Set Interactive mode for the project creation wizard. -j, --json string JSON string to patch --organization-id uint ID of the Organization to add the project to -O, --organization-name string Name of the Organization to add the project to diff --git a/go.mod b/go.mod index 5993fac1..f194cd10 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/Masterminds/semver/v3 v3.3.1 github.com/charmbracelet/huh v0.7.0 + github.com/charmbracelet/lipgloss v1.1.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-github/v66 v66.0.0 github.com/guregu/null v4.0.0+incompatible @@ -30,7 +31,6 @@ require ( github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/bubbletea v1.3.4 // 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.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect @@ -65,7 +65,3 @@ require ( golang.org/x/text v0.24.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) - -//replace ( -// github.com/uselagoon/machinery => ../machinery -//) \ No newline at end of file diff --git a/internal/util/shared.go b/internal/util/shared.go index a69d2687..b1e007e1 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -9,18 +9,12 @@ import ( "github.com/uselagoon/machinery/api/schema" "reflect" "regexp" - "slices" "strconv" ) type CreateConfig struct { Input schema.AddProjectInput OrganizationDetails schema.Organization - AutoIdle bool - AutoIdleProvided bool - StorageCalc bool - StorageCalcProvided bool - DevEnvLimit uint } var fieldCmdMap = map[string]string{ @@ -49,6 +43,11 @@ var fieldCmdMap = map[string]string{ "DeploymentsDisabled": "--deployments-disabled", } +type reflectFields struct { + fieldType reflect.Type + fieldValue reflect.Value +} + func IsValidGitURL(url string) bool { const pattern = `^((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)?$` @@ -122,56 +121,49 @@ func GetOrgDeployTargets(lc *client.Client, orgName string) ([]schema.DeployTarg return orgDeployTargets, nil } -func GenerateCLICommand(config *CreateConfig) string { +func processFields(fieldName string, fieldValue reflect.Value) string { cmd := "" - configFields := config.Input - configType := reflect.TypeOf(configFields) - configValue := reflect.ValueOf(configFields) - boolFields := []string{ - "--auto-idle", - "--storage-calc", - } - - for i := 0; i < configType.NumField(); i++ { - fieldName := configType.Field(i).Name - fieldValue := configValue.Field(i) - if !fieldValue.IsZero() { - if flag, exists := fieldCmdMap[fieldName]; exists { - if flag == "--owner" { - cmd += " " + fmt.Sprintf("%s=%v", flag, *(fieldValue.Interface().(*bool))) - } else { - cmd += " " + fmt.Sprintf("%s %v", flag, fieldValue.Interface()) - } - } + if fieldName == "OrganizationDetails" { + addOrgInputField := fieldValue.FieldByName("AddOrganizationInput") + nameField := addOrgInputField.FieldByName("Name") + if nameField.IsValid() { + cmd += " " + fmt.Sprintf("%s=%v", "--organization-name", nameField.Interface()) } } - configBoolType := reflect.TypeOf(*config) - configBoolValue := reflect.ValueOf(*config) - - for i := 0; i < configBoolType.NumField(); i++ { - fieldName := configBoolType.Field(i).Name - fieldValue := configBoolValue.Field(i) - - var fieldProvided bool - - switch fieldName { - case "AutoIdle": - fieldProvided = config.AutoIdleProvided - case "StorageCalc": - fieldProvided = config.StorageCalcProvided + if flag, exists := fieldCmdMap[fieldName]; exists { + if flag == "--owner" { + cmd += " " + fmt.Sprintf("%s=%v", flag, *(fieldValue.Interface().(*bool))) + } else { + cmd += " " + fmt.Sprintf("%s %v", flag, fieldValue.Interface()) } + } + return cmd +} - if fieldProvided { - if flag, exists := fieldCmdMap[fieldName]; exists { - if slices.Contains(boolFields, flag) { - boolVal := fieldValue.Interface().(bool) - cmd += " " + fmt.Sprintf("%s=%v", flag, boolVal) - } +func GenerateCLICommand(config *CreateConfig) string { + commands := "" + + configFields := []reflectFields{ + { + fieldType: reflect.TypeOf(config.Input), + fieldValue: reflect.ValueOf(config.Input), + }, + { + fieldType: reflect.TypeOf(*config), + fieldValue: reflect.ValueOf(*config), + }, + } + + for _, field := range configFields { + for i := 0; i < field.fieldType.NumField(); i++ { + fieldName := field.fieldType.Field(i).Name + fieldValue := field.fieldValue.Field(i) + if !fieldValue.IsZero() { + commands += processFields(fieldName, fieldValue) } } } - - return cmd + return commands } diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index cd050789..3344e715 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -262,16 +262,16 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { }), ) - case "StorageCalc": - config.StorageCalcProvided = true - inputs = append(inputs, huh.NewConfirm(). - Title(fmt.Sprintf("Enable '%s'?", field)). - Value(&config.StorageCalc)) - case "AutoIdle": - config.AutoIdleProvided = true - inputs = append(inputs, huh.NewConfirm(). - Title(fmt.Sprintf("Enable '%s'?", field)). - Value(&config.AutoIdle)) + //case "StorageCalc": + // config.StorageCalcProvided = true + // inputs = append(inputs, huh.NewConfirm(). + // Title(fmt.Sprintf("Enable '%s'?", field)). + // Value(&config.StorageCalc)) + //case "AutoIdle": + // config.AutoIdleProvided = true + // inputs = append(inputs, huh.NewConfirm(). + // Title(fmt.Sprintf("Enable '%s'?", field)). + // Value(&config.AutoIdle)) case "AddOrgOwner": config.Input.AddOrgOwner = new(bool) inputs = append(inputs, huh.NewConfirm(). @@ -289,14 +289,14 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return nil, err } - if devEnvLimit != "" { - developmentEnvLimit, err := strconv.Atoi(devEnvLimit) - if err != nil { - fmt.Println("Error:", err) - return nil, err - } - config.DevEnvLimit = uint(developmentEnvLimit) - } + //if devEnvLimit != "" { + // developmentEnvLimit, err := strconv.Atoi(devEnvLimit) + // if err != nil { + // fmt.Println("Error:", err) + // return nil, err + // } + // config.DevEnvLimit = uint(developmentEnvLimit) + //} } return config, nil From 3394dac2711c2f5f1b1eeab5897567ac78bacc6d Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Thu, 7 Aug 2025 17:35:01 +1000 Subject: [PATCH 07/14] Removes errant comments --- internal/wizard/project/create.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index 3344e715..00f24330 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -262,16 +262,6 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { }), ) - //case "StorageCalc": - // config.StorageCalcProvided = true - // inputs = append(inputs, huh.NewConfirm(). - // Title(fmt.Sprintf("Enable '%s'?", field)). - // Value(&config.StorageCalc)) - //case "AutoIdle": - // config.AutoIdleProvided = true - // inputs = append(inputs, huh.NewConfirm(). - // Title(fmt.Sprintf("Enable '%s'?", field)). - // Value(&config.AutoIdle)) case "AddOrgOwner": config.Input.AddOrgOwner = new(bool) inputs = append(inputs, huh.NewConfirm(). @@ -289,15 +279,6 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return nil, err } - //if devEnvLimit != "" { - // developmentEnvLimit, err := strconv.Atoi(devEnvLimit) - // if err != nil { - // fmt.Println("Error:", err) - // return nil, err - // } - // config.DevEnvLimit = uint(developmentEnvLimit) - //} - } return config, nil } From d35c112f78dde36b7d23a7ce57797b82fa488aaa Mon Sep 17 00:00:00 2001 From: Blaize Kaye Date: Thu, 14 Aug 2025 11:27:27 +1200 Subject: [PATCH 08/14] Sort orgs for display --- internal/wizard/project/create.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index 00f24330..26ca787d 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -5,13 +5,15 @@ import ( "encoding/json" "errors" "fmt" + "sort" + "strconv" + "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/uselagoon/lagoon-cli/internal/util" "github.com/uselagoon/machinery/api/lagoon" "github.com/uselagoon/machinery/api/lagoon/client" "github.com/uselagoon/machinery/api/schema" - "strconv" ) func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { @@ -89,6 +91,10 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { huh.NewSelect[string](). Title("Select an organization"). OptionsFunc(func() []huh.Option[string] { + // let's sort orgs + sort.Slice(organizations, func(i, j int) bool { + return organizations[i].Name < organizations[j].Name + }) options := make([]huh.Option[string], len(organizations)) for i, org := range organizations { orgProjectCount := len(org.Projects) From 96557ca736611c7cd5ecc7aa1c74f161d3837e48 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Thu, 14 Aug 2025 11:35:33 +1000 Subject: [PATCH 09/14] Adds a basic Lagoon theme --- internal/wizard/project/create.go | 13 ++++--- internal/wizard/themes/ThemeLagoon.go | 53 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 internal/wizard/themes/ThemeLagoon.go diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index 00f24330..f9eefb55 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/uselagoon/lagoon-cli/internal/util" + "github.com/uselagoon/lagoon-cli/internal/wizard/themes" "github.com/uselagoon/machinery/api/lagoon" "github.com/uselagoon/machinery/api/lagoon/client" "github.com/uselagoon/machinery/api/schema" @@ -42,7 +43,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { Title("Do you want to create this project in an Organization?"). Value(&organizationConfirm), ), - ).WithTheme(huh.ThemeCharm()) + ).WithTheme(themes.ThemeLagoon()) formErr := initForm.Run() if formErr != nil { @@ -115,7 +116,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { return nil }), ), - ).WithTheme(huh.ThemeCharm()) + ).WithTheme(themes.ThemeLagoon()) err = form2.Run() if err != nil { @@ -134,7 +135,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { ). Value(&orgRetryResp), ), - ).WithTheme(huh.ThemeCharm()) + ).WithTheme(themes.ThemeLagoon()) err := orgRetryRespForm.Run() if err != nil { @@ -194,7 +195,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { Title("Do you want to define any other fields?"). Value(&additionalFields), ), - ).WithTheme(huh.ThemeCharm()) + ).WithTheme(themes.ThemeLagoon()) err = form3.Run() if err != nil { @@ -217,7 +218,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { Options(additionalFieldsOptions...). Value(&fields), ), - ).WithTheme(huh.ThemeCharm()) + ).WithTheme(themes.ThemeLagoon()) err = form4.Run() if err != nil { @@ -272,7 +273,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { form5 := huh.NewForm( huh.NewGroup(inputs...), - ).WithTheme(huh.ThemeCharm()) + ).WithTheme(themes.ThemeLagoon()) err = form5.Run() if err != nil { diff --git a/internal/wizard/themes/ThemeLagoon.go b/internal/wizard/themes/ThemeLagoon.go new file mode 100644 index 00000000..4cebfbcf --- /dev/null +++ b/internal/wizard/themes/ThemeLagoon.go @@ -0,0 +1,53 @@ +package themes + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +func ThemeLagoon() *huh.Theme { + t := huh.ThemeBase() + + var ( + normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} + lagoonBlue = lipgloss.AdaptiveColor{Light: "#4578e5", Dark: "#4578e5"} + cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"} + lagoonLightBlue = lipgloss.Color("#2094f3") + errorText = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"} + accents = lipgloss.AdaptiveColor{Light: "#364c86", Dark: "#fafafc"} + ) + + t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) + t.Focused.Card = t.Focused.Base + t.Focused.Title = t.Focused.Title.Foreground(lagoonBlue).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(lagoonBlue).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(lagoonBlue) + t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorText) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorText) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accents) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(lagoonLightBlue) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(lagoonLightBlue) + t.Focused.Option = t.Focused.Option.Foreground(normalFg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(accents) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#02CF92", Dark: "#02A877"}).SetString("✓ ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ") + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(lagoonLightBlue) + t.Focused.Next = t.Focused.FocusedButton + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"}) + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(accents) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accents) + + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.Card = t.Blurred.Base + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + t.Group.Title = t.Focused.Title + t.Group.Description = t.Focused.Description + return t +} From f58eaf8e2bf5e02883b34a8365a9ffc2ecb15abd Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Thu, 14 Aug 2025 16:54:28 +1000 Subject: [PATCH 10/14] Puts wizard behind experimental flag --- cmd/project.go | 5 ++++- cmd/root.go | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/project.go b/cmd/project.go index f01872f6..8fd5be92 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -151,6 +151,9 @@ var addProjectCmd = &cobra.Command{ if err != nil { return err } + if interactive && !experimentalEnabled { + return fmt.Errorf("--interactive requires experimental flag to be set in .lagoon.yml") + } generatedCommand := "" if !interactive { @@ -797,7 +800,7 @@ func init() { addProjectCmd.Flags().Bool("owner", false, "Add the user as an owner of the project") addProjectCmd.Flags().StringP("organization-name", "O", "", "Name of the Organization to add the project to") addProjectCmd.Flags().UintP("organization-id", "", 0, "ID of the Organization to add the project to") - addProjectCmd.Flags().Bool("interactive", false, "Set Interactive mode for the project creation wizard.") + addProjectCmd.Flags().Bool("interactive", false, "Set Interactive mode for the project creation wizard. Requires 'experimental' flag to be set in .lagoon.yml") listCmd.AddCommand(listProjectByMetadata) listProjectByMetadata.Flags().StringP("key", "K", "", "The key name of the metadata value you are querying on") diff --git a/cmd/root.go b/cmd/root.go index 76f30b3a..5d770e0a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,6 +39,7 @@ var userPath string var configFilePath string var updateDocURL = "https://uselagoon.github.io/lagoon-cli" var verboseOutput bool +var experimentalEnabled bool var skipUpdateCheck bool @@ -262,6 +263,7 @@ func initConfig() { if cmdProject.Environment != "" && cmdProjectEnvironment == "" { cmdProjectEnvironment = cmdProject.Environment } + experimentalEnabled = lagoonCLIConfig.IsFlagSet("experimental") } func yesNo(message string) bool { From 694820261e77b2e79db46799e4cb93a7001e6641 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Thu, 13 Nov 2025 17:10:41 +1100 Subject: [PATCH 11/14] Updates naming, theme, validaiton etc --- go.mod | 32 ++++++++------- go.sum | 57 ++++++++++++++------------- internal/util/shared.go | 34 ++++++++++++++-- internal/wizard/project/create.go | 4 +- internal/wizard/themes/ThemeLagoon.go | 7 ++++ 5 files changed, 86 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index ba36731c..92632297 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/uselagoon/lagoon-cli -go 1.24.0 +go 1.24.2 -toolchain go1.24.1 +toolchain go1.24.4 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -12,17 +12,17 @@ require ( github.com/google/go-github/v66 v66.0.0 github.com/guregu/null v4.0.0+incompatible github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc - github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/jedib0t/go-pretty/v6 v6.7.1 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/manifoldco/promptui v0.9.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/skeema/knownhosts v1.3.1 + github.com/skeema/knownhosts v1.3.2 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/uselagoon/machinery v0.0.34 - golang.org/x/crypto v0.42.0 - golang.org/x/term v0.35.0 + golang.org/x/crypto v0.43.0 + golang.org/x/term v0.36.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -31,13 +31,15 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/bubbletea v1.3.4 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/bubbletea v1.3.5 // indirect + github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/x/ansi v0.8.0 // 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/charmbracelet/x/exp/strings v0.0.0-20251109135125-8916d276318f // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -47,11 +49,11 @@ require ( github.com/hashicorp/go-version v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 // 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/mattn/go-runewidth v0.0.19 // 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 @@ -61,9 +63,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.13.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 53b0ac36..b9d734b0 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,10 @@ 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.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= -github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= -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/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -30,10 +30,10 @@ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9 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/exp/strings v0.0.0-20251109135125-8916d276318f h1:OevNiFl/Y/IldUl5jtcPwEyiI3uzUsem8u7P4mzSmGI= +github.com/charmbracelet/x/exp/strings v0.0.0-20251109135125-8916d276318f/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= 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= @@ -47,6 +47,10 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -78,8 +82,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc h1:4IZpk3M4m6ypx0IlRoEyEyY1gAdicWLMQ0NcG/gBnnA= github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc/go.mod h1:UlaC6ndby46IJz9m/03cZPKKkR9ykeIVBBDE3UDBdJk= -github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= -github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.7.1 h1:bHDSsj93NuJ563hHuM7ohk/wpX7BmRFNIsVv1ssI2/M= +github.com/jedib0t/go-pretty/v6 v6.7.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -90,8 +94,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -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/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 h1:guHWmqIKr4G+gQ4uYU5vcZjsUhhklRA2uOcGVfcfqis= github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= @@ -102,8 +106,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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= @@ -118,7 +122,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -126,8 +129,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -139,23 +142,23 @@ github.com/uselagoon/machinery v0.0.34 h1:5DsvXEyMeXmzQhjt11YH7+kZJueabovrwKTv0x github.com/uselagoon/machinery v0.0.34/go.mod h1:G0ujppuNR0BrtAnlmH8xDb9TDfayb4A36aeo0DYg7fQ= 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= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/util/shared.go b/internal/util/shared.go index b1e007e1..a1443193 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -7,9 +7,11 @@ import ( "fmt" "github.com/uselagoon/machinery/api/lagoon/client" "github.com/uselagoon/machinery/api/schema" + "net/url" "reflect" "regexp" "strconv" + "strings" ) type CreateConfig struct { @@ -48,11 +50,34 @@ type reflectFields struct { fieldValue reflect.Value } -func IsValidGitURL(url string) bool { - const pattern = `^((git|ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)?$` +func IsValidGitURL(gitUrl string) bool { + const sshPattern = `^[\w.-]+@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$` + re := regexp.MustCompile(sshPattern) + if re.MatchString(sshPattern) { + return true + } + + parsedUrl, err := url.Parse(gitUrl) + if err != nil { + fmt.Println("Error parsing URL:", err) + return false + } + + if parsedUrl.Host == "" { + return false + } + + validProtocols := map[string]bool{"git": true, "ssh": true, "http": true, "https": true} + if !validProtocols[strings.ToLower(parsedUrl.Scheme)] { + return false + } + + pathParts := strings.Split(strings.Trim(parsedUrl.Path, "/"), "/") + if len(pathParts) < 2 { + return false + } - re := regexp.MustCompile(pattern) - return re.MatchString(url) + return true } func QuotaCheck(quota int) string { @@ -98,6 +123,7 @@ func GetOrgDeployTargets(lc *client.Client, orgName string) ([]schema.DeployTarg if err != nil { return nil, err } + fmt.Println() orgData, ok := orgResp.(map[string]interface{})["organizationByName"] if !ok { diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index cc2b4425..b82481a4 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -19,7 +19,7 @@ import ( func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { config := &util.CreateConfig{} - var organizationConfirm bool + organizationConfirm := true initForm := huh.NewForm( huh.NewGroup( huh.NewInput(). @@ -215,7 +215,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), } if organizationConfirm { - additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner", "AddOrgOwner")) + additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner: Add your account to the project group during creation", "AddOrgOwner")) } form4 := huh.NewForm( huh.NewGroup( diff --git a/internal/wizard/themes/ThemeLagoon.go b/internal/wizard/themes/ThemeLagoon.go index 4cebfbcf..1b18c45b 100644 --- a/internal/wizard/themes/ThemeLagoon.go +++ b/internal/wizard/themes/ThemeLagoon.go @@ -49,5 +49,12 @@ func ThemeLagoon() *huh.Theme { t.Group.Title = t.Focused.Title t.Group.Description = t.Focused.Description + t.Help.Ellipsis = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.ShortKey = lipgloss.NewStyle().Foreground(normalFg).Bold(true) + t.Help.ShortDesc = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.ShortSeparator = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.FullKey = lipgloss.NewStyle().Foreground(normalFg).Bold(true) + t.Help.FullDesc = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.FullSeparator = lipgloss.NewStyle().Foreground(lagoonBlue) return t } From 0c741fd92abd5a369b6b3f3c58b5a07fd56d766d Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Wed, 14 Jan 2026 11:04:37 +1100 Subject: [PATCH 12/14] Adds lagoon context to generated cmd + puts it behind a flag, makes giturl validation more robust --- cmd/project.go | 18 ++++++++++++++---- internal/util/shared.go | 13 +++++++++---- internal/wizard/project/create.go | 2 +- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/cmd/project.go b/cmd/project.go index 8fd5be92..de78eb0b 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -5,10 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "strconv" + "github.com/charmbracelet/huh" "github.com/uselagoon/lagoon-cli/internal/util" "github.com/uselagoon/lagoon-cli/internal/wizard/project" - "strconv" "strings" @@ -151,8 +152,12 @@ var addProjectCmd = &cobra.Command{ if err != nil { return err } + genCmdEnabled, err := cmd.Flags().GetBool("generate-command") + if err != nil { + return err + } if interactive && !experimentalEnabled { - return fmt.Errorf("--interactive requires experimental flag to be set in .lagoon.yml") + return fmt.Errorf("--interactive requires experimental flag to be set in .lagoon.yml\n") } generatedCommand := "" @@ -248,8 +253,12 @@ var addProjectCmd = &cobra.Command{ if organizationName != "" { resultData.ResultData["Organization"] = organizationName } - if interactive { - resultData.ResultData["Generated Command"] = "lagoon add project" + generatedCommand + if interactive && genCmdEnabled { + cmdPrefix := "lagoon add project" + if cmdLagoon != "" { + cmdPrefix = fmt.Sprintf("lagoon --lagoon %s add project", cmdLagoon) + } + resultData.ResultData["Generated Command"] = cmdPrefix + generatedCommand } r := output.RenderResult(resultData, outputOptions) fmt.Fprintf(cmd.OutOrStdout(), "%s", r) @@ -801,6 +810,7 @@ func init() { addProjectCmd.Flags().StringP("organization-name", "O", "", "Name of the Organization to add the project to") addProjectCmd.Flags().UintP("organization-id", "", 0, "ID of the Organization to add the project to") addProjectCmd.Flags().Bool("interactive", false, "Set Interactive mode for the project creation wizard. Requires 'experimental' flag to be set in .lagoon.yml") + addProjectCmd.Flags().Bool("generate-command", false, "Displays the generated command from the project creation wizard. Requires Interactive mode to be enabled") listCmd.AddCommand(listProjectByMetadata) listProjectByMetadata.Flags().StringP("key", "K", "", "The key name of the metadata value you are querying on") diff --git a/internal/util/shared.go b/internal/util/shared.go index a1443193..75d7fac7 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -5,13 +5,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/uselagoon/machinery/api/lagoon/client" - "github.com/uselagoon/machinery/api/schema" "net/url" "reflect" "regexp" "strconv" "strings" + + "github.com/uselagoon/machinery/api/lagoon/client" + "github.com/uselagoon/machinery/api/schema" ) type CreateConfig struct { @@ -51,9 +52,13 @@ type reflectFields struct { } func IsValidGitURL(gitUrl string) bool { + if strings.TrimSpace(gitUrl) == "" { + return false + } + const sshPattern = `^[\w.-]+@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$` re := regexp.MustCompile(sshPattern) - if re.MatchString(sshPattern) { + if re.MatchString(gitUrl) { return true } @@ -63,7 +68,7 @@ func IsValidGitURL(gitUrl string) bool { return false } - if parsedUrl.Host == "" { + if parsedUrl.Host == "" || parsedUrl.Scheme == "" { return false } diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go index b82481a4..c871969c 100644 --- a/internal/wizard/project/create.go +++ b/internal/wizard/project/create.go @@ -272,7 +272,7 @@ func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { case "AddOrgOwner": config.Input.AddOrgOwner = new(bool) inputs = append(inputs, huh.NewConfirm(). - Title(fmt.Sprintf("Enable '%s'?", field)). + Title("Add my user to this project"). Value(config.Input.AddOrgOwner)) } } From 6560102ecff54f8e2801338db6294c67fea46509 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Wed, 14 Jan 2026 13:57:12 +1100 Subject: [PATCH 13/14] Resolved linter issues --- cmd/project.go | 2 +- internal/util/shared.go | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cmd/project.go b/cmd/project.go index de78eb0b..5db7c106 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -157,7 +157,7 @@ var addProjectCmd = &cobra.Command{ return err } if interactive && !experimentalEnabled { - return fmt.Errorf("--interactive requires experimental flag to be set in .lagoon.yml\n") + return fmt.Errorf("--interactive requires experimental flag to be set in .lagoon.yml") } generatedCommand := "" diff --git a/internal/util/shared.go b/internal/util/shared.go index 75d7fac7..e15f6cac 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -78,11 +78,7 @@ func IsValidGitURL(gitUrl string) bool { } pathParts := strings.Split(strings.Trim(parsedUrl.Path, "/"), "/") - if len(pathParts) < 2 { - return false - } - - return true + return len(pathParts) >= 2 } func QuotaCheck(quota int) string { From d5dd401688682599a7a370af457ef70f6578d505 Mon Sep 17 00:00:00 2001 From: cgoodwin90 Date: Wed, 14 Jan 2026 15:20:22 +1100 Subject: [PATCH 14/14] Updates gitUrl validation --- internal/util/shared.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/util/shared.go b/internal/util/shared.go index e15f6cac..8cf4ce53 100644 --- a/internal/util/shared.go +++ b/internal/util/shared.go @@ -56,7 +56,7 @@ func IsValidGitURL(gitUrl string) bool { return false } - const sshPattern = `^[\w.-]+@[\w.-]+:[\w.-]+/[\w.-]+(?:\.git)?$` + const sshPattern = `^[\w.-]+@[\w.-]+:[\w.-]+(/[\w.-]+)+(?:\.git)?$` re := regexp.MustCompile(sshPattern) if re.MatchString(gitUrl) { return true @@ -72,7 +72,7 @@ func IsValidGitURL(gitUrl string) bool { return false } - validProtocols := map[string]bool{"git": true, "ssh": true, "http": true, "https": true} + validProtocols := map[string]bool{"git": true, "ssh": true, "http": true, "https": true, "ftp": true} if !validProtocols[strings.ToLower(parsedUrl.Scheme)] { return false }