diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 36b24f2..32787ea 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,4 +3,8 @@ updates:
- package-ecosystem: gomod
directory: /
schedule:
- interval: daily
+ interval: weekly
+ - package-ecosystem: github-actions
+ directory: .github/workflows
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/ci.lint_test.yml b/.github/workflows/ci.lint_test.yml
index ccb83e4..27f7166 100644
--- a/.github/workflows/ci.lint_test.yml
+++ b/.github/workflows/ci.lint_test.yml
@@ -10,22 +10,23 @@ on:
jobs:
Lint:
name: "CI: Linting"
- runs-on: ubuntu-latest
-
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
-
- - name: Lint Code Base
- uses: docker://ghcr.io/github/super-linter:v4
- env:
- VALIDATE_ALL_CODEBASE: true
- DEFAULT_BRANCH: master
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- VALIDATE_GO: true
- FILTER_REGEX_EXCLUDE: ".*test/.*"
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
+ - name: go mod tidy
+ run: go mod tidy
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v4
Test:
name: "CI: Go Tests"
@@ -35,18 +36,13 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: arnested/go-version-action@v1
- id: go-version
- name: Install Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v5
with:
- go-version: ${{ steps.go-version.outputs.latest }}
- check-latest: true
- - name: Checkout code
- uses: actions/checkout@v2
+ go-version: stable
- name: Test
run: |
go mod tidy
diff --git a/LICENSE b/LICENSE
index d645695..5ac678e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,202 +1,21 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+The MIT License (MIT)
+
+Copyright © 2024 Richard Weston
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index 78cd9e9..65c2472 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
aztx
-
+
@@ -9,11 +9,13 @@
-
+
-> This tool is a helper for azure-cli that leverages fzf for a nice interface to switch between subscription contexts.
+This tool is a helper for azure-cli that leverages fzf for a nice interface to switch between subscription contexts.
+
+Additionally it can also be used to quickly switch back to a previous subscription context using the `aztx -` command in a similar way to `cd -` in bash.
### 🏠 [Homepage](https://github.com/riweston/aztx#readme)
@@ -78,7 +80,7 @@ Give a ⭐️ if this project helped you!
## 📝 License
Copyright © 2021 [Richard Weston](https://github.com/riweston).
-This project is [Apache](https://github.com/riweston/aztx/blob/master/LICENSE) licensed.
+This project is [MIT](https://github.com/riweston/aztx/blob/master/LICENSE) licensed.
***
_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
diff --git a/cmd/aztx/contexts.go b/cmd/aztx/contexts.go
deleted file mode 100644
index fbeaf04..0000000
--- a/cmd/aztx/contexts.go
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
-Copyright © 2021 Richard Weston github@riweston.io
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package aztx
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "os"
-
- "github.com/google/uuid"
- "github.com/ktr0731/go-fuzzyfinder"
-)
-
-const (
- InfoColor = "\033[0;32m%s\033[0m"
- NoticeColor = "\033[0;36m%s\033[0m"
- WarningColor = "\033[1;33m%s\033[0m"
- ErrorColor = "\033[1;31m%s\033[0m"
- DebugColor = "\033[0;36m%s\033[0m"
-)
-
-type Subscription struct {
- EnvironmentName string `json:"environmentName"`
- HomeTenantID uuid.UUID `json:"homeTenantId"`
- ID uuid.UUID `json:"id"`
- IsDefault bool `json:"isDefault"`
- ManagedByTenants []string `json:"managedByTenants"`
- Name string `json:"name"`
- State string `json:"state"`
- TenantID uuid.UUID `json:"tenantId"`
- User struct {
- Name string `json:"name"`
- AccountType string `json:"type"`
- } `json:"user"`
-}
-
-type File struct {
- InstallationID uuid.UUID `json:"installationId"`
- Subscriptions []Subscription `json:"subscriptions"`
-}
-
-func SelectAzureAccountsDisplayName() {
- home, errHome := os.UserHomeDir()
- if errHome != nil {
- panic(errHome)
- }
- azureProfile := home + "/.azure/azureProfile.json"
- d := ReadAzureProfile(azureProfile)
- currentCtx := ReadAzureProfileDefault(d)
-
- idx, err := fuzzyfinder.Find(
- d.Subscriptions,
- func(i int) string {
- return d.Subscriptions[i].Name
- },
- fuzzyfinder.WithHeader(currentCtx))
- if err != nil {
- fmt.Printf(NoticeColor, "cancelled\n")
- msg := fmt.Sprintf("%s\n", currentCtx)
- fmt.Printf(InfoColor, msg)
- return
- }
-
- errWrite := WriteAzureProfile(d, d.Subscriptions[idx].ID, azureProfile)
- if errWrite != nil {
- panic(errWrite)
- }
- msg := fmt.Sprintf("Set Context: %s (%s)\n", d.Subscriptions[idx].Name, d.Subscriptions[idx].ID)
- fmt.Printf(InfoColor, msg)
-}
-
-func ReadAzureProfile(file string) File {
- jsonFile, err := os.Open(file)
- if err != nil {
- fmt.Println(err)
- }
- byteValue, errByte := ioutil.ReadAll(jsonFile)
- if errByte != nil {
- fmt.Println(errByte)
- }
- byteValue = bytes.TrimPrefix(byteValue, []byte("\xef\xbb\xbf"))
- var jsonData File
- errJSON := json.Unmarshal(byteValue, &jsonData)
- if errJSON != nil {
- fmt.Println(err)
- }
-
- return jsonData
-}
-
-func ReadAzureProfileDefault(file File) (subscription string) {
- var subscriptionName string
- var subscriptionID uuid.UUID
-
- for idx := range file.Subscriptions {
- if file.Subscriptions[idx].IsDefault {
- subscriptionName = file.Subscriptions[idx].Name
- subscriptionID = file.Subscriptions[idx].ID
- }
- }
- return fmt.Sprintf("Current Context: %s (%s)", subscriptionName, subscriptionID)
-}
-
-func WriteAzureProfile(file File, id uuid.UUID, outFile string) error {
- for idx := range file.Subscriptions {
- if file.Subscriptions[idx].ID == id {
- file.Subscriptions[idx].IsDefault = true
- } else {
- file.Subscriptions[idx].IsDefault = false
- }
- }
-
- byteValue, err := json.Marshal(&file)
- if err != nil {
- return err
- }
-
- err = ioutil.WriteFile(outFile, byteValue, 0600)
- return err
-}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..11511f1
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,120 @@
+/*
+Copyright © 2024 Richard Weston
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "github.com/ktr0731/go-fuzzyfinder"
+ "github.com/riweston/aztx/pkg/profile"
+ "github.com/riweston/aztx/pkg/state"
+ "os"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+ Use: "aztx",
+ Short: "A brief description of your application",
+ Long: `A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+ Args: cobra.MaximumNArgs(1),
+ // Uncomment the following line if your bare application
+ // has an action associated with it:
+ Run: func(cmd *cobra.Command, args []string) {
+ cfg := state.ViperAdapter{Viper: viper.GetViper()}
+ lc := state.NewStateReaderWriter(&cfg)
+ userProfileAdapter := profile.UserProfileFileAdapter{}
+ c := profile.NewConfigurationAdapter(&userProfileAdapter)
+
+ if len(args) > 0 {
+ if args[0] == "-" {
+ if err := c.SetPreviousContext(lc); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ } else {
+ os.Exit(0)
+ }
+ }
+
+ }
+ ac, err := c.SelectWithFinder()
+ if errors.Is(err, fuzzyfinder.ErrAbort) {
+ os.Exit(0)
+ }
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ if err := c.SetContext(lc, ac); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+ cobra.OnInitialize(initConfig)
+
+ // Here you will define your flags and configuration settings.
+ // Cobra supports persistent flags, which, if defined here,
+ // will be global for your application.
+
+ // Cobra also supports local flags, which will only run
+ // when this action is called directly.
+ rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+
+ // Find home directory.
+ home, err := os.UserHomeDir()
+ cobra.CheckErr(err)
+
+ // Search config in home directory with name ".aztx" (without extension).
+ viper.AddConfigPath(home)
+ viper.SetConfigType("yml")
+ viper.SetConfigName(".aztx")
+ if err := viper.ReadInConfig(); err != nil {
+ // If the config file doesn't exist, create it.
+ if err := viper.SafeWriteConfigAs(home + "/.aztx.yml"); err != nil {
+ fmt.Println("Can't write config:", err)
+ os.Exit(1)
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index 136f119..d95e0ba 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,43 @@
module github.com/riweston/aztx
-go 1.16
+go 1.22
require (
- github.com/google/uuid v1.3.0
- github.com/ktr0731/go-fuzzyfinder v0.6.0
+ github.com/google/uuid v1.6.0
+ github.com/ktr0731/go-fuzzyfinder v0.8.0
+ github.com/spf13/cobra v1.8.0
+ github.com/spf13/viper v1.18.2
+)
+
+require (
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/gdamore/encoding v1.0.0 // indirect
+ github.com/gdamore/tcell/v2 v2.6.0 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/ktr0731/go-ansisgr v0.1.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/nsf/termbox-go v1.1.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.3 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/stretchr/testify v1.9.0 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.5.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 6b7f390..46af24c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,36 +1,129 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
-github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
-github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
-github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
+github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/ktr0731/go-fuzzyfinder v0.6.0 h1:lP6B3B8CjqbKGf/K5f1X5kdpxiSkCH0+9AzgA3Cm+VU=
-github.com/ktr0731/go-fuzzyfinder v0.6.0/go.mod h1:QrbU5RFMEFBbPZnlJBqctX6028IV8qW/yCX3DCAzi1Y=
-github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
-github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ktr0731/go-ansisgr v0.1.0 h1:fbuupput8739hQbEmZn1cEKjqQFwtCCZNznnF6ANo5w=
+github.com/ktr0731/go-ansisgr v0.1.0/go.mod h1:G9lxwgBwH0iey0Dw5YQd7n6PmQTwTuTM/X5Sgm/UrzE=
+github.com/ktr0731/go-fuzzyfinder v0.8.0 h1:+yobwo9lqZZ7jd1URPdCgZXTE2U1mpIVTkQoo4roi6w=
+github.com/ktr0731/go-fuzzyfinder v0.8.0/go.mod h1:Bjpz5im+tppKE9Ii6UK1h+6RaX/lUvJ0ruO4LIYRkqo=
+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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
-github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c=
-github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
+github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
+github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
-golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go.work b/go.work
new file mode 100644
index 0000000..c9e7481
--- /dev/null
+++ b/go.work
@@ -0,0 +1,2 @@
+go 1.22
+use .
diff --git a/go.work.sum b/go.work.sum
new file mode 100644
index 0000000..ca83e6d
--- /dev/null
+++ b/go.work.sum
@@ -0,0 +1,124 @@
+cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
+cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw=
+cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
+cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
+cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
+cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
+cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
+cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
+cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
+github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
+github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
+github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
+github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
+github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE=
+github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
+github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
+github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
+github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
+github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
+github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
+github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
+github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk=
+github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
+go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k=
+go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
+go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0=
+go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
+go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4=
+go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA=
+go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao=
+go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4=
+google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
+google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
+google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
+google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
diff --git a/main.go b/main.go
index 3e89a73..38c4151 100644
--- a/main.go
+++ b/main.go
@@ -1,22 +1,28 @@
/*
-Copyright © 2021 NAME HERE
+Copyright © 2024 Richard Weston
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
- http://www.apache.org/licenses/LICENSE-2.0
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
*/
package main
-import aztx "github.com/riweston/aztx/cmd/aztx"
+import "github.com/riweston/aztx/cmd"
func main() {
- aztx.SelectAzureAccountsDisplayName()
+ cmd.Execute()
}
diff --git a/pkg/profile/azureProfile.json b/pkg/profile/azureProfile.json
new file mode 100644
index 0000000..42ea0e2
--- /dev/null
+++ b/pkg/profile/azureProfile.json
@@ -0,0 +1,47 @@
+{
+ "installationId": "e960b7cc-c5d9-11ea-a6f5-00155d82a4f4",
+ "subscriptions": [
+ {
+ "id": "9e7969ef-4cb8-4a2d-959f-bfdaae452a3d",
+ "name": "A reeeeeally long subscription name that might be truncated.",
+ "state": "Enabled",
+ "user": {
+ "name": "First Last",
+ "type": "user"
+ },
+ "isDefault": false,
+ "tenantId": "9e7969ef-4cb8-4a2d-959f-bfdaae452a3d",
+ "environmentName": "AzureCloud",
+ "homeTenantId": "9e7969ef-4cb8-4a2d-959f-bfdaae452a3d",
+ "managedByTenants": []
+ },
+ {
+ "id": "9bb28eee-ebaa-442a-83ba-5511810fb151",
+ "name": "Test Subscription 2",
+ "state": "Enabled",
+ "user": {
+ "name": "First Last",
+ "type": "user"
+ },
+ "isDefault": false,
+ "tenantId": "9bb28eee-ebaa-442a-83ba-5511810fb151",
+ "environmentName": "AzureCloud",
+ "homeTenantId": "9bb28eee-ebaa-442a-83ba-5511810fb151",
+ "managedByTenants": []
+ },
+ {
+ "id": "8fff24dd-2842-4dbb-8a66-1410c7bc231f",
+ "name": "Short",
+ "state": "Enabled",
+ "user": {
+ "name": "First Last",
+ "type": "user"
+ },
+ "isDefault": false,
+ "tenantId": "8fff24dd-2842-4dbb-8a66-1410c7bc231f",
+ "environmentName": "AzureCloud",
+ "homeTenantId": "8fff24dd-2842-4dbb-8a66-1410c7bc231f",
+ "managedByTenants": []
+ }
+ ]
+}
diff --git a/pkg/profile/config.go b/pkg/profile/config.go
new file mode 100644
index 0000000..20d23b4
--- /dev/null
+++ b/pkg/profile/config.go
@@ -0,0 +1,74 @@
+package profile
+
+import (
+ "errors"
+ "fmt"
+ "github.com/google/uuid"
+ "github.com/ktr0731/go-fuzzyfinder"
+ azurestate "github.com/riweston/aztx/pkg/state"
+)
+
+type ConfigurationAdapter struct {
+ userProfile userProfileReadWriter
+}
+
+func NewConfigurationAdapter(userProfile userProfileReadWriter) *ConfigurationAdapter {
+ return &ConfigurationAdapter{
+ userProfile: userProfile,
+ }
+}
+
+func (c *ConfigurationAdapter) SelectWithFinder() (*Subscription, error) {
+ cfg, err := c.userProfile.Read()
+ if err != nil {
+ return nil, ErrReadingConfiguration(err)
+ }
+ idx, err := c.userProfile.Find(cfg)
+ if errors.Is(err, fuzzyfinder.ErrAbort) {
+ PrintNotice("Operation aborted")
+ return nil, err
+ }
+ if err != nil {
+ return nil, ErrSelectingSubscription(err)
+ }
+ return &cfg.Subscriptions[idx], nil
+}
+
+func (c *ConfigurationAdapter) SetPreviousContext(lastContext *azurestate.LastContext) error {
+ lastContextId := lastContext.ReadLastContextId()
+ lastContextDisplayName := lastContext.ReadLastContextDisplayName()
+ if lastContextId == "" || lastContextDisplayName == "" {
+ return ErrNoPreviousContext
+ }
+ if err := c.SetContext(lastContext, &Subscription{ID: uuid.MustParse(lastContextId), Name: lastContextDisplayName}); err != nil {
+ return ErrSettingPreviousContext(err)
+ }
+ return nil
+}
+
+func (c *ConfigurationAdapter) SetContext(lastContext *azurestate.LastContext, selectedContext *Subscription) error {
+ cfg, err := c.userProfile.Read()
+ var errNotFound bool
+ if err != nil {
+ return ErrReadingConfiguration(err)
+ }
+ for i, sub := range cfg.Subscriptions {
+ if sub.IsDefault {
+ lastContext.WriteLastContext(sub.ID.String(), sub.Name)
+ }
+ cfg.Subscriptions[i].IsDefault = false
+ if sub.ID == selectedContext.ID {
+ errNotFound = false
+ cfg.Subscriptions[i].IsDefault = true
+ }
+ }
+ if errNotFound {
+ return ErrSubscriptionNotFound
+ }
+ err = c.userProfile.Write(cfg)
+ if err != nil {
+ return ErrWritingConfiguration(err)
+ }
+ PrintInfo(fmt.Sprintf("Switched to \"%s\" (%s)", selectedContext.Name, selectedContext.ID))
+ return nil
+}
diff --git a/pkg/profile/errors.go b/pkg/profile/errors.go
new file mode 100644
index 0000000..5460013
--- /dev/null
+++ b/pkg/profile/errors.go
@@ -0,0 +1,74 @@
+package profile
+
+import (
+ "errors"
+ "fmt"
+)
+
+// The errors file contains the error types used by the azure_cli package.
+
+var (
+ // ErrFileDoesNotExist is returned when the file does not exist.
+ ErrFileDoesNotExist = func(err error) error {
+ return fmt.Errorf("file does not exist: %w", err)
+ }
+
+ // ErrGettingHomeDirectory is returned when there is an error getting the home directory.
+ ErrGettingHomeDirectory = func(err error) error {
+ return fmt.Errorf("error getting home directory: %w", err)
+ }
+
+ // ErrMarshallingJSON is returned when there is an error marshalling JSON.
+ ErrMarshallingJSON = func(err error) error {
+ return fmt.Errorf("error marshalling JSON: %w", err)
+ }
+
+ // ErrUnmarshallingJSON is returned when there is an error unmarshalling JSON.
+ ErrUnmarshallingJSON = func(err error) error {
+ return fmt.Errorf("error unmarshalling JSON: %w", err)
+ }
+
+ // ErrWritingFile is returned when there is an error writing the file.
+ ErrWritingFile = func(err error) error {
+ return fmt.Errorf("error writing file: %w", err)
+ }
+
+ // ErrReadingFile is returned when there is an error reading the file.
+ ErrReadingFile = func(err error) error {
+ return fmt.Errorf("error reading file: %w", err)
+ }
+
+ // ErrPathIsEmpty is returned when the sampleConfigFilePath is empty.
+ ErrPathIsEmpty = errors.New("sampleConfigFilePath is empty")
+
+ // ErrReadingConfiguration is returned when there is an error reading the configuration.
+ ErrReadingConfiguration = func(err error) error {
+ return fmt.Errorf("error reading configuration: %w", err)
+ }
+
+ // ErrWritingConfiguration is returned when there is an error writing the configuration.
+ ErrWritingConfiguration = func(err error) error {
+ return fmt.Errorf("error writing configuration: %w", err)
+ }
+
+ // ErrSelectingSubscription is returned when there is an error selecting the subscription.
+ ErrSelectingSubscription = func(err error) error {
+ return fmt.Errorf("error selecting subscription: %w", err)
+ }
+
+ // ErrSettingPreviousContext is returned when there is an error setting the previous context.
+ ErrSettingPreviousContext = func(err error) error {
+ return fmt.Errorf("error setting previous context: %w", err)
+ }
+
+ // ErrNoPreviousContext is returned when there is no previous context.
+ ErrNoPreviousContext = errors.New("no previous context, check ~/.aztx.yml is present and has content")
+
+ // ErrSubscriptionNotFound is returned when there is no subscription found.
+ ErrSubscriptionNotFound = errors.New("no subscription found")
+
+ // ErrFetchingUserProfile is returned when there is an error fetching the user profile.
+ ErrFetchingUserProfile = func(err error) error {
+ return fmt.Errorf("error fetching user profile: %w", err)
+ }
+)
diff --git a/pkg/profile/models.go b/pkg/profile/models.go
new file mode 100644
index 0000000..fe9c219
--- /dev/null
+++ b/pkg/profile/models.go
@@ -0,0 +1,25 @@
+package profile
+
+import "github.com/google/uuid"
+
+type Configuration struct {
+ InstallationID uuid.UUID `json:"installationId"`
+ Subscriptions []Subscription `json:"subscriptions"`
+}
+
+type Subscription struct {
+ ID uuid.UUID `json:"id"`
+ Name string `json:"name"`
+ State string `json:"state"`
+ User struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ } `json:"user"`
+ IsDefault bool `json:"isDefault"`
+ TenantID uuid.UUID `json:"tenantId"`
+ EnvironmentName string `json:"environmentName"`
+ HomeTenantID uuid.UUID `json:"homeTenantId"`
+ ManagedByTenants []struct {
+ TenantID uuid.UUID `json:"tenantId"`
+ } `json:"managedByTenants"`
+}
diff --git a/pkg/profile/terminal.go b/pkg/profile/terminal.go
new file mode 100644
index 0000000..d15126b
--- /dev/null
+++ b/pkg/profile/terminal.go
@@ -0,0 +1,23 @@
+package profile
+
+import "fmt"
+
+// The terminal file contains helper functions primarily for sending messages to the terminal.
+
+const (
+ InfoColor = "\033[0;32m%s\033[0m"
+ NoticeColor = "\033[0;36m%s\033[0m"
+ WarningColor = "\033[1;33m%s\033[0m"
+ ErrorColor = "\033[1;31m%s\033[0m"
+ DebugColor = "\033[0;36m%s\033[0m"
+)
+
+// PrintInfo prints an info message to the terminal.
+func PrintInfo(message string) {
+ fmt.Println(fmt.Sprintf(InfoColor, message))
+}
+
+// PrintNotice prints a notice message to the terminal.
+func PrintNotice(message string) {
+ fmt.Println(fmt.Sprintf(NoticeColor, message))
+}
diff --git a/pkg/profile/user_profile.go b/pkg/profile/user_profile.go
new file mode 100644
index 0000000..2051649
--- /dev/null
+++ b/pkg/profile/user_profile.go
@@ -0,0 +1,128 @@
+package profile
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "github.com/ktr0731/go-fuzzyfinder"
+ "io"
+ "os"
+)
+
+type userProfileReadWriter interface {
+ Fetch() error
+ Read() (*Configuration, error)
+ Write(*Configuration) error
+ Find(*Configuration) (int, error)
+}
+
+type UserProfileFileAdapter struct {
+ path string
+ configuration *Configuration
+}
+
+func (u *UserProfileFileAdapter) Fetch() error {
+ if u.path != "" {
+ return nil
+ }
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return ErrGettingHomeDirectory(err)
+ }
+ defaultPath := home + "/.azure/azureProfile.json"
+ if _, err := os.Stat(defaultPath); os.IsNotExist(err) {
+ return ErrFileDoesNotExist(err)
+ }
+ u.path = defaultPath
+ return nil
+}
+
+func (u *UserProfileFileAdapter) Write(cfg *Configuration) error {
+ if u.path == "" {
+ return ErrPathIsEmpty
+ }
+ jsonData, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ return ErrMarshallingJSON(err)
+ }
+ err = os.WriteFile(u.path, jsonData, 0644)
+ if err != nil {
+ return ErrWritingFile(err)
+ }
+ return nil
+}
+
+func (u *UserProfileFileAdapter) Read() (*Configuration, error) {
+ if u.configuration != nil {
+ return u.configuration, nil
+ }
+ if err := u.Fetch(); err != nil {
+ return nil, ErrFetchingUserProfile(err)
+ }
+ file, err := u.openConfigFile()
+ if err != nil {
+ return nil, ErrReadingFile(err)
+ }
+ var d Configuration
+ err = u.unmarshalConfig(file, &d)
+ if err != nil {
+ return nil, ErrUnmarshallingJSON(err)
+ }
+ u.configuration = &d
+ return u.configuration, nil
+}
+
+func (u *UserProfileFileAdapter) openConfigFile() ([]byte, error) {
+ if u.path == "" {
+ return nil, ErrPathIsEmpty
+ }
+ file, err := os.Open(u.path)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ b, err := io.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+func (u *UserProfileFileAdapter) unmarshalConfig(data []byte, d *Configuration) error {
+ // handle zero width space character
+ data = bytes.Replace(data, []byte("\uFEFF"), []byte(""), -1)
+ err := json.Unmarshal(data, &d)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (u *UserProfileFileAdapter) Find(cfg *Configuration) (int, error) {
+ columnWidth := u.longestDisplayNameCharacterWidth()
+ format := fmt.Sprintf("%%-%ds %%s", columnWidth)
+ idx, err := fuzzyfinder.Find(
+ cfg.Subscriptions,
+ func(i int) string {
+ if cfg.Subscriptions[i].IsDefault {
+ currentContext := fmt.Sprintf(format, cfg.Subscriptions[i].Name, cfg.Subscriptions[i].ID)
+ return currentContext
+ }
+ return fmt.Sprintf(format, cfg.Subscriptions[i].Name, cfg.Subscriptions[i].ID)
+ },
+ )
+ if err != nil {
+ return 0, err
+ }
+ return idx, nil
+}
+
+func (u *UserProfileFileAdapter) longestDisplayNameCharacterWidth() int {
+ var max int
+ for _, sub := range u.configuration.Subscriptions {
+ if len(sub.Name) > max {
+ max = len(sub.Name)
+ }
+ }
+ return max + 2
+}
diff --git a/pkg/profile/user_profile_test.go b/pkg/profile/user_profile_test.go
new file mode 100644
index 0000000..72f3f0f
--- /dev/null
+++ b/pkg/profile/user_profile_test.go
@@ -0,0 +1,58 @@
+package profile
+
+import (
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestUserProfileFileAdapter_Read(t *testing.T) {
+ type fields struct {
+ sampleConfigFilePath string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ assertion assert.ComparisonAssertionFunc
+ expected *Configuration
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "Reads the configuration file",
+ fields: fields{
+ sampleConfigFilePath: "azureProfile.json",
+ },
+ assertion: assert.EqualValues,
+ expected: &Configuration{
+ Subscriptions: []Subscription{
+ {
+ ID: uuid.MustParse("9e7969ef-4cb8-4a2d-959f-bfdaae452a3d"),
+ Name: "A reeeeeally long subscription name that might be truncated.",
+ },
+ {
+ ID: uuid.MustParse("9bb28eee-ebaa-442a-83ba-5511810fb151"),
+ Name: "Test Subscription 2",
+ },
+ {
+ ID: uuid.MustParse("8fff24dd-2842-4dbb-8a66-1410c7bc231f"),
+ Name: "Short",
+ },
+ },
+ },
+ wantErr: assert.NoError,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ u := &UserProfileFileAdapter{
+ path: tt.fields.sampleConfigFilePath,
+ }
+ got, err := u.Read()
+ for i := range got.Subscriptions {
+ tt.assertion(t, tt.expected.Subscriptions[i].ID, got.Subscriptions[i].ID)
+ tt.assertion(t, tt.expected.Subscriptions[i].Name, got.Subscriptions[i].Name)
+ }
+ tt.wantErr(t, err)
+ })
+ }
+}
diff --git a/pkg/state/state.go b/pkg/state/state.go
new file mode 100644
index 0000000..bf43b73
--- /dev/null
+++ b/pkg/state/state.go
@@ -0,0 +1,47 @@
+package state
+
+import "github.com/spf13/viper"
+
+type stateReaderWriter interface {
+ Read(key string) string
+ Write(key, value string)
+}
+
+type LastContext struct {
+ rw stateReaderWriter
+}
+
+func NewStateReaderWriter(rw stateReaderWriter) *LastContext {
+ return &LastContext{
+ rw: rw,
+ }
+}
+
+func (lc *LastContext) ReadLastContextId() string {
+ return lc.rw.Read("lastContextId")
+
+}
+
+func (lc *LastContext) ReadLastContextDisplayName() string {
+ return lc.rw.Read("lastContextDisplayName")
+}
+
+func (lc *LastContext) WriteLastContext(id string, name string) {
+ lc.rw.Write("lastContextId", id)
+ lc.rw.Write("lastContextDisplayName", name)
+}
+
+type ViperAdapter struct {
+ Viper *viper.Viper
+}
+
+func (v *ViperAdapter) Read(key string) string {
+ return v.Viper.GetString(key)
+}
+
+func (v *ViperAdapter) Write(key, value string) {
+ v.Viper.Set(key, value)
+ if err := v.Viper.WriteConfig(); err != nil {
+ panic(err)
+ }
+}
diff --git a/test/aztx_function_test.go b/test/aztx_function_test.go
deleted file mode 100644
index 873e3e1..0000000
--- a/test/aztx_function_test.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// +build !integration
-
-package aztx_test
-
-import (
- "testing"
-
- "github.com/google/uuid"
- "github.com/riweston/aztx/cmd/aztx"
-)
-
-func TestReadFile(t *testing.T) {
- value := aztx.ReadAzureProfile("azureProfile.json")
- equals(t, "Test Subscription 1", value.Subscriptions[0].Name)
-}
-
-func TestWriteFile(t *testing.T) {
- inputFile := aztx.ReadAzureProfile("azureProfile.json")
- uuid, err := uuid.Parse("9e7969ef-4cb8-4a2d-959f-bfdaae452a3d")
- if err != nil {
- panic(err)
- }
- outFile := aztx.WriteAzureProfile(inputFile, uuid, "azureProfile_test.json")
- ok(t, outFile)
- value := aztx.ReadAzureProfile("azureProfile_test.json")
- equals(t, true, value.Subscriptions[0].IsDefault)
- equals(t, false, value.Subscriptions[1].IsDefault)
- equals(t, false, value.Subscriptions[2].IsDefault)
-}
diff --git a/test/aztx_integration_test.go b/test/aztx_integration_test.go
deleted file mode 100644
index 36b08dc..0000000
--- a/test/aztx_integration_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// +build integration
-
-package aztx
-
-import (
- "os/exec"
- "testing"
-)
-
-func TestAzBinary(t *testing.T) {
- _, err := exec.LookPath("az")
-
- if err != nil {
- t.Errorf(err.Error())
- }
-}
-
-func TestFzfBinary(t *testing.T) {
- _, err := exec.LookPath("fzf")
-
- if err != nil {
- t.Errorf(err.Error())
- }
-}
diff --git a/test/azureProfile.json b/test/azureProfile.json
deleted file mode 100644
index e97e14a..0000000
--- a/test/azureProfile.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "installationId": "e960b7cc-c5d9-11ea-a6f5-00155d82a4f4",
- "subscriptions": [
- {
- "id": "9e7969ef-4cb8-4a2d-959f-bfdaae452a3d",
- "name": "Test Subscription 1",
- "state": "Enabled",
- "user": {
- "name": "First Last",
- "type": "user"
- },
- "isDefault": false,
- "tenantId": "9e7969ef-4cb8-4a2d-959f-bfdaae452a3d",
- "environmentName": "AzureCloud",
- "homeTenantId": "9e7969ef-4cb8-4a2d-959f-bfdaae452a3d",
- "managedByTenants": []
- },
- {
- "id": "9bb28eee-ebaa-442a-83ba-5511810fb151",
- "name": "Test Subscription 2",
- "state": "Enabled",
- "user": {
- "name": "First Last",
- "type": "user"
- },
- "isDefault": false,
- "tenantId": "9bb28eee-ebaa-442a-83ba-5511810fb151",
- "environmentName": "AzureCloud",
- "homeTenantId": "9bb28eee-ebaa-442a-83ba-5511810fb151",
- "managedByTenants": []
- },
- {
- "id": "8fff24dd-2842-4dbb-8a66-1410c7bc231f",
- "name": "Test Subscription 3",
- "state": "Enabled",
- "user": {
- "name": "First Last",
- "type": "user"
- },
- "isDefault": false,
- "tenantId": "8fff24dd-2842-4dbb-8a66-1410c7bc231f",
- "environmentName": "AzureCloud",
- "homeTenantId": "8fff24dd-2842-4dbb-8a66-1410c7bc231f",
- "managedByTenants": []
- }
- ]
-}
diff --git a/test/helpers_test.go b/test/helpers_test.go
deleted file mode 100644
index 19adf7f..0000000
--- a/test/helpers_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package aztx_test
-
-import (
- "fmt"
- "path/filepath"
- "reflect"
- "runtime"
- "testing"
-)
-
-// assert fails the test if the condition is false.
-func assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
- if !condition {
- _, file, line, _ := runtime.Caller(1)
- fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
- tb.FailNow()
- }
-}
-
-// ok fails the test if an err is not nil.
-func ok(tb testing.TB, err error) {
- if err != nil {
- _, file, line, _ := runtime.Caller(1)
- fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error())
- tb.FailNow()
- }
-}
-
-// equals fails the test if exp is not equal to act.
-func equals(tb testing.TB, exp interface{}, act interface{}) {
- if !reflect.DeepEqual(exp, act) {
- _, file, line, _ := runtime.Caller(1)
- fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
- tb.FailNow()
- }
-}