diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9bb8042..02141ab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ permissions: contents: read env: - GO_VERSION: '1.23.6' + GO_VERSION: "1.25.4" jobs: lint: @@ -16,12 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #v5.3.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # ratchet:actions/setup-go@v6.0.0 with: go-version: ${{ env.GO_VERSION }} @@ -32,7 +32,7 @@ jobs: git diff --exit-code go.sum - name: lint - uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea #v6.5.1 + uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # ratchet:golangci/golangci-lint-action@v9.0.0 with: version: latest args: --issues-exit-code=1 --config=.golangci.yml @@ -43,20 +43,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #v5.3.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # ratchet:actions/setup-go@v6.0.0 with: go-version: ${{ env.GO_VERSION }} - name: Build with Goreleaser - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 #v6.2.1 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # ratchet:goreleaser/goreleaser-action@v6.4.0 with: distribution: goreleaser - version: '~> v2' + version: "~> v2" args: release --snapshot --skip publish,archive,sbom,homebrew --clean --config .goreleaser.yaml ci-windows: @@ -65,18 +65,18 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #v5.3.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # ratchet:actions/setup-go@v6.0.0 with: go-version: ${{ env.GO_VERSION }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 #v6.2.1 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # ratchet:goreleaser/goreleaser-action@v6.4.0 with: distribution: goreleaser - version: '~> v2' + version: "~> v2" args: release --snapshot --skip publish,archive,sbom,chocolatey,winget --clean --config .goreleaser-windows.yaml diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index 98b6395..371f989 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -3,14 +3,14 @@ name: goreleaser on: push: tags: - - '*' + - "*" permissions: contents: write packages: write env: - GO_VERSION: '1.23.6' + GO_VERSION: "1.25.4" jobs: goreleaser-linux: @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5.0.0 with: fetch-depth: 0 @@ -26,18 +26,18 @@ jobs: run: git fetch --force --tags - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #v5.3.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # ratchet:actions/setup-go@v6.0.0 with: go-version: ${{ env.GO_VERSION }} - name: Download Syft - uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 #v0.18.0 + uses: anchore/sbom-action/download-syft@8e94d75ddd33f69f691467e42275782e4bfefe84 # ratchet:anchore/sbom-action/download-syft@v0.20.9 - name: Run GoReleaser - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 #v6.2.1 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # ratchet:goreleaser/goreleaser-action@v6.4.0 with: distribution: goreleaser - version: '~> v2' + version: "~> v2" args: release --clean --config .goreleaser.yaml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -49,7 +49,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v5.0.0 with: fetch-depth: 0 @@ -57,17 +57,17 @@ jobs: run: git fetch --force --tags - name: Set up Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #v5.3.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # ratchet:actions/setup-go@v6.0.0 with: go-version: ${{ env.GO_VERSION }} - name: Download Syft - uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 #v0.18.0 + uses: anchore/sbom-action/download-syft@8e94d75ddd33f69f691467e42275782e4bfefe84 # ratchet:anchore/sbom-action/download-syft@v0.20.9 - name: Run GoReleaser - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 #v6.2.1 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # ratchet:goreleaser/goreleaser-action@v6.4.0 with: distribution: goreleaser - version: '~> v2' + version: "~> v2" args: release --clean --config .goreleaser-windows.yaml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 93aaaaa..c19dbc1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ go.work bin dist/ -azctx \ No newline at end of file +azctx + +.vscode/ +AGENTS.md \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 85185bf..44e9207 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,5 @@ +version: "2" + run: tests: true allow-parallel-runners: true @@ -5,10 +7,7 @@ run: # https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml linters: - # Disable all linters. - # Default: false - disable-all: true - # Enable specific linter + default: none enable: - asciicheck - bidichk @@ -17,15 +16,12 @@ linters: - errorlint - funlen #- errcheck - - gofmt - goconst - - gofumpt - gocyclo - godox - gosec #- gocritic #- govet - - goimports - goheader - misspell - nolintlint @@ -38,97 +34,99 @@ linters: - unparam - whitespace #- nilerr + settings: + dupl: + # Tokens count to trigger issue. + # Default: 150 + threshold: 100 -linters-settings: - dupl: - # Tokens count to trigger issue. - # Default: 150 - threshold: 100 - - errorlint: - # Check whether fmt.Errorf uses the %w verb for formatting errors. - # Default: true - asserts: false + errorlint: + # Check whether fmt.Errorf uses the %w verb for formatting errors. + # Default: true + asserts: false - funlen: - # Checks the number of lines in a function. - # Default: 60 - lines: -1 - # Default: 40 - statements: 50 - # Ignore comments when counting lines. - # Default false - ignore-comments: true + funlen: + # Checks the number of lines in a function. + # Default: 60 + lines: -1 + # Default: 40 + statements: 50 + # Ignore comments when counting lines. + # Default false + ignore-comments: true - goconst: - # Minimal length of string constant. - # Default: 3 - min-len: 2 - # Default: 3 - min-occurrences: 3 + goconst: + # Minimal length of string constant. + # Default: 3 + min-len: 2 + # Default: 3 + min-occurrences: 3 - # gocritic: - # enabled-tags: - # - diagnostic - # - experimental - # - opinionated - # - performance - # - style - # disabled-checks: - # - dupImport # https://github.com/go-critic/go-critic/issues/845 - # - ifElseChain - # - octalLiteral - # - whyNoLint + # gocritic: + # enabled-tags: + # - diagnostic + # - experimental + # - opinionated + # - performance + # - style + # disabled-checks: + # - dupImport # https://github.com/go-critic/go-critic/issues/845 + # - ifElseChain + # - octalLiteral + # - whyNoLint - gocyclo: - # Minimal code complexity to report. - # Default: 30 (but we recommend 10-20) - min-complexity: 15 + gocyclo: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 15 - godox: - keywords: - - FIXME - #- TODO - - FIX - - NOTE - - OPTIMIZE # marks code that should be optimized before merging - - HACK # marks hack-around that should be removed before merging + godox: + keywords: + - FIXME + #- TODO + - FIX + - NOTE + - OPTIMIZE # marks code that should be optimized before merging + - HACK # marks hack-around that should be removed before merging - gofmt: - # Simplify code: gofmt with `-s` option. - # Default: true - simplify: false - # Apply the rewrite rules to the source before reformatting. - # https://pkg.go.dev/cmd/gofmt - # Default: [] - rewrite-rules: - # - pattern: 'interface{}' - # replacement: 'any' - - pattern: 'a[b:len(a)]' - replacement: 'a[b:]' + # govet: + # enable-all: true + # disable: + # - fieldalignment - goimports: - local-prefixes: github.com/golangci/golangci-lint + misspell: + # Correct spellings using locale preferences for US or UK. + locale: US - # govet: - # enable-all: true - # disable: - # - fieldalignment + nolintlint: + # Disable to ensure that all nolint directives actually have an effect. + allow-unused: false # report any unused nolint directives + require-explanation: true # require an explanation for nolint directives + require-specific: true # require nolint directives to be specific about which linter is being skipped - misspell: - # Correct spellings using locale preferences for US or UK. - locale: US + # revive: + # rules: + # - name: indent-error-flow + # - name: unexported-return + # disabled: true + # - name: unused-parameter + # - name: unused-receiver - nolintlint: - # Disable to ensure that all nolint directives actually have an effect. - allow-unused: false # report any unused nolint directives - require-explanation: true # require an explanation for nolint directives - require-specific: true # require nolint directives to be specific about which linter is being skipped - - # revive: - # rules: - # - name: indent-error-flow - # - name: unexported-return - # disabled: true - # - name: unused-parameter - # - name: unused-receiver +formatters: + enable: + - gofmt + - gofumpt + - goimports + settings: + gofmt: + # Simplify code: gofmt with `-s` option. + # Default: true + simplify: false + # Apply the rewrite rules to the source before reformatting. + # https://pkg.go.dev/cmd/gofmt + # Default: [] + rewrite-rules: + # - pattern: 'interface{}' + # replacement: 'any' + - pattern: "a[b:len(a)]" + replacement: "a[b:]" diff --git a/.goreleaser-windows.yaml b/.goreleaser-windows.yaml index 7384252..dbdf3f9 100644 --- a/.goreleaser-windows.yaml +++ b/.goreleaser-windows.yaml @@ -11,7 +11,6 @@ builds: - windows goarch: - amd64 - - arm - arm64 ldflags: - -s -w diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c5cbfe9..6531881 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -50,23 +50,24 @@ archives: checksum: name_template: "{{ .ProjectName }}_{{ .Version }}_Darwin_Linux_checksums.txt" -brews: +homebrew_casks: - name: azctx repository: owner: whiteducksoftware name: homebrew-tap branch: main token: "{{ .Env.AUTH_GITHUB }}" - dependencies: - - azure-cli - url_template: "https://github.com/whiteducksoftware/azctx/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - commit_author: - name: "{{ .Env.GITHUB_ACTOR }}" - email: "{{ .Env.GITHUB_ACTOR_ID }}+{{ .Env.GITHUB_ACTOR }}@users.noreply.github.com" - commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" - directory: Formula + binaries: + - azctx + directory: Casks homepage: "https://github.com/whiteducksoftware/azctx" description: "Faster switching between Azure Subscriptions in your Azure CLI" license: "MIT" - install: | - bin.install "azctx" + commit_author: + name: "{{ .Env.GITHUB_ACTOR }}" + email: "{{ .Env.GITHUB_ACTOR_ID }}+{{ .Env.GITHUB_ACTOR }}@users.noreply.github.com" + commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}" + url: + template: "https://github.com/whiteducksoftware/azctx/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + dependencies: + - formula: azure-cli diff --git a/azurecli/azurecli.go b/azurecli/azurecli.go index 8e92837..2630836 100644 --- a/azurecli/azurecli.go +++ b/azurecli/azurecli.go @@ -60,7 +60,9 @@ func (cli *CLI) Reload() error { func (cli CLI) InteractiveLogin(extraArgs []string) error { // Create a spinner s := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithHiddenCursor(false)) - s.Color("green", "italic", "bold") + if err := s.Color("green", "italic", "bold"); err != nil { + log.Warn("Failed to set spinner color: %v", err) + } s.Suffix = " Logging in... Please check your browser for the login prompt." s.Start() defer s.Stop() @@ -78,7 +80,9 @@ func (cli CLI) InteractiveLogin(extraArgs []string) error { func (cli CLI) IterativeTenantLogin(extraArgs []string) error { // Create a spinner s := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithHiddenCursor(false)) - s.Color("green", "italic", "bold") + if err := s.Color("green", "italic", "bold"); err != nil { + log.Warn("Failed to set spinner color: %v", err) + } s.Start() defer s.Stop() diff --git a/azurecli/internal.go b/azurecli/internal.go index 593328f..9c4c71e 100644 --- a/azurecli/internal.go +++ b/azurecli/internal.go @@ -36,7 +36,7 @@ func ensureConfigDir() (string, error) { // Verify that the config dir exists if !utils.FileExists(configDir) { - return "", fmt.Errorf("%s (%s) is not a valid directory. Please run `az configure` and try again.", CONFIG_DIR_ENV, configDir) + return "", fmt.Errorf("%s (%s) is not a valid directory; run `az configure` and try again", CONFIG_DIR_ENV, configDir) } return configDir, nil @@ -53,7 +53,7 @@ func (cli *CLI) readProfile() error { // Verify that the azureProfile.json file exists configFilePath := fmt.Sprintf("%s/%s", configDir, PROFILES_JSON) if !utils.FileExists(configFilePath) { - return fmt.Errorf("%s is not a valid file. Please run `az configure` and try again.", configFilePath) + return fmt.Errorf("%s is not a valid file; run `az configure` and try again", configFilePath) } // Open the azureProfile.json file @@ -62,14 +62,17 @@ func (cli *CLI) readProfile() error { return fmt.Errorf("%s is not a valid file: %s", configFilePath, err.Error()) } + defer func() { + if cerr := configFile.Close(); cerr != nil { + log.Warn("Failed to close %s: %v", configFilePath, cerr) + } + }() + // Unmarshal the config file err = utils.ReadJson(configFile, &cli.profile) if err != nil { - configFile.Close() return err } - - configFile.Close() return nil } @@ -122,14 +125,17 @@ func (cli *CLI) readTenants() error { return fmt.Errorf("%s is not a valid file: %s", configFilePath, err.Error()) } + defer func() { + if cerr := configFile.Close(); cerr != nil { + log.Warn("Failed to close %s: %v", configFilePath, cerr) + } + }() + // Unmarshal the config file err = utils.ReadJson(configFile, &cli.tenants) if err != nil { - configFile.Close() return err } - - configFile.Close() return nil } @@ -148,14 +154,17 @@ func (cli CLI) writeTenants() error { return fmt.Errorf("%s is not a valid file: %s", configFilePath, err.Error()) } + defer func() { + if cerr := configFile.Close(); cerr != nil { + log.Warn("Failed to close %s: %v", configFilePath, cerr) + } + }() + // Marshal the config file err = utils.WriteJson(configFile, cli.tenants) if err != nil { - configFile.Close() return err } - - configFile.Close() return nil } @@ -189,16 +198,17 @@ func (cli CLI) execLogin(extraArgs []string) error { // Check if the line starts with "WARNING:" but ignore the "A web browser has been opened at" line if strings.HasPrefix(line, "WARNING:") && !strings.Contains(line, "A web browser has been opened at") { // Remove the "WARNING: " prefix and print the line - log.Warn(strings.TrimPrefix(line, "WARNING: ")) + log.Warn("%s", strings.TrimPrefix(line, "WARNING: ")) } } // Check if the output contains "mfa" or "multi-factor authentication" stdErrLower := strings.ToLower(stdErrString) if strings.Contains(stdErrLower, "mfa") || strings.Contains(stdErrLower, "multi-factor authentication") { - log.Error(strings.Repeat("-", 80)) + separator := strings.Repeat("-", 80) + log.Error("%s", separator) log.Error("Some tenants require explicit MFA / Individual Authentication. Please run 'azctx login --force-mfa --' to login into each tenant separately.") - log.Error(strings.Repeat("-", 80)) + log.Error("%s", separator) } return nil diff --git a/cmd/login.go b/cmd/login.go index b19db7c..5e7dbda 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -53,15 +53,15 @@ func refreshData(cmd *cobra.Command, cli azurecli.CLI, extraArgs []string) error // Fetch all available tenants err := cli.UpdateTenants() if err != nil { - log.Warn(` -` + - strings.Repeat("-", 80) + ` + separator := strings.Repeat("-", 80) + warnMsg := "\n" + separator + ` Failed fetching available tenants, only tenants which do not require explicit MFA / Individual Authentication will be available. This may be due to the azure cli being completely logged out or due to a network error. Subsequent logins should no longer have this issue. Feel free to open an issue at ` + color.New(color.FgCyan).Sprint("https://github.com/whiteducksoftware/azctx/issues") + ` if this issue persists. -` + strings.Repeat("-", 80)) +` + separator + log.Warn("%s", warnMsg) } // Try to refresh the subscriptions diff --git a/go.mod b/go.mod index 0c2cc8e..d147aae 100644 --- a/go.mod +++ b/go.mod @@ -1,50 +1,52 @@ module github.com/whiteducksoftware/azctx -go 1.23.6 +go 1.25 + +toolchain go1.25.4 require ( github.com/Masterminds/sprig/v3 v3.3.0 - github.com/briandowns/spinner v1.23.1 + github.com/briandowns/spinner v1.23.2 github.com/fatih/color v1.18.0 github.com/lithammer/fuzzysearch v1.1.8 github.com/manifoldco/promptui v0.9.0 - github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 + github.com/mattn/go-runewidth v0.0.19 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/afero v1.11.0 - github.com/spf13/cobra v1.8.1 + github.com/spf13/afero v1.15.0 + github.com/spf13/cobra v1.10.1 go.szostok.io/version v1.2.0 - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 + golang.org/x/term v0.36.0 ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/dustin/go-humanize v1.0.1 // indirect - github.com/goccy/go-yaml v1.11.0 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/muesli/termenv v0.15.2 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d780ea9..a48fec6 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -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/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= -github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= 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= @@ -21,7 +21,11 @@ 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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,14 +35,14 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= -github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/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/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -51,32 +55,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/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -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/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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= -github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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.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= @@ -85,14 +85,15 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -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.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -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/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +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= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= @@ -102,10 +103,10 @@ go.szostok.io/version v1.2.0 h1:8eMMdfsonjbibwZRLJ8TnrErY8bThFTQsZYV16mcXms= go.szostok.io/version v1.2.0/go.mod h1:EiU0gPxaXb6MZ+apSN0WgDO6F4JXyC99k9PIXf2k2E8= 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/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +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-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -123,30 +124,27 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/output.txt b/output.txt new file mode 100644 index 0000000..c4e5733 --- /dev/null +++ b/output.txt @@ -0,0 +1,12 @@ +❯ ./azctx +Use the arrow keys to navigate: ↓ ↑ → ← and / toggles search + Name Tenant +↑ Azure subscription 1 | 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 + euronet-mpa-dev | 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 + euronet-mpa-hub | 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 + euronet-mpa-prod | 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 + euronet-mpa-test | 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 + Azure Subscription Prod. Spoke (Bezahlung über ADN) | 9e65562d-b901-4409-871b-b48100b9d122 + Azure Subscription Test Spoke (Bezahlung über ADN) | 9e65562d-b901-4409-871b-b48100b9d122 + Azure subscription Hub (Bezahlung über ADN) | 9e65562d-b901-4409-871b-b48100b9d122 + Microsoft Azure Sponsorship | 9e65562d-b901-4409-871b-b48100b9d122 \ No newline at end of file diff --git a/output2.txt b/output2.txt new file mode 100644 index 0000000..7ee76d0 --- /dev/null +++ b/output2.txt @@ -0,0 +1,13 @@ +❯ go run . +Use the arrow keys to navigate: ↓ ↑ → ← and / toggles search + + {2d501e73-49ee-4fc0-84e4-2cc2baaa0897 DE1000-Dev Enabled {stephan.huettner@whiteduck.de user} false 4a607258-f067-49d3-bbd4-0a4cf611160b 4a607258-f067-49d3-bbd4-0a4cf611160b AzureCloud 4a607258-f067-49d3-bbd4-0a4cf611160b []} + {d0190d68-364b-4409-b629-f5904fd992cb DE1000-Prod Enabled {stephan.huettner@whiteduck.de user} false 4a607258-f067-49d3-bbd4-0a4cf611160b 4a607258-f067-49d3-bbd4-0a4cf611160b AzureCloud 4a607258-f067-49d3-bbd4-0a4cf611160b [{5f166d1f-49bb-4430-bb98-f80df478e632}]} + {9603c431-3ffc-443f-9340-a68ba626b432 i4Designer Enabled {stephan.huettner@whiteduck.de user} false 6b3e3388-6123-4327-aa06-2d82a999567d 6b3e3388-6123-4327-aa06-2d82a999567d AzureCloud 6b3e3388-6123-4327-aa06-2d82a999567d []} + {532cd3b3-d581-4ddd-9ac9-5c642cd45377 i4connected Enabled {stephan.huettner@whiteduck.de user} false 6b3e3388-6123-4327-aa06-2d82a999567d 6b3e3388-6123-4327-aa06-2d82a999567d AzureCloud 6b3e3388-6123-4327-aa06-2d82a999567d []} + {bb356733-885c-4c04-974f-0d6f38703ebb i4designer development Enabled {stephan.huettner@whiteduck.de user} false 6b3e3388-6123-4327-aa06-2d82a999567d 6b3e3388-6123-4327-aa06-2d82a999567d AzureCloud 6b3e3388-6123-4327-aa06-2d82a999567d []} + {63192c20-b43d-4435-937e-a58f4a0daff9 i4designer production Enabled {stephan.huettner@whiteduck.de user} false 6b3e3388-6123-4327-aa06-2d82a999567d 6b3e3388-6123-4327-aa06-2d82a999567d AzureCloud 6b3e3388-6123-4327-aa06-2d82a999567d []} + {758e0200-f997-4791-a164-7892273d6e67 Azure subscription 1 Enabled {stephan.huettner@whiteduck.de user} false 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 AzureCloud 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 []} + {c60bcd7f-f941-4500-90fb-608a43d152b4 euronet-mpa-dev Enabled {stephan.huettner@whiteduck.de user} false 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 AzureCloud 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 []} + {a5feeca9-905a-4f80-be9b-4d913a96c1ab euronet-mpa-hub Enabled {stephan.huettner@whiteduck.de user} false 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 AzureCloud 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 []} +↓ {9eb9b34e-39b1-4441-acbb-9d0f287cc4d5 euronet-mpa-prod Enabled {stephan.huettner@whiteduck.de user} false 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 AzureCloud 78d1ce4e-e22b-4c15-b9bd-bfcc1d87aa53 []} \ No newline at end of file diff --git a/prompt/pad.go b/prompt/pad.go new file mode 100644 index 0000000..d10bea0 --- /dev/null +++ b/prompt/pad.go @@ -0,0 +1,22 @@ +package prompt + +import ( + "strings" + + "github.com/mattn/go-runewidth" +) + +// pad returns the input string truncated to the given visual width and padded with spaces on the right. +func pad(value string, width int) string { + if width <= 0 { + return "" + } + + truncated := runewidth.Truncate(value, width, "") + padding := width - runewidth.StringWidth(truncated) + if padding <= 0 { + return truncated + } + + return truncated + strings.Repeat(" ", padding) +} diff --git a/prompt/pad_test.go b/prompt/pad_test.go new file mode 100644 index 0000000..7275deb --- /dev/null +++ b/prompt/pad_test.go @@ -0,0 +1,45 @@ +package prompt + +import "testing" + +func TestPad(t *testing.T) { + tests := []struct { + name string + value string + width int + expected string + }{ + { + name: "pads ASCII string", + value: "azctx", + width: 8, + expected: "azctx ", + }, + { + name: "handles multibyte characters without shifting", + value: "Bezahlung über AND", + width: 22, + expected: "Bezahlung über AND ", + }, + { + name: "truncates without breaking utf8", + value: "überraschung", + width: 4, + expected: "über", + }, + { + name: "returns empty string for non-positive widths", + value: "foo", + width: 0, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pad(tt.value, tt.width); got != tt.expected { + t.Fatalf("pad(%q, %d) = %q, want %q", tt.value, tt.width, got, tt.expected) + } + }) + } +} diff --git a/prompt/prompt.go b/prompt/prompt.go index 2b7acc6..d9851cb 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -2,6 +2,7 @@ package prompt import ( "fmt" + "os" "sort" "strings" templates "text/template" @@ -13,21 +14,19 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/lithammer/fuzzysearch/fuzzy" "github.com/manifoldco/promptui" - "github.com/olekukonko/ts" + "golang.org/x/term" ) // BuildPrompt builds a prompt for the user to select a subscription func BuildPrompt(subscriptions utils.ComparableNamedSlice[azurecli.Subscription]) promptui.Select { // Get the terminal dimensions - var terminalWidth, terminalHeigth int - if size, err := ts.GetSize(); err != nil { - terminalWidth = 100 // Default width - terminalHeigth = 20 // Default height + // Detect terminal size; default to safe dimensions for non-interactive sessions. + terminalWidth, terminalHeigth := 100, 20 + if width, height, err := term.GetSize(int(os.Stdout.Fd())); err != nil { log.Warn("Unable to get terminal dimensions, using default values (width: %d, height: %d)", terminalWidth, terminalHeigth) } else { - // Set the terminal dimensions - terminalWidth = size.Col() - terminalHeigth = size.Row() + terminalWidth = width + terminalHeigth = height } // Sort the subscriptions by name @@ -80,6 +79,7 @@ func newTemplateFuncMap() templates.FuncMap { ret["cyan"] = promptui.Styler(promptui.FGCyan) ret["bold"] = promptui.Styler(promptui.FGBold) ret["faint"] = promptui.Styler(promptui.FGFaint) + ret["pad"] = pad return ret } diff --git a/prompt/templates.go b/prompt/templates.go index afd968f..c7c16ab 100644 --- a/prompt/templates.go +++ b/prompt/templates.go @@ -2,21 +2,21 @@ package prompt var ( template_Long = promptTemplate{ - Label: "{{ repeat 4 \" \" }}{{ repeat %[1]d \" \" | print \"Name\" | trunc %[1]d }} {{ repeat 38 \" \" | print \"| SubscriptionId\" | trunc 38 }} {{ repeat %[2]d \" \" | print \"| Tenant\" | trunc %[2]d }}", - Active: "▸ {{ repeat %[1]d \" \" | print .Name | trunc %[1]d | green | %[3]s }} | {{ repeat 36 \" \" | print .Id | trunc 36 | cyan | %[3]s }} | {{ repeat %[2]d \" \" | print \")\" | print .Tenant | print \" (\" | print .TenantName | trunc %[2]d | faint | %[3]s }}", - Inactive: "{{ repeat 2 \" \" }}{{ repeat %[1]d \" \" | print .Name | trunc %[1]d | green | %[3]s }} | {{ repeat 36 \" \" | print .Id | trunc 36 | cyan | %[3]s }} | {{ repeat %[2]d \" \" | print \")\" | print .Tenant | print \" (\" | print .TenantName | trunc %[2]d | faint | %[3]s }}", + Label: "{{ repeat 4 \" \" }}{{ pad \"Name\" %[1]d }} | {{ pad \"SubscriptionId\" 36 }} | {{ pad \"Tenant\" %[2]d }}", + Active: "▸ {{ pad .Name %[1]d | green | %[3]s }} | {{ pad .Id 36 | cyan | %[3]s }} | {{ pad (printf \"%s (%s)\" .Tenant .TenantName) %[2]d | faint | %[3]s }}", + Inactive: "{{ repeat 2 \" \" }}{{ pad .Name %[1]d | green | %[3]s }} | {{ pad .Id 36 | cyan | %[3]s }} | {{ pad (printf \"%s (%s)\" .Tenant .TenantName) %[2]d | faint | %[3]s }}", IncludesIds: true, } template_Short = promptTemplate{ - Label: "{{ repeat 4 \" \" }}{{ repeat %[1]d \" \" | print \"Name\" | trunc %[1]d }} {{ repeat %[2]d \" \" | print \"Tenant\" | trunc %[2]d }}", - Active: "▸ {{ repeat %[1]d \" \" | print .Name | trunc %[1]d | green | %[3]s }} | {{ repeat %[2]d \" \" | print .TenantName | trunc %[2]d | cyan | %[3]s }}", - Inactive: "{{ repeat 2 \" \" }}{{ repeat %[1]d \" \" | print .Name | trunc %[1]d | green | %[3]s }} | {{ repeat %[2]d \" \" | print .TenantName | trunc %[2]d | cyan | %[3]s }}", + Label: "{{ repeat 4 \" \" }}{{ pad \"Name\" %[1]d }} {{ pad \"Tenant\" %[2]d }}", + Active: "▸ {{ pad .Name %[1]d | green | %[3]s }} | {{ pad .TenantName %[2]d | cyan | %[3]s }}", + Inactive: "{{ repeat 2 \" \" }}{{ pad .Name %[1]d | green | %[3]s }} | {{ pad .TenantName %[2]d | cyan | %[3]s }}", IncludesIds: false, } template_VeryShort = promptTemplate{ - Label: "{{ repeat 4 \" \" }}{{ repeat %[1]d \" \" | print \"Name\" | trunc %[1]d }}", - Active: "▸ {{ repeat %[1]d \" \" | print .Name | trunc %[1]d | green | %[3]s }}", - Inactive: "{{ repeat 2 \" \" }}{{ repeat %[1]d \" \" | print .Name | trunc %[1]d | green | %[3]s }}", + Label: "{{ repeat 4 \" \" }}{{ pad \"Name\" %[1]d }}", + Active: "▸ {{ pad .Name %[1]d | green | %[3]s }}", + Inactive: "{{ repeat 2 \" \" }}{{ pad .Name %[1]d | green | %[3]s }}", IncludesIds: false, } ) diff --git a/utils/cobra.go b/utils/cobra.go index 90e4728..5e43eba 100644 --- a/utils/cobra.go +++ b/utils/cobra.go @@ -13,7 +13,7 @@ func WrapCobraCommandHandler(fun func(cmd *cobra.Command, args []string) error) return func(cmd *cobra.Command, args []string) { err := fun(cmd, args) if err != nil { - log.Error(err.Error()) + log.Error("%s", err) os.Exit(1) } } diff --git a/utils/strings.go b/utils/strings.go index 9847f76..0a37328 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -1,6 +1,10 @@ package utils -import "strings" +import ( + "strings" + + "github.com/mattn/go-runewidth" +) // StringSlice is a slice of strings, helper type used for extension methods type StringSlice []string @@ -9,8 +13,8 @@ type StringSlice []string func (slice StringSlice) LongestLength() int { longestLength := 0 for _, s := range slice { - if len(s) > longestLength { - longestLength = len(s) + if width := runewidth.StringWidth(s); width > longestLength { + longestLength = width } } return longestLength