Compare commits
	
		
			85 Commits
		
	
	
		
			0.11.0-bet
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| cc8703310c | |||
| 
						
						
							
						
						fcd5bd863d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e6af2da9dd
	
				 | 
					
					
						|||
| 4b688825e0 | |||
| b0cf2a1f8e | |||
| 6b7020d457 | |||
| efdac610bd | |||
| cd6021f116 | |||
| ee8de8ef5c | |||
| 
						
						
							
						
						e5a653c002
	
				 | 
					
					
						|||
| 
						
						
							
						
						2cca04de90
	
				 | 
					
					
						|||
| f2f79e2df8 | |||
| 
						
						
							
						
						dd83741a9f
	
				 | 
					
					
						|||
| 
						
						
							
						
						dc2cd85d91
	
				 | 
					
					
						|||
| 
						
						
							
						
						96e59cf196
	
				 | 
					
					
						|||
| 
						
						
							
						
						11656c009d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e4e1b58501
	
				 | 
					
					
						|||
| 
						
						
							
						
						3b8f12643c
	
				 | 
					
					
						|||
| 
						
						
							
						
						e5f5154197
	
				 | 
					
					
						|||
| 
						
						
							
						
						6c1c0a8a8a
	
				 | 
					
					
						|||
| 662f45008c | |||
| 
						
						
							
						
						708c5f5223
	
				 | 
					
					
						|||
| d58552b748 | |||
| 
						
						
							
						
						51fe809851
	
				 | 
					
					
						|||
| 3f6a22747f | |||
| 
						
						
							
						
						4e75b96914
	
				 | 
					
					
						|||
| fd4ee75ab7 | |||
| 
						
						
							
						
						964ed834ee
	
				 | 
					
					
						|||
| fcb3167394 | |||
| 
						
						
							
						
						3845b40aa3
	
				 | 
					
					
						|||
| 0dc5c307af | |||
| fc5855ff28 | |||
| 
						
						
							
						
						5b504a1550
	
				 | 
					
					
						|||
| fc16a21f1c | |||
| 
						
						
							
						
						7b4d2d7230
	
				 | 
					
					
						|||
| 
						
						
							
						
						d0ccb805c6
	
				 | 
					
					
						|||
| 
						
						
							
						
						2460dd9438
	
				 | 
					
					
						|||
| 9c648a2566 | |||
| 
						
						
							
						
						22ecfb9c4c
	
				 | 
					
					
						|||
| 
						
						
							
						
						9f3cf718be
	
				 | 
					
					
						|||
| 
						
						
							
						
						b737ce2107
	
				 | 
					
					
						|||
| 
						
						
							
						
						a3d0ece7cb
	
				 | 
					
					
						|||
| 
						
						
							
						
						d63a1c28ea
	
				 | 
					
					
						|||
| 1c10e64c58 | |||
| 
						
						
							
						
						21826ec555
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b4c56d406
	
				 | 
					
					
						|||
| 
						
						
							
						
						4314195dd7
	
				 | 
					
					
						|||
| df4447b038 | |||
| 
						
						
							
						
						3fa660e579
	
				 | 
					
					
						|||
| 
						
						
							
						
						a430b1e4fd
	
				 | 
					
					
						|||
| 896c434f38 | |||
| 
						
						
							
						
						847b7238c5
	
				 | 
					
					
						|||
| 
						
						
							
						
						89d5fc91b0
	
				 | 
					
					
						|||
| 5af3c5f56e | |||
| 
						
						
							
						
						beb3864b2d
	
				 | 
					
					
						|||
| 
						
						
							
						
						581e6ef538
	
				 | 
					
					
						|||
| 
						
						
							
						
						fd642ddb84
	
				 | 
					
					
						|||
| 
						
						
							
						
						1ad8c127d9
	
				 | 
					
					
						|||
| 40aab6a6c1 | |||
| 
						
						
							
						
						4d33a24a07
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee59eb350b
	
				 | 
					
					
						|||
| 
						
						
							
						
						5da13ff15a
	
				 | 
					
					
						|||
| 
						
						
							
						
						491c594ad3
	
				 | 
					
					
						|||
| 
						
						
							
						
						c794d533be
	
				 | 
					
					
						|||
| 
						
						
							
						
						a6daf7030e
	
				 | 
					
					
						|||
| 
						
						
							
						
						fe3b7ffa9c
	
				 | 
					
					
						|||
| 
						
						
							
						
						4c066a92d8
	
				 | 
					
					
						|||
| 
						
						
							
						
						7899b57781
	
				 | 
					
					
						|||
| 
						
						
							
						
						6e0a901887
	
				 | 
					
					
						|||
| 
						
						
							
						
						713fdebc90
	
				 | 
					
					
						|||
| 
						
						
							
						
						6944d138c6
	
				 | 
					
					
						|||
| 
						
						
							
						
						fbb1f16470
	
				 | 
					
					
						|||
| 
						
						
							
						
						2473cafdf5
	
				 | 
					
					
						|||
| 0ccfbd253e | |||
| 
						
						
							
						
						6c4bee0ac7
	
				 | 
					
					
						|||
| 4fa9f536eb | |||
| 
						
						
							
						
						033c9bfc13
	
				 | 
					
					
						|||
| 0db1ee87fc | |||
| 
						
						
							
						
						d180bb924f
	
				 | 
					
					
						|||
| 
						
						
							
						
						d50d68d95a
	
				 | 
					
					
						|||
| 
						
						
							
						
						f468bc7443
	
				 | 
					
					
						|||
| 
						
						
							
						
						dee2d9d104
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c892b1d6a
	
				 | 
					
					
						|||
| 
						
						
							
						
						81b96fc7b1
	
				 | 
					
					
						|||
| c92a0d0703 | 
@ -4,5 +4,4 @@
 | 
			
		||||
Dockerfile
 | 
			
		||||
abra
 | 
			
		||||
dist
 | 
			
		||||
kadabra
 | 
			
		||||
tags
 | 
			
		||||
 | 
			
		||||
@ -11,10 +11,15 @@ steps:
 | 
			
		||||
    image: git.coopcloud.tech/toolshed/drone-xgettext-go:latest
 | 
			
		||||
    settings:
 | 
			
		||||
      keyword: i18n.G
 | 
			
		||||
      keyword_ctx: i18n.GC
 | 
			
		||||
      out: pkg/i18n/locales/abra.pot
 | 
			
		||||
      comments_tag: translators
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - make check
 | 
			
		||||
    when:
 | 
			
		||||
      event:
 | 
			
		||||
        exclude:
 | 
			
		||||
          - tag
 | 
			
		||||
 | 
			
		||||
  - name: xgettext-go status
 | 
			
		||||
    image: golang:1.24-alpine3.22
 | 
			
		||||
@ -27,6 +32,10 @@ steps:
 | 
			
		||||
      - git diff-files --exit-code
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - xgettext-go
 | 
			
		||||
    when:
 | 
			
		||||
      event:
 | 
			
		||||
        exclude:
 | 
			
		||||
          - tag
 | 
			
		||||
 | 
			
		||||
  - name: make test
 | 
			
		||||
    image: golang:1.24
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -4,6 +4,6 @@
 | 
			
		||||
.envrc
 | 
			
		||||
.vscode/
 | 
			
		||||
/abra
 | 
			
		||||
/kadabra
 | 
			
		||||
/bin
 | 
			
		||||
dist/
 | 
			
		||||
tests/integration/.bats
 | 
			
		||||
 | 
			
		||||
@ -32,31 +32,6 @@ builds:
 | 
			
		||||
      - "-s"
 | 
			
		||||
      - "-w"
 | 
			
		||||
 | 
			
		||||
  - id: kadabra
 | 
			
		||||
    binary: kadabra
 | 
			
		||||
    dir: cmd/kadabra
 | 
			
		||||
    env:
 | 
			
		||||
      - CGO_ENABLED=0
 | 
			
		||||
    goos:
 | 
			
		||||
      - linux
 | 
			
		||||
      - darwin
 | 
			
		||||
    goarch:
 | 
			
		||||
      - 386
 | 
			
		||||
      - amd64
 | 
			
		||||
      - arm
 | 
			
		||||
      - arm64
 | 
			
		||||
    goarm:
 | 
			
		||||
      - 5
 | 
			
		||||
      - 6
 | 
			
		||||
      - 7
 | 
			
		||||
    gcflags:
 | 
			
		||||
      - "all=-l -B"
 | 
			
		||||
    ldflags:
 | 
			
		||||
      - "-X 'main.Commit={{ .Commit }}'"
 | 
			
		||||
      - "-X 'main.Version={{ .Version }}'"
 | 
			
		||||
      - "-s"
 | 
			
		||||
      - "-w"
 | 
			
		||||
 | 
			
		||||
checksum:
 | 
			
		||||
  name_template: "checksums.txt"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								Makefile
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
			
		||||
ABRA         := ./cmd/abra
 | 
			
		||||
KADABRA      := ./cmd/kadabra
 | 
			
		||||
XGETTEXT     := ./bin/xgettext-go
 | 
			
		||||
COMMIT       := $(shell git rev-list -1 HEAD)
 | 
			
		||||
GOPATH       := $(shell go env GOPATH)
 | 
			
		||||
GOVERSION    := 1.24
 | 
			
		||||
@ -13,40 +13,23 @@ LINGUAS      := $(basename $(POFILES))
 | 
			
		||||
 | 
			
		||||
export GOPRIVATE=coopcloud.tech
 | 
			
		||||
 | 
			
		||||
# NOTE(d1): default `make` optimised for Abra hacking
 | 
			
		||||
all: format check build-abra test
 | 
			
		||||
all: format check build
 | 
			
		||||
 | 
			
		||||
run-abra:
 | 
			
		||||
run:
 | 
			
		||||
	@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
 | 
			
		||||
 | 
			
		||||
run-kadabra:
 | 
			
		||||
	@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
 | 
			
		||||
 | 
			
		||||
install-abra:
 | 
			
		||||
install:
 | 
			
		||||
	@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
 | 
			
		||||
 | 
			
		||||
install-kadabra:
 | 
			
		||||
	@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
 | 
			
		||||
 | 
			
		||||
install: install-abra install-kadabra
 | 
			
		||||
 | 
			
		||||
build-abra:
 | 
			
		||||
build:
 | 
			
		||||
	@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
 | 
			
		||||
 | 
			
		||||
build-kadabra:
 | 
			
		||||
	@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(KADABRA)
 | 
			
		||||
 | 
			
		||||
build: build-abra build-kadabra
 | 
			
		||||
 | 
			
		||||
build-docker-abra:
 | 
			
		||||
build-docker:
 | 
			
		||||
	@docker run -it -v $(PWD):/abra golang:$(GOVERSION) \
 | 
			
		||||
		bash -c 'cd /abra; ./scripts/docker/build.sh'
 | 
			
		||||
 | 
			
		||||
build-docker: build-docker-abra
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	@rm '$(GOPATH)/bin/abra'
 | 
			
		||||
	@rm '$(GOPATH)/bin/kadabra'
 | 
			
		||||
 | 
			
		||||
format:
 | 
			
		||||
	@gofmt -s -w $$(find . -type f -name '*.go' | grep -v "/vendor/")
 | 
			
		||||
@ -78,14 +61,20 @@ update-po:
 | 
			
		||||
	done
 | 
			
		||||
 | 
			
		||||
.PHONY: update-pot
 | 
			
		||||
update-pot:
 | 
			
		||||
	@xgettext-go \
 | 
			
		||||
update-pot: $(XGETTEXT)
 | 
			
		||||
	@${XGETTEXT} \
 | 
			
		||||
		-o pkg/i18n/locales/$(DOMAIN).pot \
 | 
			
		||||
		--keyword=i18n.G \
 | 
			
		||||
		--keyword-ctx=i18n.GC \
 | 
			
		||||
		--sort-output \
 | 
			
		||||
		--add-comments-tag="translators" \
 | 
			
		||||
		$$(find . -name "*.go" -not -path "*vendor*" | sort)
 | 
			
		||||
 | 
			
		||||
${XGETTEXT}:
 | 
			
		||||
	@mkdir -p ./bin && \
 | 
			
		||||
	wget -O ./bin/xgettext-go https://git.coopcloud.tech/toolshed/xgettext-go/raw/branch/main/xgettext-go && \
 | 
			
		||||
	chmod +x ./bin/xgettext-go
 | 
			
		||||
 | 
			
		||||
.PHONY: update-pot-po-metadata
 | 
			
		||||
update-pot-po-metadata:
 | 
			
		||||
	@sed -i "s/charset=CHARSET/charset=UTF-8/g" pkg/i18n/locales/*.po pkg/i18n/locales/*.pot
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// translators: `abra app` aliases. use a comma separated list of aliases with
 | 
			
		||||
// no spaces in between
 | 
			
		||||
var appAliases = i18n.G("a")
 | 
			
		||||
var appAliases = i18n.GC("a", "abra app")
 | 
			
		||||
 | 
			
		||||
var AppCommand = &cobra.Command{
 | 
			
		||||
	// translators: `app` command group
 | 
			
		||||
 | 
			
		||||
@ -268,7 +268,7 @@ func init() {
 | 
			
		||||
	AppBackupListCommand.Flags().BoolVarP(
 | 
			
		||||
		&showAllPaths,
 | 
			
		||||
		i18n.G("all"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "app backup list"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("show all paths"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ package app
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
@ -103,18 +104,17 @@ checkout as-is. Recipe commit hashes are also supported as values for
 | 
			
		||||
 | 
			
		||||
		toDeployVersion, err = getDeployVersion(args, deployMeta, app)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("get deploy version: %s", err))
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		versionIsChaos := false
 | 
			
		||||
		if !internal.Chaos {
 | 
			
		||||
			var err error
 | 
			
		||||
			versionIsChaos, err = app.Recipe.EnsureVersion(toDeployVersion)
 | 
			
		||||
			isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(i18n.G("ensure recipe: %s", err))
 | 
			
		||||
			}
 | 
			
		||||
			if versionIsChaos {
 | 
			
		||||
			if isChaosCommit {
 | 
			
		||||
				log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion))
 | 
			
		||||
				internal.Chaos = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -152,14 +152,26 @@ checkout as-is. Recipe commit hashes are also supported as values for
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		appPkg.ExposeAllEnv(stackName, compose, app.Env)
 | 
			
		||||
		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
 | 
			
		||||
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos || versionIsChaos)
 | 
			
		||||
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
 | 
			
		||||
		if internal.Chaos {
 | 
			
		||||
			appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
 | 
			
		||||
		}
 | 
			
		||||
		appPkg.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
		appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
 | 
			
		||||
 | 
			
		||||
		versionLabel := toDeployVersion
 | 
			
		||||
		if internal.Chaos {
 | 
			
		||||
			for _, service := range compose.Services {
 | 
			
		||||
				if service.Name == "app" {
 | 
			
		||||
					labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
 | 
			
		||||
					// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
 | 
			
		||||
					versionLabel = service.Deploy.Labels[labelKey]
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		appPkg.SetVersionLabel(compose, stackName, versionLabel)
 | 
			
		||||
 | 
			
		||||
		newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
 | 
			
		||||
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
 | 
			
		||||
 | 
			
		||||
		envVars, err := appPkg.CheckEnv(app)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -186,9 +198,12 @@ checkout as-is. Recipe commit hashes are also supported as values for
 | 
			
		||||
			log.Debug(i18n.G("skipping domain checks"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deployedVersion := config.NO_VERSION_DEFAULT
 | 
			
		||||
		deployedVersion := config.MISSING_DEFAULT
 | 
			
		||||
		if deployMeta.IsDeployed {
 | 
			
		||||
			deployedVersion = deployMeta.Version
 | 
			
		||||
			if deployMeta.IsChaos {
 | 
			
		||||
				deployedVersion = deployMeta.ChaosVersion
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Gather secrets
 | 
			
		||||
@ -300,6 +315,16 @@ func validateArgsAndFlags(args []string) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
 | 
			
		||||
	composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secStats, err := secret.PollSecretsStatus(cl, app)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -307,6 +332,10 @@ func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
 | 
			
		||||
 | 
			
		||||
	for _, secStat := range secStats {
 | 
			
		||||
		if !secStat.CreatedOnRemote {
 | 
			
		||||
			secretConfig := secretsConfig[secStat.LocalName]
 | 
			
		||||
			if secretConfig.SkipGenerate {
 | 
			
		||||
				return errors.New(i18n.G("secret not inserted (#generate=false): %s", secStat.LocalName))
 | 
			
		||||
			}
 | 
			
		||||
			return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -334,7 +363,12 @@ func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app appPkg.
 | 
			
		||||
	// Check if the recipe has a version in the .env file
 | 
			
		||||
	if app.Recipe.EnvVersion != "" && !internal.DeployLatest {
 | 
			
		||||
		if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
 | 
			
		||||
			return "", errors.New(i18n.G("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw))
 | 
			
		||||
			// NOTE(d1): use double-line 5 spaces ("FATA ") trick to make a more
 | 
			
		||||
			// informative error message. it's ugly but that's our logging situation
 | 
			
		||||
			// atm
 | 
			
		||||
			return "", errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?
 | 
			
		||||
     to return to a regular release, specify a release tag, commit SHA or use "--latest"`,
 | 
			
		||||
				formatter.BoldDirtyDefault(app.Recipe.EnvVersionRaw)))
 | 
			
		||||
		}
 | 
			
		||||
		log.Debug(i18n.G("version: taking version from .env file: %s", app.Recipe.EnvVersion))
 | 
			
		||||
		return app.Recipe.EnvVersion, nil
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										311
									
								
								cli/app/env.go
									
									
									
									
									
								
							
							
						
						
									
										311
									
								
								cli/app/env.go
									
									
									
									
									
								
							@ -1,28 +1,50 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/app"
 | 
			
		||||
	appPkg "coopcloud.tech/abra/pkg/app"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	containerPkg "coopcloud.tech/abra/pkg/container"
 | 
			
		||||
	contextPkg "coopcloud.tech/abra/pkg/context"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/i18n"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/docker/docker/api/types/filters"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// translators: `abra app env` aliases. use a comma separated list of aliases with
 | 
			
		||||
// no spaces in between
 | 
			
		||||
// translators: `abra app env` aliases. use a comma separated list of aliases
 | 
			
		||||
// with no spaces in between
 | 
			
		||||
var appEnvAliases = i18n.G("e")
 | 
			
		||||
 | 
			
		||||
var AppEnvCommand = &cobra.Command{
 | 
			
		||||
	// translators: `app env` command
 | 
			
		||||
	Use:     i18n.G("env <domain> [flags]"),
 | 
			
		||||
	Aliases: strings.Split(appEnvAliases, ","),
 | 
			
		||||
	// translators: Short description for `app env` command
 | 
			
		||||
	Short:   i18n.G("Show app .env values"),
 | 
			
		||||
	Example: i18n.G("  abra app env 1312.net"),
 | 
			
		||||
// translators: `abra app env list` aliases. use a comma separated list of
 | 
			
		||||
// aliases with no spaces in between
 | 
			
		||||
var appEnvListAliases = i18n.G("l,ls")
 | 
			
		||||
 | 
			
		||||
// translators: `abra app env pull` aliases. use a comma separated list of
 | 
			
		||||
// aliases with no spaces in between
 | 
			
		||||
var appEnvPullAliases = i18n.G("pl,p")
 | 
			
		||||
 | 
			
		||||
var AppEnvListCommand = &cobra.Command{
 | 
			
		||||
	// translators: `app env list` command
 | 
			
		||||
	Use:     i18n.G("list <domain> [flags]"),
 | 
			
		||||
	Aliases: strings.Split(appEnvListAliases, ","),
 | 
			
		||||
	// translators: Short description for `app env list` command
 | 
			
		||||
	Short:   i18n.G("List all app environment values"),
 | 
			
		||||
	Example: i18n.G("  abra app env list 1312.net"),
 | 
			
		||||
	Args:    cobra.ExactArgs(1),
 | 
			
		||||
	ValidArgsFunction: func(
 | 
			
		||||
		cmd *cobra.Command,
 | 
			
		||||
@ -49,3 +71,274 @@ var AppEnvCommand = &cobra.Command{
 | 
			
		||||
		fmt.Println(overview)
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var AppEnvPullCommand = &cobra.Command{
 | 
			
		||||
	// translators: `app pull` command
 | 
			
		||||
	Use:     i18n.G("pull <domain> [flags]"),
 | 
			
		||||
	Aliases: strings.Split(appEnvPullAliases, ","),
 | 
			
		||||
	// translators: Short description for `app env pull` command
 | 
			
		||||
	Short: i18n.G("Pull app environment values from a deployed app"),
 | 
			
		||||
	Long: i18n.G(`Pull app environment values from a deploymed app.
 | 
			
		||||
 | 
			
		||||
A convenient command for when you've lost your app environment file or want to
 | 
			
		||||
synchronize your local app environment values with what is deployed live.`),
 | 
			
		||||
	Example: i18n.G(`  # pull existing .env file and overwrite local values
 | 
			
		||||
  abra app env pull 1312.net --force
 | 
			
		||||
 | 
			
		||||
  # pull lost app .env file
 | 
			
		||||
  abra app env pull my.gitea.net --server 1312.net`),
 | 
			
		||||
	Args: cobra.MaximumNArgs(2),
 | 
			
		||||
	ValidArgsFunction: func(
 | 
			
		||||
		cmd *cobra.Command,
 | 
			
		||||
		args []string,
 | 
			
		||||
		toComplete string) ([]string, cobra.ShellCompDirective) {
 | 
			
		||||
		return autocomplete.AppNameComplete()
 | 
			
		||||
	},
 | 
			
		||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
		appName := args[0]
 | 
			
		||||
 | 
			
		||||
		appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
 | 
			
		||||
		if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
 | 
			
		||||
			log.Fatal(i18n.G("%s already exists?", appEnvPath))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if server == "" {
 | 
			
		||||
			log.Fatal(i18n.G("unable to determine server of app %s, please pass --server/-s", appName))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		serverDir := filepath.Join(config.SERVERS_DIR, server)
 | 
			
		||||
		if _, err := os.Stat(serverDir); os.IsNotExist(err) {
 | 
			
		||||
			log.Fatal(i18n.G("unknown server %s, run \"abra server add %s\"?", server, server))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		store := contextPkg.NewDefaultDockerContextStore()
 | 
			
		||||
		contexts, err := store.Store.List()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("unable to look up server context for %s: %s", server, err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var contextCreated bool
 | 
			
		||||
		if server == "default" {
 | 
			
		||||
			contextCreated = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, context := range contexts {
 | 
			
		||||
			if context.Name == server {
 | 
			
		||||
				contextCreated = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !contextCreated {
 | 
			
		||||
			log.Fatal(i18n.G("%s missing context, run \"abra server add %s\"?", server, server))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deployMeta, err := stack.IsDeployed(context.Background(), cl, appPkg.StackName(appName))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !deployMeta.IsDeployed {
 | 
			
		||||
			log.Fatal(i18n.G("%s is not deployed?", appName))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		filters := filters.NewArgs()
 | 
			
		||||
		filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(appName), "app"))
 | 
			
		||||
		targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, internal.NoInput)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("unable to retrieve container for %s: %s", appName, err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		inspectResult, err := cl.ContainerInspect(context.Background(), targetContainer.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("unable to inspect container for %s: %s", appName, err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deploymentEnv := make(map[string]string)
 | 
			
		||||
		for _, envVar := range inspectResult.Config.Env {
 | 
			
		||||
			split := strings.SplitN(envVar, "=", 2)
 | 
			
		||||
			if len(split) != 2 {
 | 
			
		||||
				log.Debug(i18n.G("no value attached to %s", envVar))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			key, val := split[0], split[1]
 | 
			
		||||
			deploymentEnv[key] = val
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug(i18n.G("pulled env values from %s deployment: %s", appName, deploymentEnv))
 | 
			
		||||
 | 
			
		||||
		var (
 | 
			
		||||
			recipeEnvVar string
 | 
			
		||||
			recipeKey    string
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		if r, ok := deploymentEnv["TYPE"]; ok {
 | 
			
		||||
			recipeKey = "TYPE"
 | 
			
		||||
			recipeEnvVar = r
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if r, ok := deploymentEnv["RECIPE"]; ok {
 | 
			
		||||
			recipeKey = "RECIPE"
 | 
			
		||||
			recipeEnvVar = r
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if recipeEnvVar == "" {
 | 
			
		||||
			log.Fatal(i18n.G("unable to determine recipe type from %s, env: %v", appName, inspectResult.Config.Env))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var recipeName = recipeEnvVar
 | 
			
		||||
		if strings.Contains(recipeEnvVar, ":") {
 | 
			
		||||
			split := strings.Split(recipeEnvVar, ":")
 | 
			
		||||
			recipeName = split[0]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		recipe := internal.ValidateRecipe(
 | 
			
		||||
			[]string{recipeName},
 | 
			
		||||
			cmd.Name(),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		version := deployMeta.Version
 | 
			
		||||
		if deployMeta.IsChaos {
 | 
			
		||||
			version = deployMeta.ChaosVersion
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := recipe.EnsureVersion(version); err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		mergedEnv, err := recipe.SampleEnv()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug(i18n.G("retrieved env values from .env.sample of %s: %s", recipe.Name, mergedEnv))
 | 
			
		||||
 | 
			
		||||
		for k, v := range deploymentEnv {
 | 
			
		||||
			mergedEnv[k] = v
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !strings.Contains(recipeEnvVar, ":") {
 | 
			
		||||
			mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug(i18n.G("final merged env values for %s are: %s", appName, mergedEnv))
 | 
			
		||||
 | 
			
		||||
		envSample, err := os.ReadFile(recipe.SampleEnvPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.WriteFile(appEnvPath, envSample, 0o664)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		read, err := os.ReadFile(appEnvPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("unable to read new env %s: %s", appEnvPath, err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sampleEnv, err := recipe.SampleEnv()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var composeFileUpdated bool
 | 
			
		||||
		newContents := string(read)
 | 
			
		||||
		for key, val := range mergedEnv {
 | 
			
		||||
			if sampleEnv[key] == val {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if key == "COMPOSE_FILE" {
 | 
			
		||||
				composeFileUpdated = true
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if m, _ := regexp.MatchString(fmt.Sprintf(`#%s=`, key), newContents); m {
 | 
			
		||||
				log.Debug(i18n.G("uncommenting %s", key))
 | 
			
		||||
				re := regexp.MustCompile(fmt.Sprintf(`#%s=`, key))
 | 
			
		||||
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if m, _ := regexp.MatchString(fmt.Sprintf(`# %s=`, key), newContents); m {
 | 
			
		||||
				log.Debug(i18n.G("uncommenting %s", key))
 | 
			
		||||
				re := regexp.MustCompile(fmt.Sprintf(`# %s=`, key))
 | 
			
		||||
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if m, _ := regexp.MatchString(fmt.Sprintf(`%s=".*"`, key), newContents); m {
 | 
			
		||||
				log.Debug(i18n.G(`inserting %s="%s" (double quotes)`, key, val))
 | 
			
		||||
				re := regexp.MustCompile(fmt.Sprintf(`%s=".*"`, key))
 | 
			
		||||
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s="%s"`, key, val))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if m, _ := regexp.MatchString(fmt.Sprintf(`%s='.*'`, key), newContents); m {
 | 
			
		||||
				log.Debug(i18n.G(`inserting %s='%s' (single quotes)`, key, val))
 | 
			
		||||
				re := regexp.MustCompile(fmt.Sprintf(`%s='.*'`, key))
 | 
			
		||||
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s='%s'`, key, val))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if m, _ := regexp.MatchString(fmt.Sprintf("%s=.*", key), newContents); m {
 | 
			
		||||
				log.Debug(i18n.G("inserting %s=%s (no quotes)", key, val))
 | 
			
		||||
				re := regexp.MustCompile(fmt.Sprintf("%s=.*", key))
 | 
			
		||||
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=%s", key, val))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.WriteFile(appEnvPath, []byte(newContents), 0)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Info(i18n.G("%s successfully created", appEnvPath))
 | 
			
		||||
 | 
			
		||||
		if composeFileUpdated {
 | 
			
		||||
			log.Warn(i18n.G("manual update required: COMPOSE_FILE=\"%s\"", mergedEnv["COMPOSE_FILE"]))
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var AppEnvCommand = &cobra.Command{
 | 
			
		||||
	// translators: `app env` command group
 | 
			
		||||
	Use:     i18n.G("env [cmd] [args] [flags]"),
 | 
			
		||||
	Aliases: strings.Split(appEnvAliases, ","),
 | 
			
		||||
	// translators: Short description for `app env` command group
 | 
			
		||||
	Short: i18n.G("Manage app environment values"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	server string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	AppEnvPullCommand.Flags().BoolVarP(
 | 
			
		||||
		&internal.Force,
 | 
			
		||||
		i18n.G("force"),
 | 
			
		||||
		i18n.G("f"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("perform action without further prompt"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	AppEnvPullCommand.Flags().StringVarP(
 | 
			
		||||
		&server,
 | 
			
		||||
		i18n.G("server"),
 | 
			
		||||
		i18n.G("s"),
 | 
			
		||||
		"",
 | 
			
		||||
		i18n.G("server associated with deployed app"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	AppEnvPullCommand.RegisterFlagCompletionFunc(
 | 
			
		||||
		i18n.G("server"),
 | 
			
		||||
		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 | 
			
		||||
			return autocomplete.ServerNameComplete()
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ type appStatus struct {
 | 
			
		||||
	Status       string `json:"status"`
 | 
			
		||||
	Chaos        string `json:"chaos"`
 | 
			
		||||
	ChaosVersion string `json:"chaosVersion"`
 | 
			
		||||
	AutoUpdate   string `json:"autoUpdate"`
 | 
			
		||||
	Version      string `json:"version"`
 | 
			
		||||
	Upgrade      string `json:"upgrade"`
 | 
			
		||||
}
 | 
			
		||||
@ -118,7 +117,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
 | 
			
		||||
					version := i18n.G("unknown")
 | 
			
		||||
					chaos := i18n.G("unknown")
 | 
			
		||||
					chaosVersion := i18n.G("unknown")
 | 
			
		||||
					autoUpdate := i18n.G("unknown")
 | 
			
		||||
					if statusMeta, ok := statuses[app.StackName()]; ok {
 | 
			
		||||
						if currentVersion, exists := statusMeta["version"]; exists {
 | 
			
		||||
							if currentVersion != "" {
 | 
			
		||||
@ -131,9 +129,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
 | 
			
		||||
						if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
 | 
			
		||||
							chaosVersion = chaosDeployVersion
 | 
			
		||||
						}
 | 
			
		||||
						if autoUpdateState, exists := statusMeta["autoUpdate"]; exists {
 | 
			
		||||
							autoUpdate = autoUpdateState
 | 
			
		||||
						}
 | 
			
		||||
						if statusMeta["status"] != "" {
 | 
			
		||||
							status = statusMeta["status"]
 | 
			
		||||
						}
 | 
			
		||||
@ -146,7 +141,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
 | 
			
		||||
					appStats.Chaos = chaos
 | 
			
		||||
					appStats.ChaosVersion = chaosVersion
 | 
			
		||||
					appStats.Version = version
 | 
			
		||||
					appStats.AutoUpdate = autoUpdate
 | 
			
		||||
 | 
			
		||||
					var newUpdates []string
 | 
			
		||||
					if version != "unknown" && chaos == "false" {
 | 
			
		||||
@ -165,6 +159,11 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						for _, update := range updates {
 | 
			
		||||
							if ok := tagcmp.IsParsable(update); !ok {
 | 
			
		||||
								log.Debug(i18n.G("unable to parse %s, skipping as upgrade option", update))
 | 
			
		||||
								continue
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							parsedUpdate, err := tagcmp.Parse(update)
 | 
			
		||||
							if err != nil {
 | 
			
		||||
								log.Fatal(err)
 | 
			
		||||
@ -226,7 +225,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
 | 
			
		||||
					i18n.G("CHAOS"),
 | 
			
		||||
					i18n.G("VERSION"),
 | 
			
		||||
					i18n.G("UPGRADE"),
 | 
			
		||||
					i18n.G("AUTOUPDATE"),
 | 
			
		||||
				}...,
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
@ -257,8 +255,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
 | 
			
		||||
						appStat.Status,
 | 
			
		||||
						chaosStatus,
 | 
			
		||||
						appStat.Version,
 | 
			
		||||
						appStat.Upgrade,
 | 
			
		||||
						appStat.AutoUpdate}...,
 | 
			
		||||
						appStat.Upgrade}...,
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -128,6 +128,10 @@ Use "--dry-run/-r" to see which secrets and volumes will be moved.`),
 | 
			
		||||
			secretName := strings.Join(sname[:len(sname)-1], "_")
 | 
			
		||||
			data := resources.Secrets[secretName]
 | 
			
		||||
			if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil {
 | 
			
		||||
				if strings.Contains(err.Error(), "already exists") {
 | 
			
		||||
					log.Info(i18n.G("skipping secret (because it already exists) on %s: %s", s.Spec.Name, newServer))
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer))
 | 
			
		||||
			}
 | 
			
		||||
			log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer))
 | 
			
		||||
 | 
			
		||||
@ -192,7 +192,27 @@ var AppNewCommand = &cobra.Command{
 | 
			
		||||
		log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))
 | 
			
		||||
 | 
			
		||||
		if len(secretsConfig) > 0 {
 | 
			
		||||
			log.Warn(i18n.G("%s requires secret generation before deploying, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
 | 
			
		||||
			var (
 | 
			
		||||
				hasSecretToGenerate bool
 | 
			
		||||
				hasSecretToSkip     bool
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			for _, secretConfig := range secretsConfig {
 | 
			
		||||
				if secretConfig.SkipGenerate {
 | 
			
		||||
					hasSecretToSkip = true
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				hasSecretToGenerate = true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if hasSecretToGenerate && !generateSecrets {
 | 
			
		||||
				log.Warn(i18n.G("%s requires secret generation before deploy, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if hasSecretToSkip {
 | 
			
		||||
				log.Warn(i18n.G("%s requires secret insertion before deploy (#generate=false)", recipe.Name))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(appSecrets) > 0 {
 | 
			
		||||
 | 
			
		||||
@ -166,7 +166,7 @@ func init() {
 | 
			
		||||
	AppRestartCommand.Flags().BoolVarP(
 | 
			
		||||
		&allServices,
 | 
			
		||||
		i18n.G("all-services"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "app restart"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("restart all services"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	appPkg "coopcloud.tech/abra/pkg/app"
 | 
			
		||||
@ -177,13 +178,14 @@ beforehand. See "abra app backup" for more.`),
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		appPkg.ExposeAllEnv(stackName, compose, app.Env)
 | 
			
		||||
		newRecipeWithDowngradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenDowngrade)
 | 
			
		||||
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDowngradeVersion)
 | 
			
		||||
 | 
			
		||||
		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
 | 
			
		||||
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
 | 
			
		||||
		if internal.Chaos {
 | 
			
		||||
			appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
 | 
			
		||||
		}
 | 
			
		||||
		appPkg.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
 | 
			
		||||
		// Gather secrets
 | 
			
		||||
		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
 | 
			
		||||
@ -203,10 +205,15 @@ beforehand. See "abra app backup" for more.`),
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deployedVersion := deployMeta.Version
 | 
			
		||||
		if deployMeta.IsChaos {
 | 
			
		||||
			deployedVersion = deployMeta.ChaosVersion
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// NOTE(d1): no release notes implemeneted for rolling back
 | 
			
		||||
		if err := internal.DeployOverview(
 | 
			
		||||
			app,
 | 
			
		||||
			deployMeta.Version,
 | 
			
		||||
			deployedVersion,
 | 
			
		||||
			chosenDowngrade,
 | 
			
		||||
			"",
 | 
			
		||||
			downgradeWarnMessages,
 | 
			
		||||
 | 
			
		||||
@ -165,7 +165,7 @@ var AppSecretInsertCommand = &cobra.Command{
 | 
			
		||||
Arbitrary secret insertion is not supported. Secrets that are inserted must
 | 
			
		||||
match those configured in the recipe beforehand.
 | 
			
		||||
 | 
			
		||||
This can be useful when you want to manually generate secrets for an app
 | 
			
		||||
This command can be useful when you want to manually generate secrets for an app
 | 
			
		||||
environment. Typically, you can let Abra generate them for you on app creation
 | 
			
		||||
(see "abra app new --secrets/-S" for more).`),
 | 
			
		||||
	Example: i18n.G(`  # insert regular secret
 | 
			
		||||
@ -574,7 +574,7 @@ func init() {
 | 
			
		||||
	AppSecretGenerateCommand.Flags().BoolVarP(
 | 
			
		||||
		&generateAllSecrets,
 | 
			
		||||
		i18n.G("all"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "app secret generate"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("generate all secrets"),
 | 
			
		||||
	)
 | 
			
		||||
@ -614,7 +614,7 @@ func init() {
 | 
			
		||||
	AppSecretRmCommand.Flags().BoolVarP(
 | 
			
		||||
		&rmAllSecrets,
 | 
			
		||||
		i18n.G("all"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "app secret rm"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("remove all secrets"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ var AppUndeployCommand = &cobra.Command{
 | 
			
		||||
	Use: i18n.G("undeploy <domain> [flags]"),
 | 
			
		||||
	// translators: Short description for `app undeploy` command
 | 
			
		||||
	Aliases: strings.Split(appUndeployAliases, ","),
 | 
			
		||||
	Short:   i18n.G("Undeploy a deployed app"),
 | 
			
		||||
	Long: i18n.G(`This does not destroy any application data.
 | 
			
		||||
 | 
			
		||||
However, you should remain vigilant, as your swarm installation will consider
 | 
			
		||||
@ -65,10 +66,15 @@ Passing "--prune/-p" does not remove those volumes.`),
 | 
			
		||||
			log.Fatal(i18n.G("%s is not deployed?", app.Name))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		version := deployMeta.Version
 | 
			
		||||
		if deployMeta.IsChaos {
 | 
			
		||||
			version = deployMeta.ChaosVersion
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := internal.DeployOverview(
 | 
			
		||||
			app,
 | 
			
		||||
			deployMeta.Version,
 | 
			
		||||
			config.NO_DOMAIN_DEFAULT,
 | 
			
		||||
			version,
 | 
			
		||||
			config.MISSING_DEFAULT,
 | 
			
		||||
			"",
 | 
			
		||||
			nil,
 | 
			
		||||
			nil,
 | 
			
		||||
@ -110,7 +116,7 @@ Passing "--prune/-p" does not remove those volumes.`),
 | 
			
		||||
 | 
			
		||||
		log.Info(i18n.G("undeploy succeeded 🟢"))
 | 
			
		||||
 | 
			
		||||
		if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
 | 
			
		||||
		if err := app.WriteRecipeVersion(version, false); err != nil {
 | 
			
		||||
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@ -190,13 +190,14 @@ beforehand. See "abra app backup" for more.`),
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		appPkg.ExposeAllEnv(stackName, compose, app.Env)
 | 
			
		||||
		newRecipeWithUpgradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenUpgrade)
 | 
			
		||||
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithUpgradeVersion)
 | 
			
		||||
 | 
			
		||||
		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
 | 
			
		||||
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
 | 
			
		||||
		if internal.Chaos {
 | 
			
		||||
			appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
 | 
			
		||||
		}
 | 
			
		||||
		appPkg.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
 | 
			
		||||
		envVars, err := appPkg.CheckEnv(app)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -241,9 +242,14 @@ beforehand. See "abra app backup" for more.`),
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deployedVersion := deployMeta.Version
 | 
			
		||||
		if deployMeta.IsChaos {
 | 
			
		||||
			deployedVersion = deployMeta.ChaosVersion
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := internal.DeployOverview(
 | 
			
		||||
			app,
 | 
			
		||||
			deployMeta.Version,
 | 
			
		||||
			deployedVersion,
 | 
			
		||||
			chosenUpgrade,
 | 
			
		||||
			upgradeReleaseNotes,
 | 
			
		||||
			upgradeWarnMessages,
 | 
			
		||||
 | 
			
		||||
@ -66,12 +66,12 @@ func DeployOverview(
 | 
			
		||||
 | 
			
		||||
	domain := app.Domain
 | 
			
		||||
	if domain == "" {
 | 
			
		||||
		domain = config.NO_DOMAIN_DEFAULT
 | 
			
		||||
		domain = config.MISSING_DEFAULT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	envVersion := app.Recipe.EnvVersionRaw
 | 
			
		||||
	if envVersion == "" {
 | 
			
		||||
		envVersion = config.NO_VERSION_DEFAULT
 | 
			
		||||
		envVersion = config.MISSING_DEFAULT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rows := [][]string{
 | 
			
		||||
@ -140,32 +140,40 @@ func DeployOverview(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDeployType(currentVersion, newVersion string) string {
 | 
			
		||||
	if newVersion == config.NO_DOMAIN_DEFAULT {
 | 
			
		||||
	if newVersion == config.MISSING_DEFAULT {
 | 
			
		||||
		return i18n.G("UNDEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.Contains(newVersion, "+U") {
 | 
			
		||||
		return i18n.G("CHAOS DEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.Contains(currentVersion, "+U") {
 | 
			
		||||
		return i18n.G("UNCHAOS DEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if currentVersion == newVersion {
 | 
			
		||||
		return ("REDEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
	if currentVersion == config.NO_VERSION_DEFAULT {
 | 
			
		||||
 | 
			
		||||
	if currentVersion == config.MISSING_DEFAULT {
 | 
			
		||||
		return i18n.G("NEW DEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	currentParsed, err := tagcmp.Parse(currentVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return i18n.G("DEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newParsed, err := tagcmp.Parse(newVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return i18n.G("DEPLOY")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if currentParsed.IsLessThan(newParsed) {
 | 
			
		||||
		return i18n.G("UPGRADE")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return i18n.G("DOWNGRADE")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -183,17 +191,17 @@ func MoveOverview(
 | 
			
		||||
 | 
			
		||||
	domain := app.Domain
 | 
			
		||||
	if domain == "" {
 | 
			
		||||
		domain = config.NO_DOMAIN_DEFAULT
 | 
			
		||||
		domain = config.MISSING_DEFAULT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretsOverview := strings.Join(secrets, "\n")
 | 
			
		||||
	if len(secrets) == 0 {
 | 
			
		||||
		secretsOverview = config.NO_SECRETS_DEFAULT
 | 
			
		||||
		secretsOverview = config.MISSING_DEFAULT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	volumesOverview := strings.Join(volumes, "\n")
 | 
			
		||||
	if len(volumes) == 0 {
 | 
			
		||||
		volumesOverview = config.NO_VOLUMES_DEFAULT
 | 
			
		||||
		volumesOverview = config.MISSING_DEFAULT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rows := [][]string{
 | 
			
		||||
 | 
			
		||||
@ -119,7 +119,7 @@ func init() {
 | 
			
		||||
	RecipeFetchCommand.Flags().BoolVarP(
 | 
			
		||||
		&fetchAllRecipes,
 | 
			
		||||
		i18n.G("all"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "recipe fetch"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("fetch all recipes"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -135,14 +135,23 @@ your private key and enter your passphrase beforehand.
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
 | 
			
		||||
			var err error
 | 
			
		||||
			tagString, err = getLabelVersion(recipe, false)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
		labelVersion, err := getLabelVersion(recipe, false)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, tag := range tags {
 | 
			
		||||
			previousTagLeftHand := strings.Split(tag, "+")[0]
 | 
			
		||||
			newTagStringLeftHand := strings.Split(labelVersion, "+")[0]
 | 
			
		||||
			if previousTagLeftHand == newTagStringLeftHand {
 | 
			
		||||
				log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
 | 
			
		||||
			tagString = labelVersion
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		isClean, err := gitPkg.IsClean(recipe.Dir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
@ -321,7 +330,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
 | 
			
		||||
 | 
			
		||||
		if !internal.NoInput {
 | 
			
		||||
			prompt := &survey.Confirm{
 | 
			
		||||
				Message: i18n.G("Use release note in release/next?"),
 | 
			
		||||
				Message: i18n.G("use release note in release/next?"),
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -140,13 +140,15 @@ likely to change.
 | 
			
		||||
				if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
 | 
			
		||||
					for serviceName := range serviceVersions {
 | 
			
		||||
						serviceMeta := serviceVersions[serviceName]
 | 
			
		||||
						changesTable.Row(
 | 
			
		||||
							[]string{
 | 
			
		||||
								serviceName,
 | 
			
		||||
								fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag),
 | 
			
		||||
								fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image]),
 | 
			
		||||
							}...,
 | 
			
		||||
						)
 | 
			
		||||
 | 
			
		||||
						existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag)
 | 
			
		||||
						newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image])
 | 
			
		||||
 | 
			
		||||
						if existingImageTag == newImageTag {
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						changesTable.Row([]string{serviceName, existingImageTag, newImageTag}...)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -381,7 +381,7 @@ func init() {
 | 
			
		||||
	RecipeUpgradeCommand.Flags().BoolVarP(
 | 
			
		||||
		&allTags,
 | 
			
		||||
		i18n.G("all-tags"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "recipe upgrade"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("list all tags, not just upgrades"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -283,6 +283,11 @@ Config:
 | 
			
		||||
		app.AppBackupSnapshotsCommand,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	app.AppEnvCommand.AddCommand(
 | 
			
		||||
		app.AppEnvListCommand,
 | 
			
		||||
		app.AppEnvPullCommand,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	app.AppCommand.AddCommand(
 | 
			
		||||
		app.AppBackupCommand,
 | 
			
		||||
		app.AppCheckCommand,
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// translators: `abra server add` aliases. use a comma separated list of
 | 
			
		||||
// aliases with no spaces in between
 | 
			
		||||
var serverAddAliases = i18n.G("a")
 | 
			
		||||
var serverAddAliases = i18n.GC("a", "server add")
 | 
			
		||||
 | 
			
		||||
var ServerAddCommand = &cobra.Command{
 | 
			
		||||
	// translators: `server add` command
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ func init() {
 | 
			
		||||
	ServerPruneCommand.Flags().BoolVarP(
 | 
			
		||||
		&allFilter,
 | 
			
		||||
		i18n.G("all"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		i18n.GC("a", "server prune"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("remove all unused images"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -1,558 +0,0 @@
 | 
			
		||||
package updater
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	appPkg "coopcloud.tech/abra/pkg/app"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/deploy"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/envfile"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/i18n"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/lint"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/convert"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"coopcloud.tech/tagcmp"
 | 
			
		||||
	charmLog "github.com/charmbracelet/log"
 | 
			
		||||
	composetypes "github.com/docker/cli/cli/compose/types"
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
	"github.com/docker/docker/api/types/filters"
 | 
			
		||||
	dockerclient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const SERVER = "localhost"
 | 
			
		||||
 | 
			
		||||
// translators: `kadabra notify` aliases. use a comma separated list of aliases
 | 
			
		||||
// with no spaces in between
 | 
			
		||||
var notifyAliases = i18n.G("n")
 | 
			
		||||
 | 
			
		||||
// NotifyCommand checks for available upgrades.
 | 
			
		||||
var NotifyCommand = &cobra.Command{
 | 
			
		||||
	// translators: `notify` command
 | 
			
		||||
	Use:     i18n.G("notify [flags]"),
 | 
			
		||||
	Aliases: strings.Split(notifyAliases, ","),
 | 
			
		||||
	// translators: Short description for `notify` command
 | 
			
		||||
	Short: i18n.G("Check for available upgrades"),
 | 
			
		||||
	Long: i18n.G(`Notify on new versions for deployed apps.
 | 
			
		||||
 | 
			
		||||
If a new patch/minor version is available, a notification is printed.
 | 
			
		||||
 | 
			
		||||
Use "--major/-m" to include new major versions.`),
 | 
			
		||||
	Args: cobra.NoArgs,
 | 
			
		||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
		cl, err := client.New("default")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stacks, err := stack.GetStacks(cl)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, stackInfo := range stacks {
 | 
			
		||||
			stackName := stackInfo.Name
 | 
			
		||||
			recipeName, err := getLabel(cl, stackName, "recipe")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if recipeName != "" {
 | 
			
		||||
				_, err = getLatestUpgrade(cl, stackName, recipeName)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// translators: `kadabra upgrade` aliases. use a comma separated list of aliases with
 | 
			
		||||
// no spaces in between
 | 
			
		||||
var upgradeAliases = i18n.G("u")
 | 
			
		||||
 | 
			
		||||
// UpgradeCommand upgrades apps.
 | 
			
		||||
var UpgradeCommand = &cobra.Command{
 | 
			
		||||
	// translators: `app upgrade` command
 | 
			
		||||
	Use:     i18n.G("upgrade [[stack] [recipe] | --all] [flags]"),
 | 
			
		||||
	Aliases: strings.Split(upgradeAliases, ","),
 | 
			
		||||
	// translators: Short description for `app upgrade` command
 | 
			
		||||
	Short: i18n.G("Upgrade apps"),
 | 
			
		||||
	Long: i18n.G(`Upgrade an app by specifying stack name and recipe. 
 | 
			
		||||
 | 
			
		||||
Use "--all" to upgrade every deployed app.
 | 
			
		||||
 | 
			
		||||
For each app with auto updates enabled, the deployed version is compared with
 | 
			
		||||
the current recipe catalogue version. If a new patch/minor version is
 | 
			
		||||
available, the app is upgraded.
 | 
			
		||||
 | 
			
		||||
To include major versions use the "--major/-m" flag. You probably don't want
 | 
			
		||||
that as it will break things. Only apps that are not deployed with "--chaos/-C"
 | 
			
		||||
are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it
 | 
			
		||||
with care.`),
 | 
			
		||||
	Args: cobra.RangeArgs(0, 2),
 | 
			
		||||
	// TODO(d1): complete stack/recipe
 | 
			
		||||
	// ValidArgsFunction: func(
 | 
			
		||||
	// 	cmd *cobra.Command,
 | 
			
		||||
	// 	args []string,
 | 
			
		||||
	// 	toComplete string) ([]string, cobra.ShellCompDirective) {
 | 
			
		||||
	// },
 | 
			
		||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
		cl, err := client.New("default")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !updateAll && len(args) != 2 {
 | 
			
		||||
			log.Fatal(i18n.G("missing arguments or --all/-a flag"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !updateAll {
 | 
			
		||||
			stackName := args[0]
 | 
			
		||||
			recipeName := args[1]
 | 
			
		||||
 | 
			
		||||
			err = tryUpgrade(cl, stackName, recipeName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stacks, err := stack.GetStacks(cl)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, stackInfo := range stacks {
 | 
			
		||||
			stackName := stackInfo.Name
 | 
			
		||||
			recipeName, err := getLabel(cl, stackName, "recipe")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = tryUpgrade(cl, stackName, recipeName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getLabel reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
 | 
			
		||||
func getLabel(cl *dockerclient.Client, stackName string, label string) (string, error) {
 | 
			
		||||
	filter := filters.NewArgs()
 | 
			
		||||
	filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
 | 
			
		||||
 | 
			
		||||
	services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, service := range services {
 | 
			
		||||
		labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
 | 
			
		||||
		if labelValue, ok := service.Spec.Labels[labelKey]; ok {
 | 
			
		||||
			return labelValue, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug(i18n.G("no %s label found for %s", label, stackName))
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getBoolLabel reads a boolean docker label from running services
 | 
			
		||||
func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool, error) {
 | 
			
		||||
	lableValue, err := getLabel(cl, stackName, label)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if lableValue != "" {
 | 
			
		||||
		value, err := strconv.ParseBool(lableValue)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return value, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug(i18n.G("boolean label %s could not be found for %s, set default to false.", label, stackName))
 | 
			
		||||
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getEnv reads env variables from docker services.
 | 
			
		||||
func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
 | 
			
		||||
	envMap := make(map[string]string)
 | 
			
		||||
	filter := filters.NewArgs()
 | 
			
		||||
	filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
 | 
			
		||||
 | 
			
		||||
	services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, service := range services {
 | 
			
		||||
		envList := service.Spec.TaskTemplate.ContainerSpec.Env
 | 
			
		||||
		for _, envString := range envList {
 | 
			
		||||
			splitString := strings.SplitN(envString, "=", 2)
 | 
			
		||||
			if len(splitString) != 2 {
 | 
			
		||||
				log.Debug(i18n.G("can't separate key from value: %s (this variable is probably unset)", envString))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			k := splitString[0]
 | 
			
		||||
			v := splitString[1]
 | 
			
		||||
			log.Debugf(i18n.G("for %s read env %s with value: %s from docker service", stackName, k, v))
 | 
			
		||||
			envMap[k] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return envMap, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getLatestUpgrade returns the latest available version for an app respecting
 | 
			
		||||
// the "--major" flag if it is newer than the currently deployed version.
 | 
			
		||||
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
 | 
			
		||||
	deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(availableUpgrades) == 0 {
 | 
			
		||||
		log.Debugf(i18n.G("no available upgrades for %s", stackName))
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chosenUpgrade string
 | 
			
		||||
	if len(availableUpgrades) > 0 {
 | 
			
		||||
		chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
 | 
			
		||||
		log.Info(i18n.G("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return chosenUpgrade, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getDeployedVersion returns the currently deployed version of an app.
 | 
			
		||||
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
 | 
			
		||||
	log.Debug(i18n.G("retrieve deployed version whether %s is already deployed", stackName))
 | 
			
		||||
 | 
			
		||||
	deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !deployMeta.IsDeployed {
 | 
			
		||||
		return "", errors.New(i18n.G("%s is not deployed?", stackName))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if deployMeta.Version == "unknown" {
 | 
			
		||||
		return "", errors.New(i18n.G("failed to determine deployed version of %s", stackName))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return deployMeta.Version, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getAvailableUpgrades returns all available versions of an app that are newer
 | 
			
		||||
// than the deployed version. It only includes major upgrades if the "--major"
 | 
			
		||||
// flag is set.
 | 
			
		||||
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
 | 
			
		||||
	deployedVersion string) ([]string, error) {
 | 
			
		||||
	catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	versions, err := recipe.GetRecipeCatalogueVersions(recipeName, catl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(versions) == 0 {
 | 
			
		||||
		log.Warn(i18n.G("no published releases for %s in the recipe catalogue?", recipeName))
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var availableUpgrades []string
 | 
			
		||||
	for _, version := range versions {
 | 
			
		||||
		parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		parsedVersion, err := tagcmp.Parse(version)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		versionDelta, err := parsedDeployedVersion.UpgradeDelta(parsedVersion)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) {
 | 
			
		||||
			availableUpgrades = append(availableUpgrades, version)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug(i18n.G("available updates for %s: %s", stackName, availableUpgrades))
 | 
			
		||||
 | 
			
		||||
	return availableUpgrades, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
 | 
			
		||||
// recipe repository.
 | 
			
		||||
func processRecipeRepoVersion(r recipe.Recipe, version string) error {
 | 
			
		||||
	if err := r.EnsureExists(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := r.EnsureUpToDate(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := r.EnsureVersion(version); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := lint.LintForErrors(r); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createDeployConfig merges and enriches the compose config for the deployment.
 | 
			
		||||
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) {
 | 
			
		||||
	env["STACK_NAME"] = stackName
 | 
			
		||||
 | 
			
		||||
	deployOpts := stack.Deploy{
 | 
			
		||||
		Namespace:    stackName,
 | 
			
		||||
		Prune:        false,
 | 
			
		||||
		ResolveImage: stack.ResolveImageAlways,
 | 
			
		||||
		Detach:       false,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	composeFiles, err := r.GetComposeFiles(env)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, deployOpts, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	deployOpts.Composefiles = composeFiles
 | 
			
		||||
	compose, err := appPkg.GetAppComposeConfig(stackName, deployOpts, env)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, deployOpts, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	appPkg.ExposeAllEnv(stackName, compose, env)
 | 
			
		||||
 | 
			
		||||
	// after the upgrade the deployment won't be in chaos state anymore
 | 
			
		||||
	appPkg.SetChaosLabel(compose, stackName, false)
 | 
			
		||||
	appPkg.SetRecipeLabel(compose, stackName, r.Name)
 | 
			
		||||
	appPkg.SetUpdateLabel(compose, stackName, env)
 | 
			
		||||
 | 
			
		||||
	return compose, deployOpts, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
 | 
			
		||||
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
 | 
			
		||||
	if recipeName == "" {
 | 
			
		||||
		log.Debug(i18n.G("don't update %s due to missing recipe name", stackName))
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	chaos, err := getBoolLabel(cl, stackName, "chaos")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if chaos && !internal.Chaos {
 | 
			
		||||
		log.Debug(i18n.G("don't update %s due to chaos deployment", stackName))
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updatesEnabled, err := getBoolLabel(cl, stackName, "autoupdate")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !updatesEnabled {
 | 
			
		||||
		log.Debug(i18n.G("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName))
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if upgradeVersion == "" {
 | 
			
		||||
		log.Debug(i18n.G("don't update %s due to no new version", stackName))
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = upgrade(cl, stackName, recipeName, upgradeVersion)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// upgrade performs all necessary steps to upgrade an app.
 | 
			
		||||
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string) error {
 | 
			
		||||
	env, err := getEnv(cl, stackName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	app := appPkg.App{
 | 
			
		||||
		Name:   stackName,
 | 
			
		||||
		Recipe: recipe.Get(recipeName),
 | 
			
		||||
		Server: SERVER,
 | 
			
		||||
		Env:    env,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r := recipe.Get(recipeName)
 | 
			
		||||
 | 
			
		||||
	if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	compose, deployOpts, err := createDeployConfig(r, stackName, app.Env)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info(i18n.G("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion))
 | 
			
		||||
 | 
			
		||||
	serviceNames, err := appPkg.GetAppServiceNames(app.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f, err := app.Filters(true, false, serviceNames...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = stack.RunDeploy(
 | 
			
		||||
		cl,
 | 
			
		||||
		deployOpts,
 | 
			
		||||
		compose,
 | 
			
		||||
		stackName,
 | 
			
		||||
		app.Server,
 | 
			
		||||
		true,
 | 
			
		||||
		f,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newKadabraApp(version, commit string) *cobra.Command {
 | 
			
		||||
	rootCmd := &cobra.Command{
 | 
			
		||||
		// translators: `kadabra` binary name
 | 
			
		||||
		Use:     i18n.G("kadabra [cmd] [flags]"),
 | 
			
		||||
		Version: fmt.Sprintf("%s-%s", version, commit[:7]),
 | 
			
		||||
		// translators: Short description for `kababra` binary
 | 
			
		||||
		Short: i18n.G("The Co-op Cloud auto-updater 🤖 🚀"),
 | 
			
		||||
		PersistentPreRun: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
			log.Logger.SetStyles(charmLog.DefaultStyles())
 | 
			
		||||
			charmLog.SetDefault(log.Logger)
 | 
			
		||||
 | 
			
		||||
			if internal.Debug {
 | 
			
		||||
				log.SetLevel(log.DebugLevel)
 | 
			
		||||
				log.SetOutput(os.Stderr)
 | 
			
		||||
				log.SetReportCaller(true)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.Debug(i18n.G("kadabra version %s, commit %s", version, commit))
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rootCmd.PersistentFlags().BoolVarP(
 | 
			
		||||
		&internal.Debug,
 | 
			
		||||
		i18n.G("debug"),
 | 
			
		||||
		i18n.G("d"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("show debug messages"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	rootCmd.PersistentFlags().BoolVarP(
 | 
			
		||||
		&internal.NoInput,
 | 
			
		||||
		i18n.G("no-input"),
 | 
			
		||||
		i18n.G("n"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("toggle non-interactive mode"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	rootCmd.AddCommand(
 | 
			
		||||
		NotifyCommand,
 | 
			
		||||
		UpgradeCommand,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return rootCmd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RunApp runs CLI abra app.
 | 
			
		||||
func RunApp(version, commit string) {
 | 
			
		||||
	app := newKadabraApp(version, commit)
 | 
			
		||||
 | 
			
		||||
	if err := app.Execute(); err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	includeMajorUpdates bool
 | 
			
		||||
	updateAll           bool
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	NotifyCommand.Flags().BoolVarP(
 | 
			
		||||
		&includeMajorUpdates,
 | 
			
		||||
		"major",
 | 
			
		||||
		"m",
 | 
			
		||||
		false,
 | 
			
		||||
		"check for major updates",
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	UpgradeCommand.Flags().BoolVarP(
 | 
			
		||||
		&internal.Chaos,
 | 
			
		||||
		i18n.G("chaos"),
 | 
			
		||||
		i18n.G("C"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("ignore uncommitted recipes changes"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	UpgradeCommand.Flags().BoolVarP(
 | 
			
		||||
		&includeMajorUpdates,
 | 
			
		||||
		i18n.G("major"),
 | 
			
		||||
		i18n.G("m"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("check for major updates"),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	UpgradeCommand.Flags().BoolVarP(
 | 
			
		||||
		&updateAll,
 | 
			
		||||
		i18n.G("all"),
 | 
			
		||||
		i18n.G("a"),
 | 
			
		||||
		false,
 | 
			
		||||
		i18n.G("update all deployed apps"),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
// Package main provides the command-line entrypoint.
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"coopcloud.tech/abra/cli/updater"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Version is the current version of Kadabra.
 | 
			
		||||
var Version string
 | 
			
		||||
 | 
			
		||||
// Commit is the current git commit of Kadabra.
 | 
			
		||||
var Commit string
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	if Version == "" {
 | 
			
		||||
		Version = "dev"
 | 
			
		||||
	}
 | 
			
		||||
	if Commit == "" {
 | 
			
		||||
		Commit = "       "
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updater.RunApp(Version, Commit)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										83
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										83
									
								
								go.mod
									
									
									
									
									
								
							@ -8,13 +8,15 @@ require (
 | 
			
		||||
	coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
 | 
			
		||||
	git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
 | 
			
		||||
	github.com/AlecAivazis/survey/v2 v2.3.7
 | 
			
		||||
	github.com/charmbracelet/bubbletea v1.3.6
 | 
			
		||||
	github.com/charmbracelet/bubbles v0.21.0
 | 
			
		||||
	github.com/charmbracelet/bubbletea v1.3.10
 | 
			
		||||
	github.com/charmbracelet/lipgloss v1.1.0
 | 
			
		||||
	github.com/charmbracelet/log v0.4.2
 | 
			
		||||
	github.com/distribution/reference v0.6.0
 | 
			
		||||
	github.com/docker/cli v28.3.3+incompatible
 | 
			
		||||
	github.com/docker/docker v28.3.3+incompatible
 | 
			
		||||
	github.com/docker/cli v28.4.0+incompatible
 | 
			
		||||
	github.com/docker/docker v28.4.0+incompatible
 | 
			
		||||
	github.com/docker/go-units v0.5.0
 | 
			
		||||
	github.com/evertras/bubble-table v0.19.2
 | 
			
		||||
	github.com/go-git/go-git/v5 v5.16.2
 | 
			
		||||
	github.com/google/go-cmp v0.7.0
 | 
			
		||||
	github.com/leonelquinteros/gotext v1.7.2
 | 
			
		||||
@ -22,7 +24,7 @@ require (
 | 
			
		||||
	github.com/moby/term v0.5.2
 | 
			
		||||
	github.com/pkg/errors v0.9.1
 | 
			
		||||
	github.com/schollz/progressbar/v3 v3.18.0
 | 
			
		||||
	golang.org/x/term v0.34.0
 | 
			
		||||
	golang.org/x/term v0.35.0
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
	gotest.tools/v3 v3.5.2
 | 
			
		||||
)
 | 
			
		||||
@ -33,22 +35,24 @@ require (
 | 
			
		||||
	github.com/BurntSushi/toml v1.5.0 // indirect
 | 
			
		||||
	github.com/Microsoft/go-winio v0.6.2 // indirect
 | 
			
		||||
	github.com/ProtonMail/go-crypto v1.3.0 // indirect
 | 
			
		||||
	github.com/atotto/clipboard v0.1.4 // indirect
 | 
			
		||||
	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 | 
			
		||||
	github.com/beorn7/perks v1.0.1 // indirect
 | 
			
		||||
	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 | 
			
		||||
	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 | 
			
		||||
	github.com/charmbracelet/colorprofile v0.3.2 // indirect
 | 
			
		||||
	github.com/charmbracelet/x/ansi v0.10.1 // indirect
 | 
			
		||||
	github.com/charmbracelet/x/ansi v0.10.2 // indirect
 | 
			
		||||
	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
 | 
			
		||||
	github.com/charmbracelet/x/term v0.2.1 // indirect
 | 
			
		||||
	github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
 | 
			
		||||
	github.com/cloudflare/circl v1.6.1 // indirect
 | 
			
		||||
	github.com/containerd/errdefs v1.0.0 // indirect
 | 
			
		||||
	github.com/containerd/errdefs/pkg v0.3.0 // indirect
 | 
			
		||||
	github.com/containerd/log v0.1.0 // indirect
 | 
			
		||||
	github.com/containerd/platforms v0.2.1 // indirect
 | 
			
		||||
	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
 | 
			
		||||
	github.com/cyphar/filepath-securejoin v0.4.1 // indirect
 | 
			
		||||
	github.com/cyphar/filepath-securejoin v0.5.0 // indirect
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
			
		||||
	github.com/docker/distribution v2.8.3+incompatible // indirect
 | 
			
		||||
	github.com/docker/go-connections v0.6.0 // indirect
 | 
			
		||||
@ -64,21 +68,21 @@ require (
 | 
			
		||||
	github.com/go-logr/logr v1.4.3 // indirect
 | 
			
		||||
	github.com/go-logr/stdr v1.2.2 // indirect
 | 
			
		||||
	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
 | 
			
		||||
	github.com/gogo/protobuf v1.3.2 // indirect
 | 
			
		||||
	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 | 
			
		||||
	github.com/google/uuid v1.6.0 // indirect
 | 
			
		||||
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 | 
			
		||||
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
 | 
			
		||||
	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 | 
			
		||||
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 | 
			
		||||
	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 | 
			
		||||
	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 | 
			
		||||
	github.com/kevinburke/ssh_config v1.2.0 // indirect
 | 
			
		||||
	github.com/kevinburke/ssh_config v1.4.0 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.18.0 // indirect
 | 
			
		||||
	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.3.0 // 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-localereader v0.0.1 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.16 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.19 // indirect
 | 
			
		||||
	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 | 
			
		||||
	github.com/miekg/pkcs11 v1.1.1 // indirect
 | 
			
		||||
	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
 | 
			
		||||
@ -90,47 +94,48 @@ require (
 | 
			
		||||
	github.com/morikuni/aec v1.0.0 // indirect
 | 
			
		||||
	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 | 
			
		||||
	github.com/muesli/cancelreader v0.2.2 // indirect
 | 
			
		||||
	github.com/muesli/reflow v0.3.0 // indirect
 | 
			
		||||
	github.com/muesli/termenv v0.16.0 // indirect
 | 
			
		||||
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 | 
			
		||||
	github.com/opencontainers/go-digest v1.0.0 // indirect
 | 
			
		||||
	github.com/opencontainers/runc v1.1.13 // indirect
 | 
			
		||||
	github.com/opencontainers/runtime-spec v1.1.0 // indirect
 | 
			
		||||
	github.com/pjbgf/sha1cd v0.4.0 // indirect
 | 
			
		||||
	github.com/pjbgf/sha1cd v0.5.0 // indirect
 | 
			
		||||
	github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
			
		||||
	github.com/prometheus/client_model v0.6.2 // indirect
 | 
			
		||||
	github.com/prometheus/common v0.65.0 // indirect
 | 
			
		||||
	github.com/prometheus/common v0.66.1 // indirect
 | 
			
		||||
	github.com/prometheus/procfs v0.17.0 // indirect
 | 
			
		||||
	github.com/rivo/uniseg v0.4.7 // indirect
 | 
			
		||||
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.3 // indirect
 | 
			
		||||
	github.com/skeema/knownhosts v1.3.1 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.7 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.10 // indirect
 | 
			
		||||
	github.com/xanzy/ssh-agent v0.3.3 // indirect
 | 
			
		||||
	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 | 
			
		||||
	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 | 
			
		||||
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 | 
			
		||||
	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
 | 
			
		||||
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/trace v1.37.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/proto/otlp v1.7.1 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.41.0 // indirect
 | 
			
		||||
	golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
 | 
			
		||||
	golang.org/x/net v0.43.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.16.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.28.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.12.0 // indirect
 | 
			
		||||
	google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
 | 
			
		||||
	google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
 | 
			
		||||
	google.golang.org/grpc v1.74.2 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.36.7 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/metric v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/sdk v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/otel/trace v1.38.0 // indirect
 | 
			
		||||
	go.opentelemetry.io/proto/otlp v1.8.0 // indirect
 | 
			
		||||
	go.yaml.in/yaml/v2 v2.4.3 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.42.0 // indirect
 | 
			
		||||
	golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
 | 
			
		||||
	golang.org/x/net v0.44.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.29.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.13.0 // indirect
 | 
			
		||||
	google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
 | 
			
		||||
	google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.75.1 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.36.9 // indirect
 | 
			
		||||
	gopkg.in/warnings.v0 v0.1.2 // indirect
 | 
			
		||||
	gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
@ -147,11 +152,11 @@ require (
 | 
			
		||||
	github.com/moby/patternmatcher v0.6.0 // indirect
 | 
			
		||||
	github.com/moby/sys/sequential v0.6.0 // indirect
 | 
			
		||||
	github.com/opencontainers/image-spec v1.1.1 // indirect
 | 
			
		||||
	github.com/prometheus/client_golang v1.23.0 // indirect
 | 
			
		||||
	github.com/prometheus/client_golang v1.23.2 // indirect
 | 
			
		||||
	github.com/sergi/go-diff v1.4.0 // indirect
 | 
			
		||||
	github.com/spf13/cobra v1.9.1
 | 
			
		||||
	github.com/stretchr/testify v1.10.0
 | 
			
		||||
	github.com/spf13/cobra v1.10.1
 | 
			
		||||
	github.com/stretchr/testify v1.11.1
 | 
			
		||||
	github.com/theupdateframework/notary v0.7.0 // indirect
 | 
			
		||||
	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 | 
			
		||||
	golang.org/x/sys v0.35.0
 | 
			
		||||
	golang.org/x/sys v0.36.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										175
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								go.sum
									
									
									
									
									
								
							@ -99,6 +99,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
 | 
			
		||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 | 
			
		||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 | 
			
		||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 | 
			
		||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 | 
			
		||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 | 
			
		||||
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 | 
			
		||||
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=
 | 
			
		||||
@ -133,20 +135,22 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
 | 
			
		||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
 | 
			
		||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
 | 
			
		||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
 | 
			
		||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
 | 
			
		||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
 | 
			
		||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
 | 
			
		||||
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
 | 
			
		||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
 | 
			
		||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
 | 
			
		||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
 | 
			
		||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
 | 
			
		||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
 | 
			
		||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
 | 
			
		||||
github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
 | 
			
		||||
github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
 | 
			
		||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
 | 
			
		||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
 | 
			
		||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
 | 
			
		||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
 | 
			
		||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
 | 
			
		||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
 | 
			
		||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
 | 
			
		||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
 | 
			
		||||
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
 | 
			
		||||
@ -164,6 +168,8 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
 | 
			
		||||
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
 | 
			
		||||
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
 | 
			
		||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
			
		||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
 | 
			
		||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
 | 
			
		||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
 | 
			
		||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
 | 
			
		||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
 | 
			
		||||
@ -296,8 +302,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 | 
			
		||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 | 
			
		||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
 | 
			
		||||
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
 | 
			
		||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
 | 
			
		||||
@ -316,16 +322,16 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
 | 
			
		||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 | 
			
		||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 | 
			
		||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 | 
			
		||||
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
 | 
			
		||||
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 | 
			
		||||
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
 | 
			
		||||
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 | 
			
		||||
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
 | 
			
		||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 | 
			
		||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
 | 
			
		||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
 | 
			
		||||
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
 | 
			
		||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
 | 
			
		||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
 | 
			
		||||
@ -367,6 +373,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
 | 
			
		||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
 | 
			
		||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 | 
			
		||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 | 
			
		||||
github.com/evertras/bubble-table v0.19.2 h1:u77oiM6JlRR+CvS5FZc3Hz+J6iEsvEDcR5kO8OFb1Yw=
 | 
			
		||||
github.com/evertras/bubble-table v0.19.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ=
 | 
			
		||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 | 
			
		||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 | 
			
		||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 | 
			
		||||
@ -439,7 +447,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
 | 
			
		||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 | 
			
		||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 | 
			
		||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 | 
			
		||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 | 
			
		||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 | 
			
		||||
@ -529,8 +536,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
 | 
			
		||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
 | 
			
		||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
			
		||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
			
		||||
@ -579,8 +586,8 @@ github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVE
 | 
			
		||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
			
		||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 | 
			
		||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 | 
			
		||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 | 
			
		||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 | 
			
		||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
 | 
			
		||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
 | 
			
		||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 | 
			
		||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 | 
			
		||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 | 
			
		||||
@ -590,6 +597,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
 | 
			
		||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 | 
			
		||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 | 
			
		||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 | 
			
		||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
			
		||||
@ -611,8 +620,8 @@ github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+
 | 
			
		||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
 | 
			
		||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 | 
			
		||||
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
 | 
			
		||||
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/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 | 
			
		||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 | 
			
		||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 | 
			
		||||
@ -631,8 +640,9 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
 | 
			
		||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 | 
			
		||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 | 
			
		||||
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/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 | 
			
		||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 | 
			
		||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 | 
			
		||||
@ -692,6 +702,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
 | 
			
		||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
 | 
			
		||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 | 
			
		||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 | 
			
		||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 | 
			
		||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 | 
			
		||||
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/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 | 
			
		||||
@ -758,8 +770,8 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt
 | 
			
		||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 | 
			
		||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 | 
			
		||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 | 
			
		||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
 | 
			
		||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
 | 
			
		||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
 | 
			
		||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
 | 
			
		||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
@ -775,8 +787,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
 | 
			
		||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 | 
			
		||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
 | 
			
		||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 | 
			
		||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
 | 
			
		||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 | 
			
		||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
 | 
			
		||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 | 
			
		||||
@ -790,8 +802,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 | 
			
		||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 | 
			
		||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
 | 
			
		||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 | 
			
		||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 | 
			
		||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 | 
			
		||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
 | 
			
		||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 | 
			
		||||
@ -806,6 +818,7 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
 | 
			
		||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
 | 
			
		||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
 | 
			
		||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 | 
			
		||||
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.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 | 
			
		||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 | 
			
		||||
@ -851,8 +864,8 @@ github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
 | 
			
		||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 | 
			
		||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 | 
			
		||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 | 
			
		||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 | 
			
		||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 | 
			
		||||
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/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 | 
			
		||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 | 
			
		||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 | 
			
		||||
@ -861,9 +874,9 @@ github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bd
 | 
			
		||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 | 
			
		||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 | 
			
		||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
			
		||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
			
		||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
 | 
			
		||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
			
		||||
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/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
 | 
			
		||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 | 
			
		||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
 | 
			
		||||
@ -878,8 +891,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 | 
			
		||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 | 
			
		||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
			
		||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 | 
			
		||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 | 
			
		||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 | 
			
		||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 | 
			
		||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
 | 
			
		||||
@ -937,37 +950,39 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 | 
			
		||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 | 
			
		||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 | 
			
		||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 | 
			
		||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
 | 
			
		||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
 | 
			
		||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
 | 
			
		||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
 | 
			
		||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 | 
			
		||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
 | 
			
		||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
 | 
			
		||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
 | 
			
		||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
 | 
			
		||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
 | 
			
		||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
 | 
			
		||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
 | 
			
		||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
 | 
			
		||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
 | 
			
		||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
 | 
			
		||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
 | 
			
		||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 | 
			
		||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
 | 
			
		||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
 | 
			
		||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
 | 
			
		||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
 | 
			
		||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
 | 
			
		||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
 | 
			
		||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 | 
			
		||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
 | 
			
		||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 | 
			
		||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
 | 
			
		||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
 | 
			
		||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 | 
			
		||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 | 
			
		||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 | 
			
		||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 | 
			
		||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 | 
			
		||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 | 
			
		||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
 | 
			
		||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
@ -986,8 +1001,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
 | 
			
		||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 | 
			
		||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
 | 
			
		||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 | 
			
		||||
@ -998,8 +1013,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 | 
			
		||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 | 
			
		||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 | 
			
		||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 | 
			
		||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
 | 
			
		||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
 | 
			
		||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
 | 
			
		||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
 | 
			
		||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 | 
			
		||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 | 
			
		||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 | 
			
		||||
@ -1063,8 +1078,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 | 
			
		||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
 | 
			
		||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 | 
			
		||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
 | 
			
		||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
@ -1082,8 +1097,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
 | 
			
		||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@ -1162,13 +1175,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 | 
			
		||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
 | 
			
		||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 | 
			
		||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
 | 
			
		||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 | 
			
		||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
			
		||||
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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
 | 
			
		||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
 | 
			
		||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
 | 
			
		||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
 | 
			
		||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
@ -1178,16 +1191,16 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
			
		||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
 | 
			
		||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 | 
			
		||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
 | 
			
		||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
 | 
			
		||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
 | 
			
		||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 | 
			
		||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
 | 
			
		||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
@ -1237,6 +1250,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
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=
 | 
			
		||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
 | 
			
		||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
 | 
			
		||||
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 | 
			
		||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 | 
			
		||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 | 
			
		||||
@ -1281,10 +1296,10 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
 | 
			
		||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
			
		||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58=
 | 
			
		||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a/go.mod h1:y2yVLIE/CSMCPXaHnSKXxu1spLPnglFLegmgdY23uuE=
 | 
			
		||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
 | 
			
		||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
 | 
			
		||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
 | 
			
		||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
 | 
			
		||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
 | 
			
		||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
 | 
			
		||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 | 
			
		||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 | 
			
		||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 | 
			
		||||
@ -1304,8 +1319,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
 | 
			
		||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 | 
			
		||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 | 
			
		||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 | 
			
		||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
 | 
			
		||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 | 
			
		||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
 | 
			
		||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 | 
			
		||||
@ -1319,8 +1334,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 | 
			
		||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 | 
			
		||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 | 
			
		||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 | 
			
		||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
 | 
			
		||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 | 
			
		||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
 | 
			
		||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
 | 
			
		||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 | 
			
		||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 | 
			
		||||
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
 | 
			
		||||
 | 
			
		||||
@ -471,13 +471,6 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
 | 
			
		||||
				result["chaosVersion"] = chaosVersion
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
 | 
			
		||||
			if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
 | 
			
		||||
				result["autoUpdate"] = autoUpdate
 | 
			
		||||
			} else {
 | 
			
		||||
				result["autoUpdate"] = "false"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
 | 
			
		||||
			if version, ok := service.Spec.Labels[labelKey]; ok {
 | 
			
		||||
				result["version"] = version
 | 
			
		||||
@ -509,7 +502,11 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExposeAllEnv exposes all env variables to the app container
 | 
			
		||||
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
 | 
			
		||||
func ExposeAllEnv(
 | 
			
		||||
	stackName string,
 | 
			
		||||
	compose *composetypes.Config,
 | 
			
		||||
	appEnv envfile.AppEnv,
 | 
			
		||||
	toDeployVersion string) {
 | 
			
		||||
	for _, service := range compose.Services {
 | 
			
		||||
		if service.Name == "app" {
 | 
			
		||||
			log.Debug(i18n.G("adding env vars to %s service config", stackName))
 | 
			
		||||
@ -517,6 +514,11 @@ func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile
 | 
			
		||||
				_, exists := service.Environment[k]
 | 
			
		||||
				if !exists {
 | 
			
		||||
					value := v
 | 
			
		||||
					if k == "TYPE" || k == "RECIPE" {
 | 
			
		||||
						// NOTE(d1): don't use the wrong version from the app env
 | 
			
		||||
						//           since we are deploying a new version
 | 
			
		||||
						value = toDeployVersion
 | 
			
		||||
					}
 | 
			
		||||
					service.Environment[k] = &value
 | 
			
		||||
					log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/envfile"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/i18n"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
	composetypes "github.com/docker/cli/cli/compose/types"
 | 
			
		||||
@ -56,23 +55,6 @@ func SetVersionLabel(compose *composetypes.Config, stackName string, version str
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
 | 
			
		||||
// auto update process for this app. The default if this variable is not set is to disable
 | 
			
		||||
// the auto update process.
 | 
			
		||||
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv envfile.AppEnv) {
 | 
			
		||||
	for _, service := range compose.Services {
 | 
			
		||||
		if service.Name == "app" {
 | 
			
		||||
			enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
 | 
			
		||||
			if !exists {
 | 
			
		||||
				enable_auto_update = "false"
 | 
			
		||||
			}
 | 
			
		||||
			log.Debug(i18n.G("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName))
 | 
			
		||||
			labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
 | 
			
		||||
			service.Deploy.Labels[labelKey] = enable_auto_update
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
 | 
			
		||||
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
 | 
			
		||||
	for _, service := range compose.Services {
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,11 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	contextPkg "coopcloud.tech/abra/pkg/context"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/i18n"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
@ -41,7 +43,21 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
 | 
			
		||||
 | 
			
		||||
	ctx, err := GetContext(serverName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
 | 
			
		||||
		serverDir := path.Join(config.SERVERS_DIR, serverName)
 | 
			
		||||
		if _, err := os.Stat(serverDir); err != nil {
 | 
			
		||||
			return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// NOTE(p4u1): when the docker context does not exist but the server folder
 | 
			
		||||
		//             is there, let's create a new docker context.
 | 
			
		||||
		if err = CreateContext(serverName); err != nil {
 | 
			
		||||
			return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctx, err = GetContext(serverName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
 | 
			
		||||
 | 
			
		||||
@ -116,10 +116,7 @@ var (
 | 
			
		||||
 | 
			
		||||
	DIRTY_DEFAULT = "+U"
 | 
			
		||||
 | 
			
		||||
	NO_DOMAIN_DEFAULT  = "N/A"
 | 
			
		||||
	NO_VERSION_DEFAULT = "N/A"
 | 
			
		||||
	NO_SECRETS_DEFAULT = "N/A"
 | 
			
		||||
	NO_VOLUMES_DEFAULT = "N/A"
 | 
			
		||||
	MISSING_DEFAULT = "-"
 | 
			
		||||
 | 
			
		||||
	UNKNOWN_DEFAULT = "unknown"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ var (
 | 
			
		||||
	Locale        = DefaultLocale
 | 
			
		||||
	_, Mo         = LoadLocale()
 | 
			
		||||
	G             = Mo.Get
 | 
			
		||||
	GC            = Mo.GetC
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func LoadLocale() (string, *gotext.Mo) {
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -49,7 +49,7 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
 | 
			
		||||
	if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
 | 
			
		||||
		log.Debug(i18n.G("ensuring env version %s", r.EnvVersion))
 | 
			
		||||
		if strings.Contains(r.EnvVersion, "+U") {
 | 
			
		||||
			return errors.New(i18n.G("can not redeploy chaos version (%s) without --chaos", r.EnvVersion))
 | 
			
		||||
			return errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?`))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -379,7 +379,7 @@ func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
 | 
			
		||||
 | 
			
		||||
	if !offline {
 | 
			
		||||
		if err := catalogue.EnsureUpToDate(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
			return nil, fmt.Errorf("unable to update catalogue: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,11 @@ type Secret struct {
 | 
			
		||||
	// Will have this remote name:
 | 
			
		||||
	//   test_example_com_test_pass_two_v2
 | 
			
		||||
	RemoteName string
 | 
			
		||||
 | 
			
		||||
	// LocalName iis the name of the secret in the recipe config. This is also
 | 
			
		||||
	// the name that you pass to `abra app secret insert` and is shown on `abra
 | 
			
		||||
	// app secret list`
 | 
			
		||||
	LocalName string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GeneratePassword generates passwords.
 | 
			
		||||
@ -133,7 +138,12 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
 | 
			
		||||
 | 
			
		||||
		lastIdx := strings.LastIndex(secretConfig.Name, "_")
 | 
			
		||||
		secretVersion := secretConfig.Name[lastIdx+1:]
 | 
			
		||||
		value := Secret{Version: secretVersion, RemoteName: secretConfig.Name}
 | 
			
		||||
 | 
			
		||||
		value := Secret{
 | 
			
		||||
			Version:    secretVersion,
 | 
			
		||||
			RemoteName: secretConfig.Name,
 | 
			
		||||
			LocalName:  secretId,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(value.RemoteName) > config.MAX_DOCKER_SECRET_LENGTH {
 | 
			
		||||
			return nil, errors.New(i18n.G("secret %s is > %d chars when combined with %s", secretId, config.MAX_DOCKER_SECRET_LENGTH, stackName))
 | 
			
		||||
@ -178,6 +188,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
 | 
			
		||||
// resolveCharset sets the passgen Alphabet required for a secret
 | 
			
		||||
func resolveCharset(input string) string {
 | 
			
		||||
	switch strings.ToLower(input) {
 | 
			
		||||
	case "hex":
 | 
			
		||||
		return passgen.AlphabetNumericAmbiguous + "abcdef"
 | 
			
		||||
	case "special":
 | 
			
		||||
		return passgen.AlphabetSpecial
 | 
			
		||||
	case "safespecial":
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,12 @@ func TestReadSecretsConfig(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version)
 | 
			
		||||
	assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset)
 | 
			
		||||
 | 
			
		||||
	// Has a length modifier and a charset=hex modifier
 | 
			
		||||
	assert.Equal(t, "test_example_com_test_pass_seven_v1", secretsFromConfig["test_pass_seven"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v1", secretsFromConfig["test_pass_seven"].Version)
 | 
			
		||||
	assert.Equal(t, 32, secretsFromConfig["test_pass_seven"].Length)
 | 
			
		||||
	assert.Equal(t, "0123456789abcdef", secretsFromConfig["test_pass_seven"].Charset)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestReadSecretsConfigWithLongDomain(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
@ -4,3 +4,4 @@ SECRET_TEST_PASS_THREE_VERSION=v2
 | 
			
		||||
SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial
 | 
			
		||||
SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special
 | 
			
		||||
SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special
 | 
			
		||||
SECRET_TEST_PASS_SEVEN_VERSION=v1 # length=32 charset=hex
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ services:
 | 
			
		||||
      - test_pass_four
 | 
			
		||||
      - test_pass_five
 | 
			
		||||
      - test_pass_six
 | 
			
		||||
      - test_pass_seven
 | 
			
		||||
 | 
			
		||||
secrets:
 | 
			
		||||
  test_pass_one:
 | 
			
		||||
@ -31,3 +32,6 @@ secrets:
 | 
			
		||||
  test_pass_six:
 | 
			
		||||
    external: true
 | 
			
		||||
    name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}
 | 
			
		||||
  test_pass_seven:
 | 
			
		||||
    external: true
 | 
			
		||||
    name: ${STACK_NAME}_test_pass_seven_${SECRET_TEST_PASS_SEVEN_VERSION}
 | 
			
		||||
 | 
			
		||||
@ -247,7 +247,7 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if terminalStatesReached == len(tasks) {
 | 
			
		||||
		if terminalStatesReached >= len(tasks) {
 | 
			
		||||
			log.Debug(i18n.G("all tasks reached terminal state"))
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								scripts/cloud-init/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								scripts/cloud-init/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
# cloud-init
 | 
			
		||||
 | 
			
		||||
This folder contains cloud-init files for installing Abra and its dependencies.
 | 
			
		||||
 | 
			
		||||
For more information, see <https://cloudinit.readthedocs.io/en/latest/index.html>
 | 
			
		||||
							
								
								
									
										49
									
								
								scripts/cloud-init/cloud-init.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								scripts/cloud-init/cloud-init.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
#cloud-config
 | 
			
		||||
 | 
			
		||||
package_update: true
 | 
			
		||||
package_upgrade: true
 | 
			
		||||
package_reboot_if_required: true
 | 
			
		||||
 | 
			
		||||
# https://packages.debian.org/bookworm/docker.io
 | 
			
		||||
packages:
 | 
			
		||||
  - ca-certificates
 | 
			
		||||
  - curl
 | 
			
		||||
  - docker.io
 | 
			
		||||
  - docker-compose
 | 
			
		||||
  # https://stackoverflow.com/a/74084180
 | 
			
		||||
  - apparmor
 | 
			
		||||
 | 
			
		||||
# https://docs.coopcloud.tech/operators/tutorial/#server-setup
 | 
			
		||||
runcmd:
 | 
			
		||||
  - curl -fsSL https://install.abra.coopcloud.tech | env HOME=/root bash
 | 
			
		||||
  - docker swarm init
 | 
			
		||||
  - docker network create -d overlay proxy
 | 
			
		||||
 | 
			
		||||
write_files:
 | 
			
		||||
  # Add abra to PATH and set EDITOR
 | 
			
		||||
  - path: /etc/profile.d/custom_path.sh
 | 
			
		||||
    content: |
 | 
			
		||||
      export PATH=$PATH:$HOME/.local/bin
 | 
			
		||||
      export EDITOR=vim
 | 
			
		||||
    owner: root:root
 | 
			
		||||
    permissions: '0755'
 | 
			
		||||
  # Send container log to journald: https://docs.coopcloud.tech/operators/handbook/#how-do-i-persist-container-logs-after-they-go-away
 | 
			
		||||
  - path: /etc/docker/daemon.json
 | 
			
		||||
    content: |
 | 
			
		||||
      {
 | 
			
		||||
          "log-driver": "journald",
 | 
			
		||||
          "log-opts": {
 | 
			
		||||
            "labels":"com.docker.swarm.service.name"
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
    owner: root:root
 | 
			
		||||
    permissions: '0644'
 | 
			
		||||
  # Rotate logs
 | 
			
		||||
  - path: /etc/systemd/journald.conf
 | 
			
		||||
    content: |
 | 
			
		||||
      [Journal]
 | 
			
		||||
      Storage=persistent
 | 
			
		||||
      SystemMaxUse=5G
 | 
			
		||||
      MaxFileSec=1month
 | 
			
		||||
    owner: root:root
 | 
			
		||||
    permissions: '0644'
 | 
			
		||||
@ -51,7 +51,7 @@ echo "========================================================================"
 | 
			
		||||
echo "BUILDING ABRA"
 | 
			
		||||
echo "========================================================================"
 | 
			
		||||
export PATH="/usr/lib/go-1.21/bin:$PATH"
 | 
			
		||||
make build-abra
 | 
			
		||||
make build
 | 
			
		||||
echo "========================================================================"
 | 
			
		||||
 | 
			
		||||
echo "========================================================================"
 | 
			
		||||
 | 
			
		||||
@ -175,7 +175,7 @@ teardown(){
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "bail if env has a hash but no --chaos" {
 | 
			
		||||
@test "do not bail if env version is a hash but no --chaos" {
 | 
			
		||||
  wantHash=$(_get_n_hash 3)
 | 
			
		||||
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
 | 
			
		||||
@ -250,6 +250,7 @@ teardown(){
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" \
 | 
			
		||||
    --no-input --no-converge-checks --chaos
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp "NEW DEPLOYMENT.*${_get_head_hash:0:8}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@ -367,6 +368,21 @@ teardown(){
 | 
			
		||||
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial "secret not generated"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "error if secret not inserted" {
 | 
			
		||||
  run sed -i 's/COMPOSE_FILE="compose.yml"/COMPOSE_FILE="compose.yml:compose.skip_pass.yml"/g' \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run sed -i 's/#SECRET_TEST_SKIP_PASS_VERSION=v1/SECRET_TEST_SKIP_PASS_VERSION=v1/g' \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial "secret not inserted"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@ -561,3 +577,18 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
  refute_output --partial "IMAGES"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "manually created server without context bails gracefully" {
 | 
			
		||||
  run mkdir -p "$ABRA_DIR/servers/default2"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/servers/default2"
 | 
			
		||||
 | 
			
		||||
  run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN_2" --no-input --no-converge-checks
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial "server missing context"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -127,3 +127,14 @@ teardown(){
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "new env version written to container env" {
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
 | 
			
		||||
    $(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -38,8 +38,8 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'NEW DEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial 'CURRENT DEPLOYMENT    N/A'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           N/A'
 | 
			
		||||
  assert_output --partial 'CURRENT DEPLOYMENT    -'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           -'
 | 
			
		||||
  assert_output --partial "NEW DEPLOYMENT        ${latestRelease}"
 | 
			
		||||
  assert_output --partial "IMAGES                nginx: ${latestRelease##*+} (new)"
 | 
			
		||||
  assert_output --partial "CONFIGS               test_conf: v1 (new)"
 | 
			
		||||
@ -57,7 +57,7 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'NEW DEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    N/A"
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    -"
 | 
			
		||||
  assert_output --partial "ENV VERSION           ${latestRelease}"
 | 
			
		||||
  assert_output --partial "NEW DEPLOYMENT        ${latestRelease}"
 | 
			
		||||
  assert_output --partial "IMAGES                nginx: ${latestRelease##*+} (new)"
 | 
			
		||||
@ -102,7 +102,7 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'NEW DEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    N/A"
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    -"
 | 
			
		||||
  assert_output --partial "ENV VERSION           0.1.1+1.20.2"
 | 
			
		||||
  assert_output --partial "NEW DEPLOYMENT        0.1.1+1.20.2"
 | 
			
		||||
 | 
			
		||||
@ -125,7 +125,7 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'NEW DEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    N/A"
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    -"
 | 
			
		||||
  assert_output --partial "ENV VERSION           0.1.1+1.20.2"
 | 
			
		||||
  assert_output --partial "NEW DEPLOYMENT        ${latestRelease}"
 | 
			
		||||
 | 
			
		||||
@ -163,7 +163,7 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "can not redeploy chaos version without --chaos" {
 | 
			
		||||
@test "cannot redeploy chaos version without --chaos" {
 | 
			
		||||
  headHash=$(_get_head_hash)
 | 
			
		||||
  latestRelease=$(_latest_release)
 | 
			
		||||
 | 
			
		||||
@ -181,7 +181,7 @@ teardown(){
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" \
 | 
			
		||||
    --no-input --no-converge-checks --force --debug
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --regexp 'can not redeploy chaos version .*' + "${headHash:0:8}+U"
 | 
			
		||||
  assert_output --regexp 'cannot redeploy previous chaos version .*' + "${headHash:0:8}+U"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "deploy then force commit deploy" {
 | 
			
		||||
@ -219,7 +219,7 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'NEW DEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    N/A"
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    -"
 | 
			
		||||
  assert_output --partial "ENV VERSION           ${latestRelease}"
 | 
			
		||||
  assert_output --partial "NEW DEPLOYMENT        ${headHash:0:8}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,17 +28,17 @@ teardown(){
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "validate app argument" {
 | 
			
		||||
  run $ABRA app env
 | 
			
		||||
  run $ABRA app env list
 | 
			
		||||
  assert_failure
 | 
			
		||||
 | 
			
		||||
  run $ABRA app env DOESNTEXIST
 | 
			
		||||
  run $ABRA app env list DOESNTEXIST
 | 
			
		||||
  assert_failure
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "show env version" {
 | 
			
		||||
  latestRelease=$(_latest_release)
 | 
			
		||||
 | 
			
		||||
  run $ABRA app env "$TEST_APP_DOMAIN"
 | 
			
		||||
  run $ABRA app env list "$TEST_APP_DOMAIN"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "$latestRelease"
 | 
			
		||||
}
 | 
			
		||||
@ -48,7 +48,7 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app env "$TEST_APP_DOMAIN"
 | 
			
		||||
  run $ABRA app env list "$TEST_APP_DOMAIN"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
@ -57,3 +57,44 @@ teardown(){
 | 
			
		||||
  run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
  assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "app env pull explodes when no deployed app" { 
 | 
			
		||||
  run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
 | 
			
		||||
  assert_failure
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "app env pull recreates app env when missing" { 
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "app env pull recreates correct version" { 
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ teardown_file(){
 | 
			
		||||
  _undeploy_app
 | 
			
		||||
  _rm_app
 | 
			
		||||
  _rm_server
 | 
			
		||||
  _reset_recipe
 | 
			
		||||
 | 
			
		||||
  if [[ -d "$ABRA_DIR/servers/foo" ]]; then
 | 
			
		||||
    run rm -rf "$ABRA_DIR/servers/foo"
 | 
			
		||||
@ -173,7 +174,7 @@ teardown(){
 | 
			
		||||
 | 
			
		||||
  run $ABRA app ls --status
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "unknown server"
 | 
			
		||||
  assert_output --partial "server missing context"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@ -193,3 +194,16 @@ teardown(){
 | 
			
		||||
    <(jq -S "." <(echo '{}'))
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "list ignores borked tags" {
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
 | 
			
		||||
    -a "2.4.8_1" -m "feat: completely borked tag"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  _deploy_app
 | 
			
		||||
 | 
			
		||||
  run $ABRA app ls --status --debug
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "unable to parse 2.4.8_1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -22,8 +22,15 @@ teardown(){
 | 
			
		||||
  _reset_recipe
 | 
			
		||||
  _reset_tags
 | 
			
		||||
 | 
			
		||||
  run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
  assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
  if [[ -f "$ABRA_DIR/recipes/$TEST_RECIPE/foo" ]]; then
 | 
			
		||||
    run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
    assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
  if [[ -f "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env" ]]; then
 | 
			
		||||
    run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env"
 | 
			
		||||
    assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env"
 | 
			
		||||
  fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "create new app" {
 | 
			
		||||
@ -270,3 +277,32 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
  refute_output --partial "requires secret generation"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "do not warn about generation when generate=false" {
 | 
			
		||||
  run $ABRA app new --domain "$TEST_APP_DOMAIN" renovate "1.0.1+41-full"
 | 
			
		||||
  assert_success
 | 
			
		||||
  refute_output --partial "requires secret generation"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "warn about insertion when generate=false" {
 | 
			
		||||
  run $ABRA app new --domain "$TEST_APP_DOMAIN" renovate "1.0.1+41-full"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "requires secret insertion"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "warn about both insert/generate when generate=false/true" {
 | 
			
		||||
  run $ABRA app new rauthy "1.0.0+0.32.3" \
 | 
			
		||||
    --no-input \
 | 
			
		||||
    --server "$TEST_SERVER" \
 | 
			
		||||
    --domain "rauthy.$TEST_APP_DOMAIN"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_output --partial "requires secret generation"
 | 
			
		||||
  assert_output --partial "requires secret insertion"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "no warn about generation if already generated" {
 | 
			
		||||
  run $ABRA app new "$TEST_RECIPE" --domain "$TEST_APP_DOMAIN" --secrets
 | 
			
		||||
  assert_success
 | 
			
		||||
  refute_output --partial "requires secret generation"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -181,7 +181,7 @@ teardown(){
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "rollback chaos deployment is not possible" {
 | 
			
		||||
@test "rollback chaos deployment is possible" {
 | 
			
		||||
  tagHash=$(_get_tag_hash "0.2.0+1.21.0")
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
 | 
			
		||||
  assert_success
 | 
			
		||||
@ -191,12 +191,13 @@ teardown(){
 | 
			
		||||
  assert_output --partial "${tagHash:0:8}"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial 'current deployment' + "${tagHash:0:8}" + 'is not a known version'
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
 | 
			
		||||
  assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "chaos commit rollback not possible" {
 | 
			
		||||
@test "specific chaos commit rollback not possible" {
 | 
			
		||||
  _deploy_app
 | 
			
		||||
 | 
			
		||||
  tagHash=$(_get_tag_hash "0.2.0+1.21.0")
 | 
			
		||||
 | 
			
		||||
@ -33,10 +33,29 @@ teardown(){
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
 | 
			
		||||
    --no-input --no-converge-checks --debug
 | 
			
		||||
    --no-input --no-converge-checks
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "new env version written to container env" {
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
 | 
			
		||||
    --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
 | 
			
		||||
    $(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ teardown(){
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'DOWNGRADE OVERVIEW'
 | 
			
		||||
  assert_output --partial 'CURRENT DEPLOYMENT    0.2.0+1.21.0'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           N/A'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           -'
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        0.1.0+1.20.0'
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
 | 
			
		||||
 | 
			
		||||
@ -106,6 +106,7 @@ teardown(){
 | 
			
		||||
 | 
			
		||||
  run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp "CURRENT DEPLOYMENT.*${_get_head_hash:0:8}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ teardown(){
 | 
			
		||||
  assert_output --partial 'UNDEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial 'CURRENT DEPLOYMENT    0.1.0+1.20.0'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           0.1.0+1.20.0'
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        N/A'
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        -'
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
@ -57,7 +57,7 @@ teardown(){
 | 
			
		||||
  assert_output --partial 'UNDEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    ${headHash:0:8}"
 | 
			
		||||
  assert_output --partial "ENV VERSION           ${headHash:0:8}"
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        N/A'
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        -'
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
@ -81,7 +81,7 @@ teardown(){
 | 
			
		||||
  assert_output --partial 'UNDEPLOY OVERVIEW'
 | 
			
		||||
  assert_output --partial "CURRENT DEPLOYMENT    ${headHash:0:8}+U"
 | 
			
		||||
  assert_output --partial "ENV VERSION           ${headHash:0:8}+U"
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        N/A'
 | 
			
		||||
  assert_output --partial 'NEW DEPLOYMENT        -'
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
 | 
			
		||||
@ -256,7 +256,7 @@ teardown(){
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "upgrade commit deployment not possible" {
 | 
			
		||||
@test "specific version upgrade after chaos deploy" {
 | 
			
		||||
  tagHash=$(_get_tag_hash "0.1.0+1.20.0")
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
 | 
			
		||||
  assert_success
 | 
			
		||||
@ -266,20 +266,29 @@ teardown(){
 | 
			
		||||
  assert_output --partial "${tagHash:0:8}"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial "not a known version"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
 | 
			
		||||
  assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
 | 
			
		||||
  assert_output --regexp "NEW DEPLOYMENT.*0\.1\.1\+1\.20\.2"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "chaos commit upgrade not possible" {
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "upgrade to latest after chaos deploy" {
 | 
			
		||||
  latestRelease=$(_latest_release)
 | 
			
		||||
 | 
			
		||||
  tagHash=$(_get_tag_hash "0.1.0+1.20.0")
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial '0.1.0+1.20.0'
 | 
			
		||||
 | 
			
		||||
  tagHash=$(_get_tag_hash "0.2.0+1.21.0")
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --chaos
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "${tagHash:0:8}"
 | 
			
		||||
 | 
			
		||||
  run $ABRA app upgrade "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial "not a known version"
 | 
			
		||||
  run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
 | 
			
		||||
  assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
 | 
			
		||||
  assert_output --partial "${latestRelease}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
 | 
			
		||||
@ -40,3 +40,21 @@ teardown(){
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# bats test_tags=slow
 | 
			
		||||
@test "new env version written to container env" {
 | 
			
		||||
  run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
 | 
			
		||||
    "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
 | 
			
		||||
    $(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ teardown(){
 | 
			
		||||
 | 
			
		||||
  assert_output --partial 'UPGRADE OVERVIEW'
 | 
			
		||||
  assert_output --partial 'CURRENT DEPLOYMENT    0.2.0+1.21.0'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           N/A'
 | 
			
		||||
  assert_output --partial 'ENV VERSION           -'
 | 
			
		||||
  assert_output --partial "NEW DEPLOYMENT        $latestRelease"
 | 
			
		||||
 | 
			
		||||
  run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,8 @@ teardown(){
 | 
			
		||||
    --domain "foobar.$TEST_SERVER"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA app deploy "foobar.$TEST_SERVER" --no-input
 | 
			
		||||
  run $ABRA app deploy "foobar.$TEST_SERVER" \
 | 
			
		||||
    --no-input --no-converge-checks
 | 
			
		||||
  assert_success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -101,6 +101,9 @@ teardown() {
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
 | 
			
		||||
 | 
			
		||||
  run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial 'no -p/--publish passed, not publishing'
 | 
			
		||||
@ -119,6 +122,9 @@ teardown() {
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -m "added some release notes"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run $ABRA recipe release  "$TEST_RECIPE" --no-input  --minor
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --partial 'no -p/--publish passed, not publishing'
 | 
			
		||||
@ -127,3 +133,27 @@ teardown() {
 | 
			
		||||
  assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/0.4.0+1.21.0"
 | 
			
		||||
  assert_file_contains "$ABRA_DIR/recipes/$TEST_RECIPE/release/0.4.0+1.21.0" "those are some release notes for the next release"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@test "recipe release conflict fails" {
 | 
			
		||||
  tagHash=$(_get_tag_hash "0.2.0+1.21.0")
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run sed -i "s/nginx:1.21.0/nginx:1.29.1/g" "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp 'nginx:1.29.1'
 | 
			
		||||
 | 
			
		||||
  run sed -i "s/0.2.0+1.21.0/0.2.0+1.29.1/g" "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
 | 
			
		||||
  assert_success
 | 
			
		||||
 | 
			
		||||
  run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
 | 
			
		||||
  assert_success
 | 
			
		||||
  assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.2\.0\+1\.29\.1'
 | 
			
		||||
 | 
			
		||||
  run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
 | 
			
		||||
  assert_failure
 | 
			
		||||
  assert_output --partial '0.2.0+... conflicts with a previous release: 0.2.0+1.21.0'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,11 +12,7 @@ setup_suite(){
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
  if [[ ! -f "$PWD/abra" ]]; then
 | 
			
		||||
    make build-abra
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
  if [[ ! -f "$PWD/kadabra" ]]; then
 | 
			
		||||
    make build-kadabra
 | 
			
		||||
    make
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
  if [[ -d "$ABRA_DIR" ]]; then
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								vendor/github.com/atotto/clipboard/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								vendor/github.com/atotto/clipboard/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
language: go
 | 
			
		||||
 | 
			
		||||
os:
 | 
			
		||||
 - linux
 | 
			
		||||
 - osx
 | 
			
		||||
 - windows
 | 
			
		||||
 | 
			
		||||
go:
 | 
			
		||||
 - go1.13.x
 | 
			
		||||
 - go1.x
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
 - xvfb
 | 
			
		||||
 | 
			
		||||
before_install:
 | 
			
		||||
 - export DISPLAY=:99.0
 | 
			
		||||
 | 
			
		||||
script:
 | 
			
		||||
 - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xsel; fi
 | 
			
		||||
 - go test -v .
 | 
			
		||||
 - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xclip; fi
 | 
			
		||||
 - go test -v .
 | 
			
		||||
							
								
								
									
										4
									
								
								vendor/golang.org/x/sync/LICENSE → vendor/github.com/atotto/clipboard/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/golang.org/x/sync/LICENSE → vendor/github.com/atotto/clipboard/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
Copyright 2009 The Go Authors.
 | 
			
		||||
Copyright (c) 2013 Ato Araki. All rights reserved.
 | 
			
		||||
 | 
			
		||||
Redistribution and use in source and binary forms, with or without
 | 
			
		||||
modification, are permitted provided that the following conditions are
 | 
			
		||||
@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
 | 
			
		||||
copyright notice, this list of conditions and the following disclaimer
 | 
			
		||||
in the documentation and/or other materials provided with the
 | 
			
		||||
distribution.
 | 
			
		||||
   * Neither the name of Google LLC nor the names of its
 | 
			
		||||
   * Neither the name of @atotto. nor the names of its
 | 
			
		||||
contributors may be used to endorse or promote products derived from
 | 
			
		||||
this software without specific prior written permission.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								vendor/github.com/atotto/clipboard/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								vendor/github.com/atotto/clipboard/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
[](https://travis-ci.org/atotto/clipboard)
 | 
			
		||||
 | 
			
		||||
[](http://godoc.org/github.com/atotto/clipboard)
 | 
			
		||||
 | 
			
		||||
# Clipboard for Go
 | 
			
		||||
 | 
			
		||||
Provide copying and pasting to the Clipboard for Go.
 | 
			
		||||
 | 
			
		||||
Build:
 | 
			
		||||
 | 
			
		||||
    $ go get github.com/atotto/clipboard
 | 
			
		||||
 | 
			
		||||
Platforms:
 | 
			
		||||
 | 
			
		||||
* OSX
 | 
			
		||||
* Windows 7 (probably work on other Windows)
 | 
			
		||||
* Linux, Unix (requires 'xclip' or 'xsel' command to be installed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Document: 
 | 
			
		||||
 | 
			
		||||
* http://godoc.org/github.com/atotto/clipboard
 | 
			
		||||
 | 
			
		||||
Notes:
 | 
			
		||||
 | 
			
		||||
* Text string only
 | 
			
		||||
* UTF-8 text encoding only (no conversion)
 | 
			
		||||
 | 
			
		||||
TODO:
 | 
			
		||||
 | 
			
		||||
* Clipboard watcher(?)
 | 
			
		||||
 | 
			
		||||
## Commands:
 | 
			
		||||
 | 
			
		||||
paste shell command:
 | 
			
		||||
 | 
			
		||||
    $ go get github.com/atotto/clipboard/cmd/gopaste
 | 
			
		||||
    $ # example:
 | 
			
		||||
    $ gopaste > document.txt
 | 
			
		||||
 | 
			
		||||
copy shell command:
 | 
			
		||||
 | 
			
		||||
    $ go get github.com/atotto/clipboard/cmd/gocopy
 | 
			
		||||
    $ # example:
 | 
			
		||||
    $ cat document.txt | gocopy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								vendor/github.com/atotto/clipboard/clipboard.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								vendor/github.com/atotto/clipboard/clipboard.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
// Copyright 2013 @atotto. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
// Package clipboard read/write on clipboard
 | 
			
		||||
package clipboard
 | 
			
		||||
 | 
			
		||||
// ReadAll read string from clipboard
 | 
			
		||||
func ReadAll() (string, error) {
 | 
			
		||||
	return readAll()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteAll write string to clipboard
 | 
			
		||||
func WriteAll(text string) error {
 | 
			
		||||
	return writeAll(text)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Unsupported might be set true during clipboard init, to help callers decide
 | 
			
		||||
// whether or not to offer clipboard options.
 | 
			
		||||
var Unsupported bool
 | 
			
		||||
							
								
								
									
										52
									
								
								vendor/github.com/atotto/clipboard/clipboard_darwin.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								vendor/github.com/atotto/clipboard/clipboard_darwin.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
// Copyright 2013 @atotto. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
// +build darwin
 | 
			
		||||
 | 
			
		||||
package clipboard
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	pasteCmdArgs = "pbpaste"
 | 
			
		||||
	copyCmdArgs  = "pbcopy"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getPasteCommand() *exec.Cmd {
 | 
			
		||||
	return exec.Command(pasteCmdArgs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getCopyCommand() *exec.Cmd {
 | 
			
		||||
	return exec.Command(copyCmdArgs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readAll() (string, error) {
 | 
			
		||||
	pasteCmd := getPasteCommand()
 | 
			
		||||
	out, err := pasteCmd.Output()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return string(out), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeAll(text string) error {
 | 
			
		||||
	copyCmd := getCopyCommand()
 | 
			
		||||
	in, err := copyCmd.StdinPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := copyCmd.Start(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := in.Write([]byte(text)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := in.Close(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return copyCmd.Wait()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								vendor/github.com/atotto/clipboard/clipboard_plan9.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								vendor/github.com/atotto/clipboard/clipboard_plan9.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
// Copyright 2013 @atotto. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
// +build plan9
 | 
			
		||||
 | 
			
		||||
package clipboard
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func readAll() (string, error) {
 | 
			
		||||
	f, err := os.Open("/dev/snarf")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	str, err := ioutil.ReadAll(f)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	return string(str), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeAll(text string) error {
 | 
			
		||||
	f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
	
 | 
			
		||||
	_, err = f.Write([]byte(text))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										149
									
								
								vendor/github.com/atotto/clipboard/clipboard_unix.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								vendor/github.com/atotto/clipboard/clipboard_unix.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,149 @@
 | 
			
		||||
// Copyright 2013 @atotto. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
// +build freebsd linux netbsd openbsd solaris dragonfly
 | 
			
		||||
 | 
			
		||||
package clipboard
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	xsel               = "xsel"
 | 
			
		||||
	xclip              = "xclip"
 | 
			
		||||
	powershellExe      = "powershell.exe"
 | 
			
		||||
	clipExe            = "clip.exe"
 | 
			
		||||
	wlcopy             = "wl-copy"
 | 
			
		||||
	wlpaste            = "wl-paste"
 | 
			
		||||
	termuxClipboardGet = "termux-clipboard-get"
 | 
			
		||||
	termuxClipboardSet = "termux-clipboard-set"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	Primary bool
 | 
			
		||||
	trimDos bool
 | 
			
		||||
 | 
			
		||||
	pasteCmdArgs []string
 | 
			
		||||
	copyCmdArgs  []string
 | 
			
		||||
 | 
			
		||||
	xselPasteArgs = []string{xsel, "--output", "--clipboard"}
 | 
			
		||||
	xselCopyArgs  = []string{xsel, "--input", "--clipboard"}
 | 
			
		||||
 | 
			
		||||
	xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
 | 
			
		||||
	xclipCopyArgs  = []string{xclip, "-in", "-selection", "clipboard"}
 | 
			
		||||
 | 
			
		||||
	powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"}
 | 
			
		||||
	clipExeCopyArgs        = []string{clipExe}
 | 
			
		||||
 | 
			
		||||
	wlpasteArgs = []string{wlpaste, "--no-newline"}
 | 
			
		||||
	wlcopyArgs  = []string{wlcopy}
 | 
			
		||||
 | 
			
		||||
	termuxPasteArgs = []string{termuxClipboardGet}
 | 
			
		||||
	termuxCopyArgs  = []string{termuxClipboardSet}
 | 
			
		||||
 | 
			
		||||
	missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	if os.Getenv("WAYLAND_DISPLAY") != "" {
 | 
			
		||||
		pasteCmdArgs = wlpasteArgs
 | 
			
		||||
		copyCmdArgs = wlcopyArgs
 | 
			
		||||
 | 
			
		||||
		if _, err := exec.LookPath(wlcopy); err == nil {
 | 
			
		||||
			if _, err := exec.LookPath(wlpaste); err == nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pasteCmdArgs = xclipPasteArgs
 | 
			
		||||
	copyCmdArgs = xclipCopyArgs
 | 
			
		||||
 | 
			
		||||
	if _, err := exec.LookPath(xclip); err == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pasteCmdArgs = xselPasteArgs
 | 
			
		||||
	copyCmdArgs = xselCopyArgs
 | 
			
		||||
 | 
			
		||||
	if _, err := exec.LookPath(xsel); err == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pasteCmdArgs = termuxPasteArgs
 | 
			
		||||
	copyCmdArgs = termuxCopyArgs
 | 
			
		||||
 | 
			
		||||
	if _, err := exec.LookPath(termuxClipboardSet); err == nil {
 | 
			
		||||
		if _, err := exec.LookPath(termuxClipboardGet); err == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pasteCmdArgs = powershellExePasteArgs
 | 
			
		||||
	copyCmdArgs = clipExeCopyArgs
 | 
			
		||||
	trimDos = true
 | 
			
		||||
 | 
			
		||||
	if _, err := exec.LookPath(clipExe); err == nil {
 | 
			
		||||
		if _, err := exec.LookPath(powershellExe); err == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Unsupported = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPasteCommand() *exec.Cmd {
 | 
			
		||||
	if Primary {
 | 
			
		||||
		pasteCmdArgs = pasteCmdArgs[:1]
 | 
			
		||||
	}
 | 
			
		||||
	return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getCopyCommand() *exec.Cmd {
 | 
			
		||||
	if Primary {
 | 
			
		||||
		copyCmdArgs = copyCmdArgs[:1]
 | 
			
		||||
	}
 | 
			
		||||
	return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readAll() (string, error) {
 | 
			
		||||
	if Unsupported {
 | 
			
		||||
		return "", missingCommands
 | 
			
		||||
	}
 | 
			
		||||
	pasteCmd := getPasteCommand()
 | 
			
		||||
	out, err := pasteCmd.Output()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	result := string(out)
 | 
			
		||||
	if trimDos && len(result) > 1 {
 | 
			
		||||
		result = result[:len(result)-2]
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeAll(text string) error {
 | 
			
		||||
	if Unsupported {
 | 
			
		||||
		return missingCommands
 | 
			
		||||
	}
 | 
			
		||||
	copyCmd := getCopyCommand()
 | 
			
		||||
	in, err := copyCmd.StdinPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := copyCmd.Start(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := in.Write([]byte(text)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := in.Close(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return copyCmd.Wait()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										157
									
								
								vendor/github.com/atotto/clipboard/clipboard_windows.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								vendor/github.com/atotto/clipboard/clipboard_windows.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
			
		||||
// Copyright 2013 @atotto. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a BSD-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
// +build windows
 | 
			
		||||
 | 
			
		||||
package clipboard
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	cfUnicodetext = 13
 | 
			
		||||
	gmemMoveable  = 0x0002
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	user32                     = syscall.MustLoadDLL("user32")
 | 
			
		||||
	isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
 | 
			
		||||
	openClipboard              = user32.MustFindProc("OpenClipboard")
 | 
			
		||||
	closeClipboard             = user32.MustFindProc("CloseClipboard")
 | 
			
		||||
	emptyClipboard             = user32.MustFindProc("EmptyClipboard")
 | 
			
		||||
	getClipboardData           = user32.MustFindProc("GetClipboardData")
 | 
			
		||||
	setClipboardData           = user32.MustFindProc("SetClipboardData")
 | 
			
		||||
 | 
			
		||||
	kernel32     = syscall.NewLazyDLL("kernel32")
 | 
			
		||||
	globalAlloc  = kernel32.NewProc("GlobalAlloc")
 | 
			
		||||
	globalFree   = kernel32.NewProc("GlobalFree")
 | 
			
		||||
	globalLock   = kernel32.NewProc("GlobalLock")
 | 
			
		||||
	globalUnlock = kernel32.NewProc("GlobalUnlock")
 | 
			
		||||
	lstrcpy      = kernel32.NewProc("lstrcpyW")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
 | 
			
		||||
func waitOpenClipboard() error {
 | 
			
		||||
	started := time.Now()
 | 
			
		||||
	limit := started.Add(time.Second)
 | 
			
		||||
	var r uintptr
 | 
			
		||||
	var err error
 | 
			
		||||
	for time.Now().Before(limit) {
 | 
			
		||||
		r, _, err = openClipboard.Call(0)
 | 
			
		||||
		if r != 0 {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		time.Sleep(time.Millisecond)
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readAll() (string, error) {
 | 
			
		||||
	// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
 | 
			
		||||
	// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
 | 
			
		||||
	runtime.LockOSThread()
 | 
			
		||||
	defer runtime.UnlockOSThread()
 | 
			
		||||
	if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	err := waitOpenClipboard()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h, _, err := getClipboardData.Call(cfUnicodetext)
 | 
			
		||||
	if h == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	l, _, err := globalLock.Call(h)
 | 
			
		||||
	if l == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
 | 
			
		||||
 | 
			
		||||
	r, _, err := globalUnlock.Call(h)
 | 
			
		||||
	if r == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	closed, _, err := closeClipboard.Call()
 | 
			
		||||
	if closed == 0 {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return text, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeAll(text string) error {
 | 
			
		||||
	// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
 | 
			
		||||
	// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
 | 
			
		||||
	runtime.LockOSThread()
 | 
			
		||||
	defer runtime.UnlockOSThread()
 | 
			
		||||
 | 
			
		||||
	err := waitOpenClipboard()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, _, err := emptyClipboard.Call(0)
 | 
			
		||||
	if r == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := syscall.StringToUTF16(text)
 | 
			
		||||
 | 
			
		||||
	// "If the hMem parameter identifies a memory object, the object must have
 | 
			
		||||
	// been allocated using the function with the GMEM_MOVEABLE flag."
 | 
			
		||||
	h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
 | 
			
		||||
	if h == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if h != 0 {
 | 
			
		||||
			globalFree.Call(h)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	l, _, err := globalLock.Call(h)
 | 
			
		||||
	if l == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
 | 
			
		||||
	if r == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, _, err = globalUnlock.Call(h)
 | 
			
		||||
	if r == 0 {
 | 
			
		||||
		if err.(syscall.Errno) != 0 {
 | 
			
		||||
			_, _, _ = closeClipboard.Call()
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, _, err = setClipboardData.Call(cfUnicodetext, h)
 | 
			
		||||
	if r == 0 {
 | 
			
		||||
		_, _, _ = closeClipboard.Call()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	h = 0 // suppress deferred cleanup
 | 
			
		||||
	closed, _, err := closeClipboard.Call()
 | 
			
		||||
	if closed == 0 {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								vendor/github.com/charmbracelet/bubbles/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/github.com/charmbracelet/bubbles/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2020-2023 Charmbracelet, Inc
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
							
								
								
									
										219
									
								
								vendor/github.com/charmbracelet/bubbles/cursor/cursor.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								vendor/github.com/charmbracelet/bubbles/cursor/cursor.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,219 @@
 | 
			
		||||
// Package cursor provides cursor functionality for Bubble Tea applications.
 | 
			
		||||
package cursor
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const defaultBlinkSpeed = time.Millisecond * 530
 | 
			
		||||
 | 
			
		||||
// initialBlinkMsg initializes cursor blinking.
 | 
			
		||||
type initialBlinkMsg struct{}
 | 
			
		||||
 | 
			
		||||
// BlinkMsg signals that the cursor should blink. It contains metadata that
 | 
			
		||||
// allows us to tell if the blink message is the one we're expecting.
 | 
			
		||||
type BlinkMsg struct {
 | 
			
		||||
	id  int
 | 
			
		||||
	tag int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// blinkCanceled is sent when a blink operation is canceled.
 | 
			
		||||
type blinkCanceled struct{}
 | 
			
		||||
 | 
			
		||||
// blinkCtx manages cursor blinking.
 | 
			
		||||
type blinkCtx struct {
 | 
			
		||||
	ctx    context.Context
 | 
			
		||||
	cancel context.CancelFunc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mode describes the behavior of the cursor.
 | 
			
		||||
type Mode int
 | 
			
		||||
 | 
			
		||||
// Available cursor modes.
 | 
			
		||||
const (
 | 
			
		||||
	CursorBlink Mode = iota
 | 
			
		||||
	CursorStatic
 | 
			
		||||
	CursorHide
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// String returns the cursor mode in a human-readable format. This method is
 | 
			
		||||
// provisional and for informational purposes only.
 | 
			
		||||
func (c Mode) String() string {
 | 
			
		||||
	return [...]string{
 | 
			
		||||
		"blink",
 | 
			
		||||
		"static",
 | 
			
		||||
		"hidden",
 | 
			
		||||
	}[c]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Model is the Bubble Tea model for this cursor element.
 | 
			
		||||
type Model struct {
 | 
			
		||||
	BlinkSpeed time.Duration
 | 
			
		||||
	// Style for styling the cursor block.
 | 
			
		||||
	Style lipgloss.Style
 | 
			
		||||
	// TextStyle is the style used for the cursor when it is hidden (when blinking).
 | 
			
		||||
	// I.e. displaying normal text.
 | 
			
		||||
	TextStyle lipgloss.Style
 | 
			
		||||
 | 
			
		||||
	// char is the character under the cursor
 | 
			
		||||
	char string
 | 
			
		||||
	// The ID of this Model as it relates to other cursors
 | 
			
		||||
	id int
 | 
			
		||||
	// focus indicates whether the containing input is focused
 | 
			
		||||
	focus bool
 | 
			
		||||
	// Cursor Blink state.
 | 
			
		||||
	Blink bool
 | 
			
		||||
	// Used to manage cursor blink
 | 
			
		||||
	blinkCtx *blinkCtx
 | 
			
		||||
	// The ID of the blink message we're expecting to receive.
 | 
			
		||||
	blinkTag int
 | 
			
		||||
	// mode determines the behavior of the cursor
 | 
			
		||||
	mode Mode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new model with default settings.
 | 
			
		||||
func New() Model {
 | 
			
		||||
	return Model{
 | 
			
		||||
		BlinkSpeed: defaultBlinkSpeed,
 | 
			
		||||
 | 
			
		||||
		Blink: true,
 | 
			
		||||
		mode:  CursorBlink,
 | 
			
		||||
 | 
			
		||||
		blinkCtx: &blinkCtx{
 | 
			
		||||
			ctx: context.Background(),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update updates the cursor.
 | 
			
		||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 | 
			
		||||
	switch msg := msg.(type) {
 | 
			
		||||
	case initialBlinkMsg:
 | 
			
		||||
		// We accept all initialBlinkMsgs generated by the Blink command.
 | 
			
		||||
 | 
			
		||||
		if m.mode != CursorBlink || !m.focus {
 | 
			
		||||
			return m, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cmd := m.BlinkCmd()
 | 
			
		||||
		return m, cmd
 | 
			
		||||
 | 
			
		||||
	case tea.FocusMsg:
 | 
			
		||||
		return m, m.Focus()
 | 
			
		||||
 | 
			
		||||
	case tea.BlurMsg:
 | 
			
		||||
		m.Blur()
 | 
			
		||||
		return m, nil
 | 
			
		||||
 | 
			
		||||
	case BlinkMsg:
 | 
			
		||||
		// We're choosy about whether to accept blinkMsgs so that our cursor
 | 
			
		||||
		// only exactly when it should.
 | 
			
		||||
 | 
			
		||||
		// Is this model blink-able?
 | 
			
		||||
		if m.mode != CursorBlink || !m.focus {
 | 
			
		||||
			return m, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Were we expecting this blink message?
 | 
			
		||||
		if msg.id != m.id || msg.tag != m.blinkTag {
 | 
			
		||||
			return m, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var cmd tea.Cmd
 | 
			
		||||
		if m.mode == CursorBlink {
 | 
			
		||||
			m.Blink = !m.Blink
 | 
			
		||||
			cmd = m.BlinkCmd()
 | 
			
		||||
		}
 | 
			
		||||
		return m, cmd
 | 
			
		||||
 | 
			
		||||
	case blinkCanceled: // no-op
 | 
			
		||||
		return m, nil
 | 
			
		||||
	}
 | 
			
		||||
	return m, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mode returns the model's cursor mode. For available cursor modes, see
 | 
			
		||||
// type Mode.
 | 
			
		||||
func (m Model) Mode() Mode {
 | 
			
		||||
	return m.mode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetMode sets the model's cursor mode. This method returns a command.
 | 
			
		||||
//
 | 
			
		||||
// For available cursor modes, see type CursorMode.
 | 
			
		||||
func (m *Model) SetMode(mode Mode) tea.Cmd {
 | 
			
		||||
	// Adjust the mode value if it's value is out of range
 | 
			
		||||
	if mode < CursorBlink || mode > CursorHide {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	m.mode = mode
 | 
			
		||||
	m.Blink = m.mode == CursorHide || !m.focus
 | 
			
		||||
	if mode == CursorBlink {
 | 
			
		||||
		return Blink
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BlinkCmd is a command used to manage cursor blinking.
 | 
			
		||||
func (m *Model) BlinkCmd() tea.Cmd {
 | 
			
		||||
	if m.mode != CursorBlink {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
 | 
			
		||||
		m.blinkCtx.cancel()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
 | 
			
		||||
	m.blinkCtx.cancel = cancel
 | 
			
		||||
 | 
			
		||||
	m.blinkTag++
 | 
			
		||||
 | 
			
		||||
	return func() tea.Msg {
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		<-ctx.Done()
 | 
			
		||||
		if ctx.Err() == context.DeadlineExceeded {
 | 
			
		||||
			return BlinkMsg{id: m.id, tag: m.blinkTag}
 | 
			
		||||
		}
 | 
			
		||||
		return blinkCanceled{}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blink is a command used to initialize cursor blinking.
 | 
			
		||||
func Blink() tea.Msg {
 | 
			
		||||
	return initialBlinkMsg{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Focus focuses the cursor to allow it to blink if desired.
 | 
			
		||||
func (m *Model) Focus() tea.Cmd {
 | 
			
		||||
	m.focus = true
 | 
			
		||||
	m.Blink = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
 | 
			
		||||
 | 
			
		||||
	if m.mode == CursorBlink && m.focus {
 | 
			
		||||
		return m.BlinkCmd()
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blur blurs the cursor.
 | 
			
		||||
func (m *Model) Blur() {
 | 
			
		||||
	m.focus = false
 | 
			
		||||
	m.Blink = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetChar sets the character under the cursor.
 | 
			
		||||
func (m *Model) SetChar(char string) {
 | 
			
		||||
	m.char = char
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// View displays the cursor.
 | 
			
		||||
func (m Model) View() string {
 | 
			
		||||
	if m.Blink {
 | 
			
		||||
		return m.TextStyle.Inline(true).Render(m.char)
 | 
			
		||||
	}
 | 
			
		||||
	return m.Style.Inline(true).Reverse(true).Render(m.char)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								vendor/github.com/charmbracelet/bubbles/key/key.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								vendor/github.com/charmbracelet/bubbles/key/key.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
// Package key provides some types and functions for generating user-definable
 | 
			
		||||
// keymappings useful in Bubble Tea components. There are a few different ways
 | 
			
		||||
// you can define a keymapping with this package. Here's one example:
 | 
			
		||||
//
 | 
			
		||||
//	type KeyMap struct {
 | 
			
		||||
//	    Up key.Binding
 | 
			
		||||
//	    Down key.Binding
 | 
			
		||||
//	}
 | 
			
		||||
//
 | 
			
		||||
//	var DefaultKeyMap = KeyMap{
 | 
			
		||||
//	    Up: key.NewBinding(
 | 
			
		||||
//	        key.WithKeys("k", "up"),        // actual keybindings
 | 
			
		||||
//	        key.WithHelp("↑/k", "move up"), // corresponding help text
 | 
			
		||||
//	    ),
 | 
			
		||||
//	    Down: key.NewBinding(
 | 
			
		||||
//	        key.WithKeys("j", "down"),
 | 
			
		||||
//	        key.WithHelp("↓/j", "move down"),
 | 
			
		||||
//	    ),
 | 
			
		||||
//	}
 | 
			
		||||
//
 | 
			
		||||
//	func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
			
		||||
//	    switch msg := msg.(type) {
 | 
			
		||||
//	    case tea.KeyMsg:
 | 
			
		||||
//	        switch {
 | 
			
		||||
//	        case key.Matches(msg, DefaultKeyMap.Up):
 | 
			
		||||
//	            // The user pressed up
 | 
			
		||||
//	        case key.Matches(msg, DefaultKeyMap.Down):
 | 
			
		||||
//	            // The user pressed down
 | 
			
		||||
//	        }
 | 
			
		||||
//	    }
 | 
			
		||||
//
 | 
			
		||||
//	    // ...
 | 
			
		||||
//	}
 | 
			
		||||
//
 | 
			
		||||
// The help information, which is not used in the example above, can be used
 | 
			
		||||
// to render help text for keystrokes in your views.
 | 
			
		||||
package key
 | 
			
		||||
 | 
			
		||||
import "fmt"
 | 
			
		||||
 | 
			
		||||
// Binding describes a set of keybindings and, optionally, their associated
 | 
			
		||||
// help text.
 | 
			
		||||
type Binding struct {
 | 
			
		||||
	keys     []string
 | 
			
		||||
	help     Help
 | 
			
		||||
	disabled bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindingOpt is an initialization option for a keybinding. It's used as an
 | 
			
		||||
// argument to NewBinding.
 | 
			
		||||
type BindingOpt func(*Binding)
 | 
			
		||||
 | 
			
		||||
// NewBinding returns a new keybinding from a set of BindingOpt options.
 | 
			
		||||
func NewBinding(opts ...BindingOpt) Binding {
 | 
			
		||||
	b := &Binding{}
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt(b)
 | 
			
		||||
	}
 | 
			
		||||
	return *b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithKeys initializes a keybinding with the given keystrokes.
 | 
			
		||||
func WithKeys(keys ...string) BindingOpt {
 | 
			
		||||
	return func(b *Binding) {
 | 
			
		||||
		b.keys = keys
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithHelp initializes a keybinding with the given help text.
 | 
			
		||||
func WithHelp(key, desc string) BindingOpt {
 | 
			
		||||
	return func(b *Binding) {
 | 
			
		||||
		b.help = Help{Key: key, Desc: desc}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithDisabled initializes a disabled keybinding.
 | 
			
		||||
func WithDisabled() BindingOpt {
 | 
			
		||||
	return func(b *Binding) {
 | 
			
		||||
		b.disabled = true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetKeys sets the keys for the keybinding.
 | 
			
		||||
func (b *Binding) SetKeys(keys ...string) {
 | 
			
		||||
	b.keys = keys
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Keys returns the keys for the keybinding.
 | 
			
		||||
func (b Binding) Keys() []string {
 | 
			
		||||
	return b.keys
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetHelp sets the help text for the keybinding.
 | 
			
		||||
func (b *Binding) SetHelp(key, desc string) {
 | 
			
		||||
	b.help = Help{Key: key, Desc: desc}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Help returns the Help information for the keybinding.
 | 
			
		||||
func (b Binding) Help() Help {
 | 
			
		||||
	return b.help
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Enabled returns whether or not the keybinding is enabled. Disabled
 | 
			
		||||
// keybindings won't be activated and won't show up in help. Keybindings are
 | 
			
		||||
// enabled by default.
 | 
			
		||||
func (b Binding) Enabled() bool {
 | 
			
		||||
	return !b.disabled && b.keys != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetEnabled enables or disables the keybinding.
 | 
			
		||||
func (b *Binding) SetEnabled(v bool) {
 | 
			
		||||
	b.disabled = !v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Unbind removes the keys and help from this binding, effectively nullifying
 | 
			
		||||
// it. This is a step beyond disabling it, since applications can enable
 | 
			
		||||
// or disable key bindings based on application state.
 | 
			
		||||
func (b *Binding) Unbind() {
 | 
			
		||||
	b.keys = nil
 | 
			
		||||
	b.help = Help{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Help is help information for a given keybinding.
 | 
			
		||||
type Help struct {
 | 
			
		||||
	Key  string
 | 
			
		||||
	Desc string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Matches checks if the given key matches the given bindings.
 | 
			
		||||
func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {
 | 
			
		||||
	keys := k.String()
 | 
			
		||||
	for _, binding := range b {
 | 
			
		||||
		for _, v := range binding.keys {
 | 
			
		||||
			if keys == v && binding.Enabled() {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
			
		||||
// Package runeutil provides a utility function for use in Bubbles
 | 
			
		||||
// that can process Key messages containing runes.
 | 
			
		||||
package runeutil
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"unicode"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Sanitizer is a helper for bubble widgets that want to process
 | 
			
		||||
// Runes from input key messages.
 | 
			
		||||
type Sanitizer interface {
 | 
			
		||||
	// Sanitize removes control characters from runes in a KeyRunes
 | 
			
		||||
	// message, and optionally replaces newline/carriage return/tabs by a
 | 
			
		||||
	// specified character.
 | 
			
		||||
	//
 | 
			
		||||
	// The rune array is modified in-place if possible. In that case, the
 | 
			
		||||
	// returned slice is the original slice shortened after the control
 | 
			
		||||
	// characters have been removed/translated.
 | 
			
		||||
	Sanitize(runes []rune) []rune
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewSanitizer constructs a rune sanitizer.
 | 
			
		||||
func NewSanitizer(opts ...Option) Sanitizer {
 | 
			
		||||
	s := sanitizer{
 | 
			
		||||
		replaceNewLine: []rune("\n"),
 | 
			
		||||
		replaceTab:     []rune("    "),
 | 
			
		||||
	}
 | 
			
		||||
	for _, o := range opts {
 | 
			
		||||
		s = o(s)
 | 
			
		||||
	}
 | 
			
		||||
	return &s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Option is the type of option that can be passed to Sanitize().
 | 
			
		||||
type Option func(sanitizer) sanitizer
 | 
			
		||||
 | 
			
		||||
// ReplaceTabs replaces tabs by the specified string.
 | 
			
		||||
func ReplaceTabs(tabRepl string) Option {
 | 
			
		||||
	return func(s sanitizer) sanitizer {
 | 
			
		||||
		s.replaceTab = []rune(tabRepl)
 | 
			
		||||
		return s
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReplaceNewlines replaces newline characters by the specified string.
 | 
			
		||||
func ReplaceNewlines(nlRepl string) Option {
 | 
			
		||||
	return func(s sanitizer) sanitizer {
 | 
			
		||||
		s.replaceNewLine = []rune(nlRepl)
 | 
			
		||||
		return s
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *sanitizer) Sanitize(runes []rune) []rune {
 | 
			
		||||
	// dstrunes are where we are storing the result.
 | 
			
		||||
	dstrunes := runes[:0:len(runes)]
 | 
			
		||||
	// copied indicates whether dstrunes is an alias of runes
 | 
			
		||||
	// or a copy. We need a copy when dst moves past src.
 | 
			
		||||
	// We use this as an optimization to avoid allocating
 | 
			
		||||
	// a new rune slice in the common case where the output
 | 
			
		||||
	// is smaller or equal to the input.
 | 
			
		||||
	copied := false
 | 
			
		||||
 | 
			
		||||
	for src := 0; src < len(runes); src++ {
 | 
			
		||||
		r := runes[src]
 | 
			
		||||
		switch {
 | 
			
		||||
		case r == utf8.RuneError:
 | 
			
		||||
			// skip
 | 
			
		||||
 | 
			
		||||
		case r == '\r' || r == '\n':
 | 
			
		||||
			if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
 | 
			
		||||
				dst := len(dstrunes)
 | 
			
		||||
				dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
 | 
			
		||||
				copy(dstrunes, runes[:dst])
 | 
			
		||||
				copied = true
 | 
			
		||||
			}
 | 
			
		||||
			dstrunes = append(dstrunes, s.replaceNewLine...)
 | 
			
		||||
 | 
			
		||||
		case r == '\t':
 | 
			
		||||
			if len(dstrunes)+len(s.replaceTab) > src && !copied {
 | 
			
		||||
				dst := len(dstrunes)
 | 
			
		||||
				dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
 | 
			
		||||
				copy(dstrunes, runes[:dst])
 | 
			
		||||
				copied = true
 | 
			
		||||
			}
 | 
			
		||||
			dstrunes = append(dstrunes, s.replaceTab...)
 | 
			
		||||
 | 
			
		||||
		case unicode.IsControl(r):
 | 
			
		||||
			// Other control characters: skip.
 | 
			
		||||
 | 
			
		||||
		default:
 | 
			
		||||
			// Keep the character.
 | 
			
		||||
			dstrunes = append(dstrunes, runes[src])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return dstrunes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type sanitizer struct {
 | 
			
		||||
	replaceNewLine []rune
 | 
			
		||||
	replaceTab     []rune
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										224
									
								
								vendor/github.com/charmbracelet/bubbles/spinner/spinner.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								vendor/github.com/charmbracelet/bubbles/spinner/spinner.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,224 @@
 | 
			
		||||
// Package spinner provides a spinner component for Bubble Tea applications.
 | 
			
		||||
package spinner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Internal ID management. Used during animating to ensure that frame messages
 | 
			
		||||
// are received only by spinner components that sent them.
 | 
			
		||||
var lastID int64
 | 
			
		||||
 | 
			
		||||
func nextID() int {
 | 
			
		||||
	return int(atomic.AddInt64(&lastID, 1))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Spinner is a set of frames used in animating the spinner.
 | 
			
		||||
type Spinner struct {
 | 
			
		||||
	Frames []string
 | 
			
		||||
	FPS    time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Some spinners to choose from. You could also make your own.
 | 
			
		||||
var (
 | 
			
		||||
	Line = Spinner{
 | 
			
		||||
		Frames: []string{"|", "/", "-", "\\"},
 | 
			
		||||
		FPS:    time.Second / 10, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Dot = Spinner{
 | 
			
		||||
		Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
 | 
			
		||||
		FPS:    time.Second / 10, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	MiniDot = Spinner{
 | 
			
		||||
		Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
 | 
			
		||||
		FPS:    time.Second / 12, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Jump = Spinner{
 | 
			
		||||
		Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
 | 
			
		||||
		FPS:    time.Second / 10, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Pulse = Spinner{
 | 
			
		||||
		Frames: []string{"█", "▓", "▒", "░"},
 | 
			
		||||
		FPS:    time.Second / 8, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Points = Spinner{
 | 
			
		||||
		Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
 | 
			
		||||
		FPS:    time.Second / 7, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Globe = Spinner{
 | 
			
		||||
		Frames: []string{"🌍", "🌎", "🌏"},
 | 
			
		||||
		FPS:    time.Second / 4, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Moon = Spinner{
 | 
			
		||||
		Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
 | 
			
		||||
		FPS:    time.Second / 8, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Monkey = Spinner{
 | 
			
		||||
		Frames: []string{"🙈", "🙉", "🙊"},
 | 
			
		||||
		FPS:    time.Second / 3, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Meter = Spinner{
 | 
			
		||||
		Frames: []string{
 | 
			
		||||
			"▱▱▱",
 | 
			
		||||
			"▰▱▱",
 | 
			
		||||
			"▰▰▱",
 | 
			
		||||
			"▰▰▰",
 | 
			
		||||
			"▰▰▱",
 | 
			
		||||
			"▰▱▱",
 | 
			
		||||
			"▱▱▱",
 | 
			
		||||
		},
 | 
			
		||||
		FPS: time.Second / 7, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Hamburger = Spinner{
 | 
			
		||||
		Frames: []string{"☱", "☲", "☴", "☲"},
 | 
			
		||||
		FPS:    time.Second / 3, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
	Ellipsis = Spinner{
 | 
			
		||||
		Frames: []string{"", ".", "..", "..."},
 | 
			
		||||
		FPS:    time.Second / 3, //nolint:mnd
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Model contains the state for the spinner. Use New to create new models
 | 
			
		||||
// rather than using Model as a struct literal.
 | 
			
		||||
type Model struct {
 | 
			
		||||
	// Spinner settings to use. See type Spinner.
 | 
			
		||||
	Spinner Spinner
 | 
			
		||||
 | 
			
		||||
	// Style sets the styling for the spinner. Most of the time you'll just
 | 
			
		||||
	// want foreground and background coloring, and potentially some padding.
 | 
			
		||||
	//
 | 
			
		||||
	// For an introduction to styling with Lip Gloss see:
 | 
			
		||||
	// https://github.com/charmbracelet/lipgloss
 | 
			
		||||
	Style lipgloss.Style
 | 
			
		||||
 | 
			
		||||
	frame int
 | 
			
		||||
	id    int
 | 
			
		||||
	tag   int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ID returns the spinner's unique ID.
 | 
			
		||||
func (m Model) ID() int {
 | 
			
		||||
	return m.id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New returns a model with default values.
 | 
			
		||||
func New(opts ...Option) Model {
 | 
			
		||||
	m := Model{
 | 
			
		||||
		Spinner: Line,
 | 
			
		||||
		id:      nextID(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt(&m)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewModel returns a model with default values.
 | 
			
		||||
//
 | 
			
		||||
// Deprecated: use [New] instead.
 | 
			
		||||
var NewModel = New
 | 
			
		||||
 | 
			
		||||
// TickMsg indicates that the timer has ticked and we should render a frame.
 | 
			
		||||
type TickMsg struct {
 | 
			
		||||
	Time time.Time
 | 
			
		||||
	tag  int
 | 
			
		||||
	ID   int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update is the Tea update function.
 | 
			
		||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 | 
			
		||||
	switch msg := msg.(type) {
 | 
			
		||||
	case TickMsg:
 | 
			
		||||
		// If an ID is set, and the ID doesn't belong to this spinner, reject
 | 
			
		||||
		// the message.
 | 
			
		||||
		if msg.ID > 0 && msg.ID != m.id {
 | 
			
		||||
			return m, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If a tag is set, and it's not the one we expect, reject the message.
 | 
			
		||||
		// This prevents the spinner from receiving too many messages and
 | 
			
		||||
		// thus spinning too fast.
 | 
			
		||||
		if msg.tag > 0 && msg.tag != m.tag {
 | 
			
		||||
			return m, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m.frame++
 | 
			
		||||
		if m.frame >= len(m.Spinner.Frames) {
 | 
			
		||||
			m.frame = 0
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m.tag++
 | 
			
		||||
		return m, m.tick(m.id, m.tag)
 | 
			
		||||
	default:
 | 
			
		||||
		return m, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// View renders the model's view.
 | 
			
		||||
func (m Model) View() string {
 | 
			
		||||
	if m.frame >= len(m.Spinner.Frames) {
 | 
			
		||||
		return "(error)"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m.Style.Render(m.Spinner.Frames[m.frame])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tick is the command used to advance the spinner one frame. Use this command
 | 
			
		||||
// to effectively start the spinner.
 | 
			
		||||
func (m Model) Tick() tea.Msg {
 | 
			
		||||
	return TickMsg{
 | 
			
		||||
		// The time at which the tick occurred.
 | 
			
		||||
		Time: time.Now(),
 | 
			
		||||
 | 
			
		||||
		// The ID of the spinner that this message belongs to. This can be
 | 
			
		||||
		// helpful when routing messages, however bear in mind that spinners
 | 
			
		||||
		// will ignore messages that don't contain ID by default.
 | 
			
		||||
		ID: m.id,
 | 
			
		||||
 | 
			
		||||
		tag: m.tag,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Model) tick(id, tag int) tea.Cmd {
 | 
			
		||||
	return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
 | 
			
		||||
		return TickMsg{
 | 
			
		||||
			Time: t,
 | 
			
		||||
			ID:   id,
 | 
			
		||||
			tag:  tag,
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tick is the command used to advance the spinner one frame. Use this command
 | 
			
		||||
// to effectively start the spinner.
 | 
			
		||||
//
 | 
			
		||||
// Deprecated: Use [Model.Tick] instead.
 | 
			
		||||
func Tick() tea.Msg {
 | 
			
		||||
	return TickMsg{Time: time.Now()}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Option is used to set options in New. For example:
 | 
			
		||||
//
 | 
			
		||||
//	spinner := New(WithSpinner(Dot))
 | 
			
		||||
type Option func(*Model)
 | 
			
		||||
 | 
			
		||||
// WithSpinner is an option to set the spinner.
 | 
			
		||||
func WithSpinner(spinner Spinner) Option {
 | 
			
		||||
	return func(m *Model) {
 | 
			
		||||
		m.Spinner = spinner
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithStyle is an option to set the spinner style.
 | 
			
		||||
func WithStyle(style lipgloss.Style) Option {
 | 
			
		||||
	return func(m *Model) {
 | 
			
		||||
		m.Style = style
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										898
									
								
								vendor/github.com/charmbracelet/bubbles/textinput/textinput.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										898
									
								
								vendor/github.com/charmbracelet/bubbles/textinput/textinput.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,898 @@
 | 
			
		||||
// Package textinput provides a text input component for Bubble Tea
 | 
			
		||||
// applications.
 | 
			
		||||
package textinput
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode"
 | 
			
		||||
 | 
			
		||||
	"github.com/atotto/clipboard"
 | 
			
		||||
	"github.com/charmbracelet/bubbles/cursor"
 | 
			
		||||
	"github.com/charmbracelet/bubbles/key"
 | 
			
		||||
	"github.com/charmbracelet/bubbles/runeutil"
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
	rw "github.com/mattn/go-runewidth"
 | 
			
		||||
	"github.com/rivo/uniseg"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Internal messages for clipboard operations.
 | 
			
		||||
type (
 | 
			
		||||
	pasteMsg    string
 | 
			
		||||
	pasteErrMsg struct{ error }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EchoMode sets the input behavior of the text input field.
 | 
			
		||||
type EchoMode int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// EchoNormal displays text as is. This is the default behavior.
 | 
			
		||||
	EchoNormal EchoMode = iota
 | 
			
		||||
 | 
			
		||||
	// EchoPassword displays the EchoCharacter mask instead of actual
 | 
			
		||||
	// characters. This is commonly used for password fields.
 | 
			
		||||
	EchoPassword
 | 
			
		||||
 | 
			
		||||
	// EchoNone displays nothing as characters are entered. This is commonly
 | 
			
		||||
	// seen for password fields on the command line.
 | 
			
		||||
	EchoNone
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ValidateFunc is a function that returns an error if the input is invalid.
 | 
			
		||||
type ValidateFunc func(string) error
 | 
			
		||||
 | 
			
		||||
// KeyMap is the key bindings for different actions within the textinput.
 | 
			
		||||
type KeyMap struct {
 | 
			
		||||
	CharacterForward        key.Binding
 | 
			
		||||
	CharacterBackward       key.Binding
 | 
			
		||||
	WordForward             key.Binding
 | 
			
		||||
	WordBackward            key.Binding
 | 
			
		||||
	DeleteWordBackward      key.Binding
 | 
			
		||||
	DeleteWordForward       key.Binding
 | 
			
		||||
	DeleteAfterCursor       key.Binding
 | 
			
		||||
	DeleteBeforeCursor      key.Binding
 | 
			
		||||
	DeleteCharacterBackward key.Binding
 | 
			
		||||
	DeleteCharacterForward  key.Binding
 | 
			
		||||
	LineStart               key.Binding
 | 
			
		||||
	LineEnd                 key.Binding
 | 
			
		||||
	Paste                   key.Binding
 | 
			
		||||
	AcceptSuggestion        key.Binding
 | 
			
		||||
	NextSuggestion          key.Binding
 | 
			
		||||
	PrevSuggestion          key.Binding
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DefaultKeyMap is the default set of key bindings for navigating and acting
 | 
			
		||||
// upon the textinput.
 | 
			
		||||
var DefaultKeyMap = KeyMap{
 | 
			
		||||
	CharacterForward:        key.NewBinding(key.WithKeys("right", "ctrl+f")),
 | 
			
		||||
	CharacterBackward:       key.NewBinding(key.WithKeys("left", "ctrl+b")),
 | 
			
		||||
	WordForward:             key.NewBinding(key.WithKeys("alt+right", "ctrl+right", "alt+f")),
 | 
			
		||||
	WordBackward:            key.NewBinding(key.WithKeys("alt+left", "ctrl+left", "alt+b")),
 | 
			
		||||
	DeleteWordBackward:      key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")),
 | 
			
		||||
	DeleteWordForward:       key.NewBinding(key.WithKeys("alt+delete", "alt+d")),
 | 
			
		||||
	DeleteAfterCursor:       key.NewBinding(key.WithKeys("ctrl+k")),
 | 
			
		||||
	DeleteBeforeCursor:      key.NewBinding(key.WithKeys("ctrl+u")),
 | 
			
		||||
	DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")),
 | 
			
		||||
	DeleteCharacterForward:  key.NewBinding(key.WithKeys("delete", "ctrl+d")),
 | 
			
		||||
	LineStart:               key.NewBinding(key.WithKeys("home", "ctrl+a")),
 | 
			
		||||
	LineEnd:                 key.NewBinding(key.WithKeys("end", "ctrl+e")),
 | 
			
		||||
	Paste:                   key.NewBinding(key.WithKeys("ctrl+v")),
 | 
			
		||||
	AcceptSuggestion:        key.NewBinding(key.WithKeys("tab")),
 | 
			
		||||
	NextSuggestion:          key.NewBinding(key.WithKeys("down", "ctrl+n")),
 | 
			
		||||
	PrevSuggestion:          key.NewBinding(key.WithKeys("up", "ctrl+p")),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Model is the Bubble Tea model for this text input element.
 | 
			
		||||
type Model struct {
 | 
			
		||||
	Err error
 | 
			
		||||
 | 
			
		||||
	// General settings.
 | 
			
		||||
	Prompt        string
 | 
			
		||||
	Placeholder   string
 | 
			
		||||
	EchoMode      EchoMode
 | 
			
		||||
	EchoCharacter rune
 | 
			
		||||
	Cursor        cursor.Model
 | 
			
		||||
 | 
			
		||||
	// Deprecated: use [cursor.BlinkSpeed] instead.
 | 
			
		||||
	BlinkSpeed time.Duration
 | 
			
		||||
 | 
			
		||||
	// Styles. These will be applied as inline styles.
 | 
			
		||||
	//
 | 
			
		||||
	// For an introduction to styling with Lip Gloss see:
 | 
			
		||||
	// https://github.com/charmbracelet/lipgloss
 | 
			
		||||
	PromptStyle      lipgloss.Style
 | 
			
		||||
	TextStyle        lipgloss.Style
 | 
			
		||||
	PlaceholderStyle lipgloss.Style
 | 
			
		||||
	CompletionStyle  lipgloss.Style
 | 
			
		||||
 | 
			
		||||
	// Deprecated: use Cursor.Style instead.
 | 
			
		||||
	CursorStyle lipgloss.Style
 | 
			
		||||
 | 
			
		||||
	// CharLimit is the maximum amount of characters this input element will
 | 
			
		||||
	// accept. If 0 or less, there's no limit.
 | 
			
		||||
	CharLimit int
 | 
			
		||||
 | 
			
		||||
	// Width is the maximum number of characters that can be displayed at once.
 | 
			
		||||
	// It essentially treats the text field like a horizontally scrolling
 | 
			
		||||
	// viewport. If 0 or less this setting is ignored.
 | 
			
		||||
	Width int
 | 
			
		||||
 | 
			
		||||
	// KeyMap encodes the keybindings recognized by the widget.
 | 
			
		||||
	KeyMap KeyMap
 | 
			
		||||
 | 
			
		||||
	// Underlying text value.
 | 
			
		||||
	value []rune
 | 
			
		||||
 | 
			
		||||
	// focus indicates whether user input focus should be on this input
 | 
			
		||||
	// component. When false, ignore keyboard input and hide the cursor.
 | 
			
		||||
	focus bool
 | 
			
		||||
 | 
			
		||||
	// Cursor position.
 | 
			
		||||
	pos int
 | 
			
		||||
 | 
			
		||||
	// Used to emulate a viewport when width is set and the content is
 | 
			
		||||
	// overflowing.
 | 
			
		||||
	offset      int
 | 
			
		||||
	offsetRight int
 | 
			
		||||
 | 
			
		||||
	// Validate is a function that checks whether or not the text within the
 | 
			
		||||
	// input is valid. If it is not valid, the `Err` field will be set to the
 | 
			
		||||
	// error returned by the function. If the function is not defined, all
 | 
			
		||||
	// input is considered valid.
 | 
			
		||||
	Validate ValidateFunc
 | 
			
		||||
 | 
			
		||||
	// rune sanitizer for input.
 | 
			
		||||
	rsan runeutil.Sanitizer
 | 
			
		||||
 | 
			
		||||
	// Should the input suggest to complete
 | 
			
		||||
	ShowSuggestions bool
 | 
			
		||||
 | 
			
		||||
	// suggestions is a list of suggestions that may be used to complete the
 | 
			
		||||
	// input.
 | 
			
		||||
	suggestions            [][]rune
 | 
			
		||||
	matchedSuggestions     [][]rune
 | 
			
		||||
	currentSuggestionIndex int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new model with default settings.
 | 
			
		||||
func New() Model {
 | 
			
		||||
	return Model{
 | 
			
		||||
		Prompt:           "> ",
 | 
			
		||||
		EchoCharacter:    '*',
 | 
			
		||||
		CharLimit:        0,
 | 
			
		||||
		PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
 | 
			
		||||
		ShowSuggestions:  false,
 | 
			
		||||
		CompletionStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
 | 
			
		||||
		Cursor:           cursor.New(),
 | 
			
		||||
		KeyMap:           DefaultKeyMap,
 | 
			
		||||
 | 
			
		||||
		suggestions: [][]rune{},
 | 
			
		||||
		value:       nil,
 | 
			
		||||
		focus:       false,
 | 
			
		||||
		pos:         0,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewModel creates a new model with default settings.
 | 
			
		||||
//
 | 
			
		||||
// Deprecated: Use [New] instead.
 | 
			
		||||
var NewModel = New
 | 
			
		||||
 | 
			
		||||
// SetValue sets the value of the text input.
 | 
			
		||||
func (m *Model) SetValue(s string) {
 | 
			
		||||
	// Clean up any special characters in the input provided by the
 | 
			
		||||
	// caller. This avoids bugs due to e.g. tab characters and whatnot.
 | 
			
		||||
	runes := m.san().Sanitize([]rune(s))
 | 
			
		||||
	err := m.validate(runes)
 | 
			
		||||
	m.setValueInternal(runes, err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Model) setValueInternal(runes []rune, err error) {
 | 
			
		||||
	m.Err = err
 | 
			
		||||
 | 
			
		||||
	empty := len(m.value) == 0
 | 
			
		||||
 | 
			
		||||
	if m.CharLimit > 0 && len(runes) > m.CharLimit {
 | 
			
		||||
		m.value = runes[:m.CharLimit]
 | 
			
		||||
	} else {
 | 
			
		||||
		m.value = runes
 | 
			
		||||
	}
 | 
			
		||||
	if (m.pos == 0 && empty) || m.pos > len(m.value) {
 | 
			
		||||
		m.SetCursor(len(m.value))
 | 
			
		||||
	}
 | 
			
		||||
	m.handleOverflow()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Value returns the value of the text input.
 | 
			
		||||
func (m Model) Value() string {
 | 
			
		||||
	return string(m.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Position returns the cursor position.
 | 
			
		||||
func (m Model) Position() int {
 | 
			
		||||
	return m.pos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetCursor moves the cursor to the given position. If the position is
 | 
			
		||||
// out of bounds the cursor will be moved to the start or end accordingly.
 | 
			
		||||
func (m *Model) SetCursor(pos int) {
 | 
			
		||||
	m.pos = clamp(pos, 0, len(m.value))
 | 
			
		||||
	m.handleOverflow()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CursorStart moves the cursor to the start of the input field.
 | 
			
		||||
func (m *Model) CursorStart() {
 | 
			
		||||
	m.SetCursor(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CursorEnd moves the cursor to the end of the input field.
 | 
			
		||||
func (m *Model) CursorEnd() {
 | 
			
		||||
	m.SetCursor(len(m.value))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Focused returns the focus state on the model.
 | 
			
		||||
func (m Model) Focused() bool {
 | 
			
		||||
	return m.focus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Focus sets the focus state on the model. When the model is in focus it can
 | 
			
		||||
// receive keyboard input and the cursor will be shown.
 | 
			
		||||
func (m *Model) Focus() tea.Cmd {
 | 
			
		||||
	m.focus = true
 | 
			
		||||
	return m.Cursor.Focus()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blur removes the focus state on the model.  When the model is blurred it can
 | 
			
		||||
// not receive keyboard input and the cursor will be hidden.
 | 
			
		||||
func (m *Model) Blur() {
 | 
			
		||||
	m.focus = false
 | 
			
		||||
	m.Cursor.Blur()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Reset sets the input to its default state with no input.
 | 
			
		||||
func (m *Model) Reset() {
 | 
			
		||||
	m.value = nil
 | 
			
		||||
	m.SetCursor(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetSuggestions sets the suggestions for the input.
 | 
			
		||||
func (m *Model) SetSuggestions(suggestions []string) {
 | 
			
		||||
	m.suggestions = make([][]rune, len(suggestions))
 | 
			
		||||
	for i, s := range suggestions {
 | 
			
		||||
		m.suggestions[i] = []rune(s)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.updateSuggestions()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// rsan initializes or retrieves the rune sanitizer.
 | 
			
		||||
func (m *Model) san() runeutil.Sanitizer {
 | 
			
		||||
	if m.rsan == nil {
 | 
			
		||||
		// Textinput has all its input on a single line so collapse
 | 
			
		||||
		// newlines/tabs to single spaces.
 | 
			
		||||
		m.rsan = runeutil.NewSanitizer(
 | 
			
		||||
			runeutil.ReplaceTabs(" "), runeutil.ReplaceNewlines(" "))
 | 
			
		||||
	}
 | 
			
		||||
	return m.rsan
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Model) insertRunesFromUserInput(v []rune) {
 | 
			
		||||
	// Clean up any special characters in the input provided by the
 | 
			
		||||
	// clipboard. This avoids bugs due to e.g. tab characters and
 | 
			
		||||
	// whatnot.
 | 
			
		||||
	paste := m.san().Sanitize(v)
 | 
			
		||||
 | 
			
		||||
	var availSpace int
 | 
			
		||||
	if m.CharLimit > 0 {
 | 
			
		||||
		availSpace = m.CharLimit - len(m.value)
 | 
			
		||||
 | 
			
		||||
		// If the char limit's been reached, cancel.
 | 
			
		||||
		if availSpace <= 0 {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If there's not enough space to paste the whole thing cut the pasted
 | 
			
		||||
		// runes down so they'll fit.
 | 
			
		||||
		if availSpace < len(paste) {
 | 
			
		||||
			paste = paste[:availSpace]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Stuff before and after the cursor
 | 
			
		||||
	head := m.value[:m.pos]
 | 
			
		||||
	tailSrc := m.value[m.pos:]
 | 
			
		||||
	tail := make([]rune, len(tailSrc))
 | 
			
		||||
	copy(tail, tailSrc)
 | 
			
		||||
 | 
			
		||||
	// Insert pasted runes
 | 
			
		||||
	for _, r := range paste {
 | 
			
		||||
		head = append(head, r)
 | 
			
		||||
		m.pos++
 | 
			
		||||
		if m.CharLimit > 0 {
 | 
			
		||||
			availSpace--
 | 
			
		||||
			if availSpace <= 0 {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Put it all back together
 | 
			
		||||
	value := append(head, tail...)
 | 
			
		||||
	inputErr := m.validate(value)
 | 
			
		||||
	m.setValueInternal(value, inputErr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// If a max width is defined, perform some logic to treat the visible area
 | 
			
		||||
// as a horizontally scrolling viewport.
 | 
			
		||||
func (m *Model) handleOverflow() {
 | 
			
		||||
	if m.Width <= 0 || uniseg.StringWidth(string(m.value)) <= m.Width {
 | 
			
		||||
		m.offset = 0
 | 
			
		||||
		m.offsetRight = len(m.value)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Correct right offset if we've deleted characters
 | 
			
		||||
	m.offsetRight = min(m.offsetRight, len(m.value))
 | 
			
		||||
 | 
			
		||||
	if m.pos < m.offset {
 | 
			
		||||
		m.offset = m.pos
 | 
			
		||||
 | 
			
		||||
		w := 0
 | 
			
		||||
		i := 0
 | 
			
		||||
		runes := m.value[m.offset:]
 | 
			
		||||
 | 
			
		||||
		for i < len(runes) && w <= m.Width {
 | 
			
		||||
			w += rw.RuneWidth(runes[i])
 | 
			
		||||
			if w <= m.Width+1 {
 | 
			
		||||
				i++
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m.offsetRight = m.offset + i
 | 
			
		||||
	} else if m.pos >= m.offsetRight {
 | 
			
		||||
		m.offsetRight = m.pos
 | 
			
		||||
 | 
			
		||||
		w := 0
 | 
			
		||||
		runes := m.value[:m.offsetRight]
 | 
			
		||||
		i := len(runes) - 1
 | 
			
		||||
 | 
			
		||||
		for i > 0 && w < m.Width {
 | 
			
		||||
			w += rw.RuneWidth(runes[i])
 | 
			
		||||
			if w <= m.Width {
 | 
			
		||||
				i--
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m.offset = m.offsetRight - (len(runes) - 1 - i)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// deleteBeforeCursor deletes all text before the cursor.
 | 
			
		||||
func (m *Model) deleteBeforeCursor() {
 | 
			
		||||
	m.value = m.value[m.pos:]
 | 
			
		||||
	m.Err = m.validate(m.value)
 | 
			
		||||
	m.offset = 0
 | 
			
		||||
	m.SetCursor(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// deleteAfterCursor deletes all text after the cursor. If input is masked
 | 
			
		||||
// delete everything after the cursor so as not to reveal word breaks in the
 | 
			
		||||
// masked input.
 | 
			
		||||
func (m *Model) deleteAfterCursor() {
 | 
			
		||||
	m.value = m.value[:m.pos]
 | 
			
		||||
	m.Err = m.validate(m.value)
 | 
			
		||||
	m.SetCursor(len(m.value))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// deleteWordBackward deletes the word left to the cursor.
 | 
			
		||||
func (m *Model) deleteWordBackward() {
 | 
			
		||||
	if m.pos == 0 || len(m.value) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.EchoMode != EchoNormal {
 | 
			
		||||
		m.deleteBeforeCursor()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Linter note: it's critical that we acquire the initial cursor position
 | 
			
		||||
	// here prior to altering it via SetCursor() below. As such, moving this
 | 
			
		||||
	// call into the corresponding if clause does not apply here.
 | 
			
		||||
	oldPos := m.pos //nolint:ifshort
 | 
			
		||||
 | 
			
		||||
	m.SetCursor(m.pos - 1)
 | 
			
		||||
	for unicode.IsSpace(m.value[m.pos]) {
 | 
			
		||||
		if m.pos <= 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		// ignore series of whitespace before cursor
 | 
			
		||||
		m.SetCursor(m.pos - 1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for m.pos > 0 {
 | 
			
		||||
		if !unicode.IsSpace(m.value[m.pos]) {
 | 
			
		||||
			m.SetCursor(m.pos - 1)
 | 
			
		||||
		} else {
 | 
			
		||||
			if m.pos > 0 {
 | 
			
		||||
				// keep the previous space
 | 
			
		||||
				m.SetCursor(m.pos + 1)
 | 
			
		||||
			}
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if oldPos > len(m.value) {
 | 
			
		||||
		m.value = m.value[:m.pos]
 | 
			
		||||
	} else {
 | 
			
		||||
		m.value = append(m.value[:m.pos], m.value[oldPos:]...)
 | 
			
		||||
	}
 | 
			
		||||
	m.Err = m.validate(m.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// deleteWordForward deletes the word right to the cursor. If input is masked
 | 
			
		||||
// delete everything after the cursor so as not to reveal word breaks in the
 | 
			
		||||
// masked input.
 | 
			
		||||
func (m *Model) deleteWordForward() {
 | 
			
		||||
	if m.pos >= len(m.value) || len(m.value) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.EchoMode != EchoNormal {
 | 
			
		||||
		m.deleteAfterCursor()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldPos := m.pos
 | 
			
		||||
	m.SetCursor(m.pos + 1)
 | 
			
		||||
	for unicode.IsSpace(m.value[m.pos]) {
 | 
			
		||||
		// ignore series of whitespace after cursor
 | 
			
		||||
		m.SetCursor(m.pos + 1)
 | 
			
		||||
 | 
			
		||||
		if m.pos >= len(m.value) {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for m.pos < len(m.value) {
 | 
			
		||||
		if !unicode.IsSpace(m.value[m.pos]) {
 | 
			
		||||
			m.SetCursor(m.pos + 1)
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.pos > len(m.value) {
 | 
			
		||||
		m.value = m.value[:oldPos]
 | 
			
		||||
	} else {
 | 
			
		||||
		m.value = append(m.value[:oldPos], m.value[m.pos:]...)
 | 
			
		||||
	}
 | 
			
		||||
	m.Err = m.validate(m.value)
 | 
			
		||||
 | 
			
		||||
	m.SetCursor(oldPos)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// wordBackward moves the cursor one word to the left. If input is masked, move
 | 
			
		||||
// input to the start so as not to reveal word breaks in the masked input.
 | 
			
		||||
func (m *Model) wordBackward() {
 | 
			
		||||
	if m.pos == 0 || len(m.value) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.EchoMode != EchoNormal {
 | 
			
		||||
		m.CursorStart()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	i := m.pos - 1
 | 
			
		||||
	for i >= 0 {
 | 
			
		||||
		if unicode.IsSpace(m.value[i]) {
 | 
			
		||||
			m.SetCursor(m.pos - 1)
 | 
			
		||||
			i--
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i >= 0 {
 | 
			
		||||
		if !unicode.IsSpace(m.value[i]) {
 | 
			
		||||
			m.SetCursor(m.pos - 1)
 | 
			
		||||
			i--
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// wordForward moves the cursor one word to the right. If the input is masked,
 | 
			
		||||
// move input to the end so as not to reveal word breaks in the masked input.
 | 
			
		||||
func (m *Model) wordForward() {
 | 
			
		||||
	if m.pos >= len(m.value) || len(m.value) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.EchoMode != EchoNormal {
 | 
			
		||||
		m.CursorEnd()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	i := m.pos
 | 
			
		||||
	for i < len(m.value) {
 | 
			
		||||
		if unicode.IsSpace(m.value[i]) {
 | 
			
		||||
			m.SetCursor(m.pos + 1)
 | 
			
		||||
			i++
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i < len(m.value) {
 | 
			
		||||
		if !unicode.IsSpace(m.value[i]) {
 | 
			
		||||
			m.SetCursor(m.pos + 1)
 | 
			
		||||
			i++
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Model) echoTransform(v string) string {
 | 
			
		||||
	switch m.EchoMode {
 | 
			
		||||
	case EchoPassword:
 | 
			
		||||
		return strings.Repeat(string(m.EchoCharacter), uniseg.StringWidth(v))
 | 
			
		||||
	case EchoNone:
 | 
			
		||||
		return ""
 | 
			
		||||
	case EchoNormal:
 | 
			
		||||
		return v
 | 
			
		||||
	default:
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update is the Bubble Tea update loop.
 | 
			
		||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 | 
			
		||||
	if !m.focus {
 | 
			
		||||
		return m, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Need to check for completion before, because key is configurable and might be double assigned
 | 
			
		||||
	keyMsg, ok := msg.(tea.KeyMsg)
 | 
			
		||||
	if ok && key.Matches(keyMsg, m.KeyMap.AcceptSuggestion) {
 | 
			
		||||
		if m.canAcceptSuggestion() {
 | 
			
		||||
			m.value = append(m.value, m.matchedSuggestions[m.currentSuggestionIndex][len(m.value):]...)
 | 
			
		||||
			m.CursorEnd()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Let's remember where the position of the cursor currently is so that if
 | 
			
		||||
	// the cursor position changes, we can reset the blink.
 | 
			
		||||
	oldPos := m.pos
 | 
			
		||||
 | 
			
		||||
	switch msg := msg.(type) {
 | 
			
		||||
	case tea.KeyMsg:
 | 
			
		||||
		switch {
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.DeleteWordBackward):
 | 
			
		||||
			m.deleteWordBackward()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
 | 
			
		||||
			m.Err = nil
 | 
			
		||||
			if len(m.value) > 0 {
 | 
			
		||||
				m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
 | 
			
		||||
				m.Err = m.validate(m.value)
 | 
			
		||||
				if m.pos > 0 {
 | 
			
		||||
					m.SetCursor(m.pos - 1)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.WordBackward):
 | 
			
		||||
			m.wordBackward()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.CharacterBackward):
 | 
			
		||||
			if m.pos > 0 {
 | 
			
		||||
				m.SetCursor(m.pos - 1)
 | 
			
		||||
			}
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.WordForward):
 | 
			
		||||
			m.wordForward()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.CharacterForward):
 | 
			
		||||
			if m.pos < len(m.value) {
 | 
			
		||||
				m.SetCursor(m.pos + 1)
 | 
			
		||||
			}
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.LineStart):
 | 
			
		||||
			m.CursorStart()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
 | 
			
		||||
			if len(m.value) > 0 && m.pos < len(m.value) {
 | 
			
		||||
				m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
 | 
			
		||||
				m.Err = m.validate(m.value)
 | 
			
		||||
			}
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.LineEnd):
 | 
			
		||||
			m.CursorEnd()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
 | 
			
		||||
			m.deleteAfterCursor()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
 | 
			
		||||
			m.deleteBeforeCursor()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.Paste):
 | 
			
		||||
			return m, Paste
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.DeleteWordForward):
 | 
			
		||||
			m.deleteWordForward()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.NextSuggestion):
 | 
			
		||||
			m.nextSuggestion()
 | 
			
		||||
		case key.Matches(msg, m.KeyMap.PrevSuggestion):
 | 
			
		||||
			m.previousSuggestion()
 | 
			
		||||
		default:
 | 
			
		||||
			// Input one or more regular characters.
 | 
			
		||||
			m.insertRunesFromUserInput(msg.Runes)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Check again if can be completed
 | 
			
		||||
		// because value might be something that does not match the completion prefix
 | 
			
		||||
		m.updateSuggestions()
 | 
			
		||||
 | 
			
		||||
	case pasteMsg:
 | 
			
		||||
		m.insertRunesFromUserInput([]rune(msg))
 | 
			
		||||
 | 
			
		||||
	case pasteErrMsg:
 | 
			
		||||
		m.Err = msg
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var cmds []tea.Cmd
 | 
			
		||||
	var cmd tea.Cmd
 | 
			
		||||
 | 
			
		||||
	m.Cursor, cmd = m.Cursor.Update(msg)
 | 
			
		||||
	cmds = append(cmds, cmd)
 | 
			
		||||
 | 
			
		||||
	if oldPos != m.pos && m.Cursor.Mode() == cursor.CursorBlink {
 | 
			
		||||
		m.Cursor.Blink = false
 | 
			
		||||
		cmds = append(cmds, m.Cursor.BlinkCmd())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.handleOverflow()
 | 
			
		||||
	return m, tea.Batch(cmds...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// View renders the textinput in its current state.
 | 
			
		||||
func (m Model) View() string {
 | 
			
		||||
	// Placeholder text
 | 
			
		||||
	if len(m.value) == 0 && m.Placeholder != "" {
 | 
			
		||||
		return m.placeholderView()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	styleText := m.TextStyle.Inline(true).Render
 | 
			
		||||
 | 
			
		||||
	value := m.value[m.offset:m.offsetRight]
 | 
			
		||||
	pos := max(0, m.pos-m.offset)
 | 
			
		||||
	v := styleText(m.echoTransform(string(value[:pos])))
 | 
			
		||||
 | 
			
		||||
	if pos < len(value) { //nolint:nestif
 | 
			
		||||
		char := m.echoTransform(string(value[pos]))
 | 
			
		||||
		m.Cursor.SetChar(char)
 | 
			
		||||
		v += m.Cursor.View()                                   // cursor and text under it
 | 
			
		||||
		v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
 | 
			
		||||
		v += m.completionView(0)                               // suggested completion
 | 
			
		||||
	} else {
 | 
			
		||||
		if m.focus && m.canAcceptSuggestion() {
 | 
			
		||||
			suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
 | 
			
		||||
			if len(value) < len(suggestion) {
 | 
			
		||||
				m.Cursor.TextStyle = m.CompletionStyle
 | 
			
		||||
				m.Cursor.SetChar(m.echoTransform(string(suggestion[pos])))
 | 
			
		||||
				v += m.Cursor.View()
 | 
			
		||||
				v += m.completionView(1)
 | 
			
		||||
			} else {
 | 
			
		||||
				m.Cursor.SetChar(" ")
 | 
			
		||||
				v += m.Cursor.View()
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			m.Cursor.SetChar(" ")
 | 
			
		||||
			v += m.Cursor.View()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If a max width and background color were set fill the empty spaces with
 | 
			
		||||
	// the background color.
 | 
			
		||||
	valWidth := uniseg.StringWidth(string(value))
 | 
			
		||||
	if m.Width > 0 && valWidth <= m.Width {
 | 
			
		||||
		padding := max(0, m.Width-valWidth)
 | 
			
		||||
		if valWidth+padding <= m.Width && pos < len(value) {
 | 
			
		||||
			padding++
 | 
			
		||||
		}
 | 
			
		||||
		v += styleText(strings.Repeat(" ", padding))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m.PromptStyle.Render(m.Prompt) + v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// placeholderView returns the prompt and placeholder view, if any.
 | 
			
		||||
func (m Model) placeholderView() string {
 | 
			
		||||
	var (
 | 
			
		||||
		v     string
 | 
			
		||||
		style = m.PlaceholderStyle.Inline(true).Render
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	p := make([]rune, m.Width+1)
 | 
			
		||||
	copy(p, []rune(m.Placeholder))
 | 
			
		||||
 | 
			
		||||
	m.Cursor.TextStyle = m.PlaceholderStyle
 | 
			
		||||
	m.Cursor.SetChar(string(p[:1]))
 | 
			
		||||
	v += m.Cursor.View()
 | 
			
		||||
 | 
			
		||||
	// If the entire placeholder is already set and no padding is needed, finish
 | 
			
		||||
	if m.Width < 1 && len(p) <= 1 {
 | 
			
		||||
		return m.PromptStyle.Render(m.Prompt) + v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If Width is set then size placeholder accordingly
 | 
			
		||||
	if m.Width > 0 {
 | 
			
		||||
		// available width is width - len + cursor offset of 1
 | 
			
		||||
		minWidth := lipgloss.Width(m.Placeholder)
 | 
			
		||||
		availWidth := m.Width - minWidth + 1
 | 
			
		||||
 | 
			
		||||
		// if width < len, 'subtract'(add) number to len and dont add padding
 | 
			
		||||
		if availWidth < 0 {
 | 
			
		||||
			minWidth += availWidth
 | 
			
		||||
			availWidth = 0
 | 
			
		||||
		}
 | 
			
		||||
		// append placeholder[len] - cursor, append padding
 | 
			
		||||
		v += style(string(p[1:minWidth]))
 | 
			
		||||
		v += style(strings.Repeat(" ", availWidth))
 | 
			
		||||
	} else {
 | 
			
		||||
		// if there is no width, the placeholder can be any length
 | 
			
		||||
		v += style(string(p[1:]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m.PromptStyle.Render(m.Prompt) + v
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blink is a command used to initialize cursor blinking.
 | 
			
		||||
func Blink() tea.Msg {
 | 
			
		||||
	return cursor.Blink()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Paste is a command for pasting from the clipboard into the text input.
 | 
			
		||||
func Paste() tea.Msg {
 | 
			
		||||
	str, err := clipboard.ReadAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return pasteErrMsg{err}
 | 
			
		||||
	}
 | 
			
		||||
	return pasteMsg(str)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func clamp(v, low, high int) int {
 | 
			
		||||
	if high < low {
 | 
			
		||||
		low, high = high, low
 | 
			
		||||
	}
 | 
			
		||||
	return min(high, max(low, v))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated.
 | 
			
		||||
 | 
			
		||||
// Deprecated: use [cursor.Mode].
 | 
			
		||||
//
 | 
			
		||||
//nolint:revive
 | 
			
		||||
type CursorMode int
 | 
			
		||||
 | 
			
		||||
//nolint:revive
 | 
			
		||||
const (
 | 
			
		||||
	// Deprecated: use [cursor.CursorBlink].
 | 
			
		||||
	CursorBlink = CursorMode(cursor.CursorBlink)
 | 
			
		||||
	// Deprecated: use [cursor.CursorStatic].
 | 
			
		||||
	CursorStatic = CursorMode(cursor.CursorStatic)
 | 
			
		||||
	// Deprecated: use [cursor.CursorHide].
 | 
			
		||||
	CursorHide = CursorMode(cursor.CursorHide)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (c CursorMode) String() string {
 | 
			
		||||
	return cursor.Mode(c).String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: use [cursor.Mode].
 | 
			
		||||
//
 | 
			
		||||
//nolint:revive
 | 
			
		||||
func (m Model) CursorMode() CursorMode {
 | 
			
		||||
	return CursorMode(m.Cursor.Mode())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: use cursor.SetMode().
 | 
			
		||||
//
 | 
			
		||||
//nolint:revive
 | 
			
		||||
func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
 | 
			
		||||
	return m.Cursor.SetMode(cursor.Mode(mode))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Model) completionView(offset int) string {
 | 
			
		||||
	var (
 | 
			
		||||
		value = m.value
 | 
			
		||||
		style = m.PlaceholderStyle.Inline(true).Render
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if m.canAcceptSuggestion() {
 | 
			
		||||
		suggestion := m.matchedSuggestions[m.currentSuggestionIndex]
 | 
			
		||||
		if len(value) < len(suggestion) {
 | 
			
		||||
			return style(string(suggestion[len(value)+offset:]))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Model) getSuggestions(sugs [][]rune) []string {
 | 
			
		||||
	suggestions := make([]string, len(sugs))
 | 
			
		||||
	for i, s := range sugs {
 | 
			
		||||
		suggestions[i] = string(s)
 | 
			
		||||
	}
 | 
			
		||||
	return suggestions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AvailableSuggestions returns the list of available suggestions.
 | 
			
		||||
func (m *Model) AvailableSuggestions() []string {
 | 
			
		||||
	return m.getSuggestions(m.suggestions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchedSuggestions returns the list of matched suggestions.
 | 
			
		||||
func (m *Model) MatchedSuggestions() []string {
 | 
			
		||||
	return m.getSuggestions(m.matchedSuggestions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CurrentSuggestionIndex returns the currently selected suggestion index.
 | 
			
		||||
func (m *Model) CurrentSuggestionIndex() int {
 | 
			
		||||
	return m.currentSuggestionIndex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CurrentSuggestion returns the currently selected suggestion.
 | 
			
		||||
func (m *Model) CurrentSuggestion() string {
 | 
			
		||||
	if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(m.matchedSuggestions[m.currentSuggestionIndex])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// canAcceptSuggestion returns whether there is an acceptable suggestion to
 | 
			
		||||
// autocomplete the current value.
 | 
			
		||||
func (m *Model) canAcceptSuggestion() bool {
 | 
			
		||||
	return len(m.matchedSuggestions) > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateSuggestions refreshes the list of matching suggestions.
 | 
			
		||||
func (m *Model) updateSuggestions() {
 | 
			
		||||
	if !m.ShowSuggestions {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(m.value) <= 0 || len(m.suggestions) <= 0 {
 | 
			
		||||
		m.matchedSuggestions = [][]rune{}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	matches := [][]rune{}
 | 
			
		||||
	for _, s := range m.suggestions {
 | 
			
		||||
		suggestion := string(s)
 | 
			
		||||
 | 
			
		||||
		if strings.HasPrefix(strings.ToLower(suggestion), strings.ToLower(string(m.value))) {
 | 
			
		||||
			matches = append(matches, []rune(suggestion))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !reflect.DeepEqual(matches, m.matchedSuggestions) {
 | 
			
		||||
		m.currentSuggestionIndex = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.matchedSuggestions = matches
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nextSuggestion selects the next suggestion.
 | 
			
		||||
func (m *Model) nextSuggestion() {
 | 
			
		||||
	m.currentSuggestionIndex = (m.currentSuggestionIndex + 1)
 | 
			
		||||
	if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
 | 
			
		||||
		m.currentSuggestionIndex = 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// previousSuggestion selects the previous suggestion.
 | 
			
		||||
func (m *Model) previousSuggestion() {
 | 
			
		||||
	m.currentSuggestionIndex = (m.currentSuggestionIndex - 1)
 | 
			
		||||
	if m.currentSuggestionIndex < 0 {
 | 
			
		||||
		m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Model) validate(v []rune) error {
 | 
			
		||||
	if m.Validate != nil {
 | 
			
		||||
		return m.Validate(string(v))
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								vendor/github.com/charmbracelet/bubbletea/.golangci.yml
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/github.com/charmbracelet/bubbletea/.golangci.yml
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -26,6 +26,10 @@ linters:
 | 
			
		||||
    - whitespace
 | 
			
		||||
    - wrapcheck
 | 
			
		||||
  exclusions:
 | 
			
		||||
    rules:
 | 
			
		||||
      - text: '(slog|log)\.\w+'
 | 
			
		||||
        linters:
 | 
			
		||||
          - noctx
 | 
			
		||||
    generated: lax
 | 
			
		||||
    presets:
 | 
			
		||||
      - common-false-positives
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2020-2023 Charmbracelet, Inc
 | 
			
		||||
Copyright (c) 2020-2025 Charmbracelet, Inc
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								vendor/github.com/charmbracelet/bubbletea/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/github.com/charmbracelet/bubbletea/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -9,7 +9,7 @@
 | 
			
		||||
    <br>
 | 
			
		||||
    <a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
 | 
			
		||||
    <a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
 | 
			
		||||
    <a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
 | 
			
		||||
    <a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
The fun, functional and stateful way to build terminal apps. A Go framework
 | 
			
		||||
@ -395,6 +395,6 @@ of days past.
 | 
			
		||||
 | 
			
		||||
Part of [Charm](https://charm.sh).
 | 
			
		||||
 | 
			
		||||
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
 | 
			
		||||
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
 | 
			
		||||
 | 
			
		||||
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								vendor/github.com/charmbracelet/bubbletea/commands.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								vendor/github.com/charmbracelet/bubbletea/commands.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -13,6 +13,27 @@ import (
 | 
			
		||||
//		       return tea.Batch(someCommand, someOtherCommand)
 | 
			
		||||
//	    }
 | 
			
		||||
func Batch(cmds ...Cmd) Cmd {
 | 
			
		||||
	return compactCmds[BatchMsg](cmds)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BatchMsg is a message used to perform a bunch of commands concurrently with
 | 
			
		||||
// no ordering guarantees. You can send a BatchMsg with Batch.
 | 
			
		||||
type BatchMsg []Cmd
 | 
			
		||||
 | 
			
		||||
// Sequence runs the given commands one at a time, in order. Contrast this with
 | 
			
		||||
// Batch, which runs commands concurrently.
 | 
			
		||||
func Sequence(cmds ...Cmd) Cmd {
 | 
			
		||||
	return compactCmds[sequenceMsg](cmds)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sequenceMsg is used internally to run the given commands in order.
 | 
			
		||||
type sequenceMsg []Cmd
 | 
			
		||||
 | 
			
		||||
// compactCmds ignores any nil commands in cmds, and returns the most direct
 | 
			
		||||
// command possible. That is, considering the non-nil commands, if there are
 | 
			
		||||
// none it returns nil, if there is exactly one it returns that command
 | 
			
		||||
// directly, else it returns the non-nil commands as type T.
 | 
			
		||||
func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
 | 
			
		||||
	var validCmds []Cmd //nolint:prealloc
 | 
			
		||||
	for _, c := range cmds {
 | 
			
		||||
		if c == nil {
 | 
			
		||||
@ -27,26 +48,11 @@ func Batch(cmds ...Cmd) Cmd {
 | 
			
		||||
		return validCmds[0]
 | 
			
		||||
	default:
 | 
			
		||||
		return func() Msg {
 | 
			
		||||
			return BatchMsg(validCmds)
 | 
			
		||||
			return T(validCmds)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BatchMsg is a message used to perform a bunch of commands concurrently with
 | 
			
		||||
// no ordering guarantees. You can send a BatchMsg with Batch.
 | 
			
		||||
type BatchMsg []Cmd
 | 
			
		||||
 | 
			
		||||
// Sequence runs the given commands one at a time, in order. Contrast this with
 | 
			
		||||
// Batch, which runs commands concurrently.
 | 
			
		||||
func Sequence(cmds ...Cmd) Cmd {
 | 
			
		||||
	return func() Msg {
 | 
			
		||||
		return sequenceMsg(cmds)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sequenceMsg is used internally to run the given commands in order.
 | 
			
		||||
type sequenceMsg []Cmd
 | 
			
		||||
 | 
			
		||||
// Every is a command that ticks in sync with the system clock. So, if you
 | 
			
		||||
// wanted to tick with the system clock every second, minute or hour you
 | 
			
		||||
// could use this. It's also handy for having different things tick in sync.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -108,7 +108,7 @@ func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32,
 | 
			
		||||
	return originalMode, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// cancelMixin represents a goroutine-safe cancelation status.
 | 
			
		||||
// cancelMixin represents a goroutine-safe cancellation status.
 | 
			
		||||
type cancelMixin struct {
 | 
			
		||||
	unsafeCanceled bool
 | 
			
		||||
	lock           sync.Mutex
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								vendor/github.com/charmbracelet/bubbletea/key_windows.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								vendor/github.com/charmbracelet/bubbletea/key_windows.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -109,12 +109,12 @@ func peekAndReadConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
 | 
			
		||||
	return events, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Convert i to unit32 or panic if it cannot be converted. Check satisifes lint G115.
 | 
			
		||||
// Convert i to unit32 or panic if it cannot be converted. Check satisfies lint G115.
 | 
			
		||||
func intToUint32OrDie(i int) uint32 {
 | 
			
		||||
	if i < 0 {
 | 
			
		||||
		panic("cannot convert numEvents " + fmt.Sprint(i) + " to uint32")
 | 
			
		||||
	}
 | 
			
		||||
	return uint32(i)
 | 
			
		||||
	return uint32(i) //nolint:gosec
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Keeps peeking until there is data or the input is cancelled.
 | 
			
		||||
@ -158,16 +158,16 @@ func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action Mou
 | 
			
		||||
		return button, action
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
 | 
			
		||||
	switch btn {
 | 
			
		||||
	case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
 | 
			
		||||
		button = MouseButtonLeft
 | 
			
		||||
	case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button
 | 
			
		||||
	case coninput.RIGHTMOST_BUTTON_PRESSED: // right button
 | 
			
		||||
		button = MouseButtonRight
 | 
			
		||||
	case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
 | 
			
		||||
	case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
 | 
			
		||||
		button = MouseButtonMiddle
 | 
			
		||||
	case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
 | 
			
		||||
	case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
 | 
			
		||||
		button = MouseButtonBackward
 | 
			
		||||
	case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
 | 
			
		||||
	case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
 | 
			
		||||
		button = MouseButtonForward
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/screen.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/screen.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -131,7 +131,7 @@ func EnableBracketedPaste() Msg {
 | 
			
		||||
type enableBracketedPasteMsg struct{}
 | 
			
		||||
 | 
			
		||||
// DisableBracketedPaste is a special command that tells the Bubble Tea program
 | 
			
		||||
// to accept bracketed paste input.
 | 
			
		||||
// to stop processing bracketed paste input.
 | 
			
		||||
//
 | 
			
		||||
// Note that bracketed paste will be automatically disabled when the
 | 
			
		||||
// program quits.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -277,7 +277,7 @@ func (r *standardRenderer) flush() {
 | 
			
		||||
		// using the full terminal window.
 | 
			
		||||
		buf.WriteString(ansi.CursorPosition(0, len(newLines)))
 | 
			
		||||
	} else {
 | 
			
		||||
		buf.WriteString(ansi.CursorBackward(r.width))
 | 
			
		||||
		buf.WriteByte('\r')
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, _ = r.out.Write(buf.Bytes())
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										109
									
								
								vendor/github.com/charmbracelet/bubbletea/tea.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										109
									
								
								vendor/github.com/charmbracelet/bubbletea/tea.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -24,7 +24,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/charmbracelet/x/term"
 | 
			
		||||
	"github.com/muesli/cancelreader"
 | 
			
		||||
	"golang.org/x/sync/errgroup"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
 | 
			
		||||
@ -73,7 +72,7 @@ const (
 | 
			
		||||
	customInput
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// String implements the stringer interface for [inputType]. It is inteded to
 | 
			
		||||
// String implements the stringer interface for [inputType]. It is intended to
 | 
			
		||||
// be used in testing.
 | 
			
		||||
func (i inputType) String() string {
 | 
			
		||||
	return [...]string{
 | 
			
		||||
@ -220,7 +219,7 @@ func Suspend() Msg {
 | 
			
		||||
// You can send this message with [Suspend()].
 | 
			
		||||
type SuspendMsg struct{}
 | 
			
		||||
 | 
			
		||||
// ResumeMsg can be listen to to do something once a program is resumed back
 | 
			
		||||
// ResumeMsg can be listen to do something once a program is resumed back
 | 
			
		||||
// from a suspend state.
 | 
			
		||||
type ResumeMsg struct{}
 | 
			
		||||
 | 
			
		||||
@ -472,42 +471,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 | 
			
		||||
				p.exec(msg.cmd, msg.fn)
 | 
			
		||||
 | 
			
		||||
			case BatchMsg:
 | 
			
		||||
				for _, cmd := range msg {
 | 
			
		||||
					select {
 | 
			
		||||
					case <-p.ctx.Done():
 | 
			
		||||
						return model, nil
 | 
			
		||||
					case cmds <- cmd:
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				go p.execBatchMsg(msg)
 | 
			
		||||
				continue
 | 
			
		||||
 | 
			
		||||
			case sequenceMsg:
 | 
			
		||||
				go func() {
 | 
			
		||||
					// Execute commands one at a time, in order.
 | 
			
		||||
					for _, cmd := range msg {
 | 
			
		||||
						if cmd == nil {
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						msg := cmd()
 | 
			
		||||
						if batchMsg, ok := msg.(BatchMsg); ok {
 | 
			
		||||
							g, _ := errgroup.WithContext(p.ctx)
 | 
			
		||||
							for _, cmd := range batchMsg {
 | 
			
		||||
								cmd := cmd
 | 
			
		||||
								g.Go(func() error {
 | 
			
		||||
									p.Send(cmd())
 | 
			
		||||
									return nil
 | 
			
		||||
								})
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							//nolint:errcheck,gosec
 | 
			
		||||
							g.Wait() // wait for all commands from batch msg to finish
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						p.Send(msg)
 | 
			
		||||
					}
 | 
			
		||||
				}()
 | 
			
		||||
				go p.execSequenceMsg(msg)
 | 
			
		||||
				continue
 | 
			
		||||
 | 
			
		||||
			case setWindowTitleMsg:
 | 
			
		||||
				p.SetWindowTitle(string(msg))
 | 
			
		||||
@ -535,6 +504,74 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Program) execSequenceMsg(msg sequenceMsg) {
 | 
			
		||||
	if !p.startupOptions.has(withoutCatchPanics) {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if r := recover(); r != nil {
 | 
			
		||||
				p.recoverFromGoPanic(r)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Execute commands one at a time, in order.
 | 
			
		||||
	for _, cmd := range msg {
 | 
			
		||||
		if cmd == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		msg := cmd()
 | 
			
		||||
		switch msg := msg.(type) {
 | 
			
		||||
		case BatchMsg:
 | 
			
		||||
			p.execBatchMsg(msg)
 | 
			
		||||
		case sequenceMsg:
 | 
			
		||||
			p.execSequenceMsg(msg)
 | 
			
		||||
		default:
 | 
			
		||||
			p.Send(msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Program) execBatchMsg(msg BatchMsg) {
 | 
			
		||||
	if !p.startupOptions.has(withoutCatchPanics) {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if r := recover(); r != nil {
 | 
			
		||||
				p.recoverFromGoPanic(r)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Execute commands one at a time.
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	for _, cmd := range msg {
 | 
			
		||||
		if cmd == nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		wg.Add(1)
 | 
			
		||||
		go func() {
 | 
			
		||||
			defer wg.Done()
 | 
			
		||||
 | 
			
		||||
			if !p.startupOptions.has(withoutCatchPanics) {
 | 
			
		||||
				defer func() {
 | 
			
		||||
					if r := recover(); r != nil {
 | 
			
		||||
						p.recoverFromGoPanic(r)
 | 
			
		||||
					}
 | 
			
		||||
				}()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			msg := cmd()
 | 
			
		||||
			switch msg := msg.(type) {
 | 
			
		||||
			case BatchMsg:
 | 
			
		||||
				p.execBatchMsg(msg)
 | 
			
		||||
			case sequenceMsg:
 | 
			
		||||
				p.execSequenceMsg(msg)
 | 
			
		||||
			default:
 | 
			
		||||
				p.Send(msg)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wg.Wait() // wait for all commands from batch msg to finish
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Run initializes the program and runs its event loops, blocking until it gets
 | 
			
		||||
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
 | 
			
		||||
// Returns the final model.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/tty_windows.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/charmbracelet/bubbletea/tty_windows.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -56,7 +56,7 @@ func (p *Program) initInput() (err error) {
 | 
			
		||||
 | 
			
		||||
// Open the Windows equivalent of a TTY.
 | 
			
		||||
func openInputTTY() (*os.File, error) {
 | 
			
		||||
	f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644)
 | 
			
		||||
	f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) //nolint:gosec
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error opening file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								vendor/github.com/charmbracelet/x/ansi/inband.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								vendor/github.com/charmbracelet/x/ansi/inband.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
package ansi
 | 
			
		||||
 | 
			
		||||
import "fmt"
 | 
			
		||||
 | 
			
		||||
// InBandResize encodes an in-band terminal resize event sequence.
 | 
			
		||||
//
 | 
			
		||||
//	CSI 48 ; height_cells ; widht_cells ; height_pixels ; width_pixels t
 | 
			
		||||
//
 | 
			
		||||
// See https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
 | 
			
		||||
func InBandResize(heightCells, widthCells, heightPixels, widthPixels int) string {
 | 
			
		||||
	if heightCells < 0 {
 | 
			
		||||
		heightCells = 0
 | 
			
		||||
	}
 | 
			
		||||
	if widthCells < 0 {
 | 
			
		||||
		widthCells = 0
 | 
			
		||||
	}
 | 
			
		||||
	if heightPixels < 0 {
 | 
			
		||||
		heightPixels = 0
 | 
			
		||||
	}
 | 
			
		||||
	if widthPixels < 0 {
 | 
			
		||||
		widthPixels = 0
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("\x1b[48;%d;%d;%d;%dt", heightCells, widthCells, heightPixels, widthPixels)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								vendor/github.com/charmbracelet/x/ansi/palette.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								vendor/github.com/charmbracelet/x/ansi/palette.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
package ansi
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"image/color"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SetPalette sets the palette color for the given index. The index is a 16
 | 
			
		||||
// color index between 0 and 15. The color is a 24-bit RGB color.
 | 
			
		||||
//
 | 
			
		||||
//	OSC P n rrggbb BEL
 | 
			
		||||
//
 | 
			
		||||
// Where n is the color index in hex (0-f), and rrggbb is the color in
 | 
			
		||||
// hexadecimal format (e.g., ff0000 for red).
 | 
			
		||||
//
 | 
			
		||||
// This sequence is specific to the Linux Console and may not work in other
 | 
			
		||||
// terminal emulators.
 | 
			
		||||
//
 | 
			
		||||
// See https://man7.org/linux/man-pages/man4/console_codes.4.html
 | 
			
		||||
func SetPalette(i int, c color.Color) string {
 | 
			
		||||
	if c == nil || i < 0 || i > 15 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	r, g, b, _ := c.RGBA()
 | 
			
		||||
	return fmt.Sprintf("\x1b]P%x%02x%02x%02x\x07", i, r>>8, g>>8, b>>8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ResetPalette resets the color palette to the default values.
 | 
			
		||||
//
 | 
			
		||||
// This sequence is specific to the Linux Console and may not work in other
 | 
			
		||||
// terminal emulators.
 | 
			
		||||
//
 | 
			
		||||
// See https://man7.org/linux/man-pages/man4/console_codes.4.html
 | 
			
		||||
const ResetPalette = "\x1b]R\x07"
 | 
			
		||||
							
								
								
									
										49
									
								
								vendor/github.com/charmbracelet/x/ansi/progress.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								vendor/github.com/charmbracelet/x/ansi/progress.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
package ansi
 | 
			
		||||
 | 
			
		||||
import "strconv"
 | 
			
		||||
 | 
			
		||||
// ResetProgressBar is a sequence that resets the progress bar to its default
 | 
			
		||||
// state (hidden).
 | 
			
		||||
//
 | 
			
		||||
// OSC 9 ; 4 ; 0 BEL
 | 
			
		||||
//
 | 
			
		||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
 | 
			
		||||
const ResetProgressBar = "\x1b]9;4;0\x07"
 | 
			
		||||
 | 
			
		||||
// SetProgressBar returns a sequence for setting the progress bar to a specific
 | 
			
		||||
// percentage (0-100) in the "default" state.
 | 
			
		||||
//
 | 
			
		||||
// OSC 9 ; 4 ; 1 Percentage BEL
 | 
			
		||||
//
 | 
			
		||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
 | 
			
		||||
func SetProgressBar(percentage int) string {
 | 
			
		||||
	return "\x1b]9;4;1;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetErrorProgressBar returns a sequence for setting the progress bar to a
 | 
			
		||||
// specific percentage (0-100) in the "Error" state..
 | 
			
		||||
//
 | 
			
		||||
// OSC 9 ; 4 ; 2 Percentage BEL
 | 
			
		||||
//
 | 
			
		||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
 | 
			
		||||
func SetErrorProgressBar(percentage int) string {
 | 
			
		||||
	return "\x1b]9;4;2;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetIndeterminateProgressBar is a sequence that sets the progress bar to the
 | 
			
		||||
// indeterminate state.
 | 
			
		||||
//
 | 
			
		||||
// OSC 9 ; 4 ; 3 BEL
 | 
			
		||||
//
 | 
			
		||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
 | 
			
		||||
const SetIndeterminateProgressBar = "\x1b]9;4;3\x07"
 | 
			
		||||
 | 
			
		||||
// SetWarningProgressBar is a sequence that sets the progress bar to the
 | 
			
		||||
// "Warning" state.
 | 
			
		||||
//
 | 
			
		||||
// OSC 9 ; 4 ; 4 Percentage BEL
 | 
			
		||||
//
 | 
			
		||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
 | 
			
		||||
func SetWarningProgressBar(percentage int) string {
 | 
			
		||||
	return "\x1b]9;4;4;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								vendor/github.com/charmbracelet/x/ansi/winop.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/github.com/charmbracelet/x/ansi/winop.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -8,16 +8,22 @@ import (
 | 
			
		||||
const (
 | 
			
		||||
	// ResizeWindowWinOp is a window operation that resizes the terminal
 | 
			
		||||
	// window.
 | 
			
		||||
	//
 | 
			
		||||
	// Deprecated: Use constant number directly with [WindowOp].
 | 
			
		||||
	ResizeWindowWinOp = 4
 | 
			
		||||
 | 
			
		||||
	// RequestWindowSizeWinOp is a window operation that requests a report of
 | 
			
		||||
	// the size of the terminal window in pixels. The response is in the form:
 | 
			
		||||
	//  CSI 4 ; height ; width t
 | 
			
		||||
	//
 | 
			
		||||
	// Deprecated: Use constant number directly with [WindowOp].
 | 
			
		||||
	RequestWindowSizeWinOp = 14
 | 
			
		||||
 | 
			
		||||
	// RequestCellSizeWinOp is a window operation that requests a report of
 | 
			
		||||
	// the size of the terminal cell size in pixels. The response is in the form:
 | 
			
		||||
	//  CSI 6 ; height ; width t
 | 
			
		||||
	//
 | 
			
		||||
	// Deprecated: Use constant number directly with [WindowOp].
 | 
			
		||||
	RequestCellSizeWinOp = 16
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								vendor/github.com/clipperhouse/uax29/v2/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/github.com/clipperhouse/uax29/v2/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2020 Matt Sherman
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
							
								
								
									
										82
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
 | 
			
		||||
 | 
			
		||||
## Quick start
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
go get "github.com/clipperhouse/uax29/v2/graphemes"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```go
 | 
			
		||||
import "github.com/clipperhouse/uax29/v2/graphemes"
 | 
			
		||||
 | 
			
		||||
text := "Hello, 世界. Nice dog! 👍🐶"
 | 
			
		||||
 | 
			
		||||
tokens := graphemes.FromString(text)
 | 
			
		||||
 | 
			
		||||
for tokens.Next() {                     // Next() returns true until end of data
 | 
			
		||||
	fmt.Println(tokens.Value())         // Do something with the current grapheme
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
[](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
 | 
			
		||||
 | 
			
		||||
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
 | 
			
		||||
 | 
			
		||||
## Conformance
 | 
			
		||||
 | 
			
		||||
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## APIs
 | 
			
		||||
 | 
			
		||||
### If you have a `string`
 | 
			
		||||
 | 
			
		||||
```go
 | 
			
		||||
text := "Hello, 世界. Nice dog! 👍🐶"
 | 
			
		||||
 | 
			
		||||
tokens := graphemes.FromString(text)
 | 
			
		||||
 | 
			
		||||
for tokens.Next() {                     // Next() returns true until end of data
 | 
			
		||||
	fmt.Println(tokens.Value())         // Do something with the current grapheme
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### If you have an `io.Reader`
 | 
			
		||||
 | 
			
		||||
`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
 | 
			
		||||
 | 
			
		||||
```go
 | 
			
		||||
r := getYourReader()                        // from a file or network maybe
 | 
			
		||||
tokens := graphemes.FromReader(r)
 | 
			
		||||
 | 
			
		||||
for tokens.Scan() {                         // Scan() returns true until error or EOF
 | 
			
		||||
	fmt.Println(tokens.Text())              // Do something with the current grapheme
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if tokens.Err() != nil {                    // Check the error
 | 
			
		||||
	log.Fatal(tokens.Err())
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### If you have a `[]byte`
 | 
			
		||||
 | 
			
		||||
```go
 | 
			
		||||
b := []byte("Hello, 世界. Nice dog! 👍🐶")
 | 
			
		||||
 | 
			
		||||
tokens := graphemes.FromBytes(b)
 | 
			
		||||
 | 
			
		||||
for tokens.Next() {                     // Next() returns true until end of data
 | 
			
		||||
	fmt.Println(tokens.Value())         // Do something with the current grapheme
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Performance
 | 
			
		||||
 | 
			
		||||
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations.
 | 
			
		||||
 | 
			
		||||
### Invalid inputs
 | 
			
		||||
 | 
			
		||||
Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”.
 | 
			
		||||
 | 
			
		||||
Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid).
 | 
			
		||||
							
								
								
									
										28
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
package graphemes
 | 
			
		||||
 | 
			
		||||
import "github.com/clipperhouse/uax29/v2/internal/iterators"
 | 
			
		||||
 | 
			
		||||
type Iterator[T iterators.Stringish] struct {
 | 
			
		||||
	*iterators.Iterator[T]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	splitFuncString = splitFunc[string]
 | 
			
		||||
	splitFuncBytes  = splitFunc[[]byte]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FromString returns an iterator for the grapheme clusters in the input string.
 | 
			
		||||
// Iterate while Next() is true, and access the grapheme via Value().
 | 
			
		||||
func FromString(s string) Iterator[string] {
 | 
			
		||||
	return Iterator[string]{
 | 
			
		||||
		iterators.New(splitFuncString, s),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
 | 
			
		||||
// Iterate while Next() is true, and access the grapheme via Value().
 | 
			
		||||
func FromBytes(b []byte) Iterator[[]byte] {
 | 
			
		||||
	return Iterator[[]byte]{
 | 
			
		||||
		iterators.New(splitFuncBytes, b),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
 | 
			
		||||
package graphemes
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"io"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Scanner struct {
 | 
			
		||||
	*bufio.Scanner
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FromReader returns a Scanner, to split graphemes per
 | 
			
		||||
// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
 | 
			
		||||
//
 | 
			
		||||
// It embeds a [bufio.Scanner], so you can use its methods.
 | 
			
		||||
//
 | 
			
		||||
// Iterate through graphemes by calling Scan() until false, then check Err().
 | 
			
		||||
func FromReader(r io.Reader) *Scanner {
 | 
			
		||||
	sc := bufio.NewScanner(r)
 | 
			
		||||
	sc.Split(SplitFunc)
 | 
			
		||||
	return &Scanner{
 | 
			
		||||
		Scanner: sc,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,174 @@
 | 
			
		||||
package graphemes
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
 | 
			
		||||
	"github.com/clipperhouse/uax29/v2/internal/iterators"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// is determines if lookup intersects propert(ies)
 | 
			
		||||
func (lookup property) is(properties property) bool {
 | 
			
		||||
	return (lookup & properties) != 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const _Ignore = _Extend
 | 
			
		||||
 | 
			
		||||
// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
 | 
			
		||||
//
 | 
			
		||||
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
 | 
			
		||||
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
 | 
			
		||||
 | 
			
		||||
func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) {
 | 
			
		||||
	var empty T
 | 
			
		||||
	if len(data) == 0 {
 | 
			
		||||
		return 0, empty, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// These vars are stateful across loop iterations
 | 
			
		||||
	var pos int
 | 
			
		||||
	var lastExIgnore property = 0     // "last excluding ignored categories"
 | 
			
		||||
	var lastLastExIgnore property = 0 // "last one before that"
 | 
			
		||||
	var regionalIndicatorCount int
 | 
			
		||||
 | 
			
		||||
	// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
 | 
			
		||||
	// to the right of the ×, from which we look back or forward
 | 
			
		||||
 | 
			
		||||
	current, w := lookup(data[pos:])
 | 
			
		||||
	if w == 0 {
 | 
			
		||||
		if !atEOF {
 | 
			
		||||
			// Rune extends past current data, request more
 | 
			
		||||
			return 0, empty, nil
 | 
			
		||||
		}
 | 
			
		||||
		pos = len(data)
 | 
			
		||||
		return pos, data[:pos], nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// https://unicode.org/reports/tr29/#GB1
 | 
			
		||||
	// Start of text always advances
 | 
			
		||||
	pos += w
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		eot := pos == len(data) // "end of text"
 | 
			
		||||
 | 
			
		||||
		if eot {
 | 
			
		||||
			if !atEOF {
 | 
			
		||||
				// Token extends past current data, request more
 | 
			
		||||
				return 0, empty, nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// https://unicode.org/reports/tr29/#GB2
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/*
 | 
			
		||||
			We've switched the evaluation order of GB1↓ and GB2↑. It's ok:
 | 
			
		||||
			because we've checked for len(data) at the top of this function,
 | 
			
		||||
			sot and eot are mutually exclusive, order doesn't matter.
 | 
			
		||||
		*/
 | 
			
		||||
 | 
			
		||||
		// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
 | 
			
		||||
		// to the right of the ×, from which we look back or forward
 | 
			
		||||
 | 
			
		||||
		// Remember previous properties to avoid lookups/lookbacks
 | 
			
		||||
		last := current
 | 
			
		||||
		if !last.is(_Ignore) {
 | 
			
		||||
			lastLastExIgnore = lastExIgnore
 | 
			
		||||
			lastExIgnore = last
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		current, w = lookup(data[pos:])
 | 
			
		||||
		if w == 0 {
 | 
			
		||||
			if atEOF {
 | 
			
		||||
				// Just return the bytes, we can't do anything with them
 | 
			
		||||
				pos = len(data)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			// Rune extends past current data, request more
 | 
			
		||||
			return 0, empty, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Optimization: no rule can possibly apply
 | 
			
		||||
		if current|last == 0 { // i.e. both are zero
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB3
 | 
			
		||||
		if current.is(_LF) && last.is(_CR) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB4
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB5
 | 
			
		||||
		if (current | last).is(_Control | _CR | _LF) {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB6
 | 
			
		||||
		if current.is(_L|_V|_LV|_LVT) && last.is(_L) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB7
 | 
			
		||||
		if current.is(_V|_T) && last.is(_LV|_V) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB8
 | 
			
		||||
		if current.is(_T) && last.is(_LVT|_T) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB9
 | 
			
		||||
		if current.is(_Extend | _ZWJ) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB9a
 | 
			
		||||
		if current.is(_SpacingMark) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB9b
 | 
			
		||||
		if last.is(_Prepend) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB9c
 | 
			
		||||
		// TODO(clipperhouse):
 | 
			
		||||
		// It appears to be added in Unicode 15.1.0:
 | 
			
		||||
		// https://unicode.org/versions/Unicode15.1.0/#Migration
 | 
			
		||||
		// This package currently supports Unicode 15.0.0, so
 | 
			
		||||
		// out of scope for now
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB11
 | 
			
		||||
		if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
 | 
			
		||||
			pos += w
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB12
 | 
			
		||||
		// https://unicode.org/reports/tr29/#GB13
 | 
			
		||||
		if (current & last).is(_RegionalIndicator) {
 | 
			
		||||
			regionalIndicatorCount++
 | 
			
		||||
 | 
			
		||||
			odd := regionalIndicatorCount%2 == 1
 | 
			
		||||
			if odd {
 | 
			
		||||
				pos += w
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If we fall through all the above rules, it's a grapheme cluster break
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Return token
 | 
			
		||||
	return pos, data[:pos], nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1409
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1409
									
								
								vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										85
									
								
								vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
package iterators
 | 
			
		||||
 | 
			
		||||
type Stringish interface {
 | 
			
		||||
	[]byte | string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SplitFunc[T Stringish] func(T, bool) (int, T, error)
 | 
			
		||||
 | 
			
		||||
// Iterator is a generic iterator for words that are either []byte or string.
 | 
			
		||||
// Iterate while Next() is true, and access the word via Value().
 | 
			
		||||
type Iterator[T Stringish] struct {
 | 
			
		||||
	split SplitFunc[T]
 | 
			
		||||
	data  T
 | 
			
		||||
	start int
 | 
			
		||||
	pos   int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new Iterator for the given data and SplitFunc.
 | 
			
		||||
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
 | 
			
		||||
	return &Iterator[T]{
 | 
			
		||||
		split: split,
 | 
			
		||||
		data:  data,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetText sets the text for the iterator to operate on, and resets all state.
 | 
			
		||||
func (iter *Iterator[T]) SetText(data T) {
 | 
			
		||||
	iter.data = data
 | 
			
		||||
	iter.start = 0
 | 
			
		||||
	iter.pos = 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Split sets the SplitFunc for the Iterator.
 | 
			
		||||
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
 | 
			
		||||
	iter.split = split
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Next advances the iterator to the next token. It returns false when there
 | 
			
		||||
// are no remaining tokens or an error occurred.
 | 
			
		||||
func (iter *Iterator[T]) Next() bool {
 | 
			
		||||
	if iter.pos == len(iter.data) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if iter.pos > len(iter.data) {
 | 
			
		||||
		panic("SplitFunc advanced beyond the end of the data")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	iter.start = iter.pos
 | 
			
		||||
 | 
			
		||||
	advance, _, err := iter.split(iter.data[iter.pos:], true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	if advance <= 0 {
 | 
			
		||||
		panic("SplitFunc returned a zero or negative advance")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	iter.pos += advance
 | 
			
		||||
	if iter.pos > len(iter.data) {
 | 
			
		||||
		panic("SplitFunc advanced beyond the end of the data")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Value returns the current token.
 | 
			
		||||
func (iter *Iterator[T]) Value() T {
 | 
			
		||||
	return iter.data[iter.start:iter.pos]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Start returns the byte position of the current token in the original data.
 | 
			
		||||
func (iter *Iterator[T]) Start() int {
 | 
			
		||||
	return iter.start
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// End returns the byte position after the current token in the original data.
 | 
			
		||||
func (iter *Iterator[T]) End() int {
 | 
			
		||||
	return iter.pos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Reset resets the iterator to the beginning of the data.
 | 
			
		||||
func (iter *Iterator[T]) Reset() {
 | 
			
		||||
	iter.start = 0
 | 
			
		||||
	iter.pos = 0
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								vendor/github.com/cyphar/filepath-securejoin/.golangci.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								vendor/github.com/cyphar/filepath-securejoin/.golangci.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
# SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
 | 
			
		||||
# Copyright (C) 2025 SUSE LLC
 | 
			
		||||
#
 | 
			
		||||
# This Source Code Form is subject to the terms of the Mozilla Public
 | 
			
		||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
			
		||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
 | 
			
		||||
 | 
			
		||||
version: "2"
 | 
			
		||||
 | 
			
		||||
linters:
 | 
			
		||||
  enable:
 | 
			
		||||
    - asasalint
 | 
			
		||||
    - asciicheck
 | 
			
		||||
    - containedctx
 | 
			
		||||
    - contextcheck
 | 
			
		||||
    - errcheck
 | 
			
		||||
    - errorlint
 | 
			
		||||
    - exhaustive
 | 
			
		||||
    - forcetypeassert
 | 
			
		||||
    - godot
 | 
			
		||||
    - goprintffuncname
 | 
			
		||||
    - govet
 | 
			
		||||
    - importas
 | 
			
		||||
    - ineffassign
 | 
			
		||||
    - makezero
 | 
			
		||||
    - misspell
 | 
			
		||||
    - musttag
 | 
			
		||||
    - nilerr
 | 
			
		||||
    - nilnesserr
 | 
			
		||||
    - nilnil
 | 
			
		||||
    - noctx
 | 
			
		||||
    - prealloc
 | 
			
		||||
    - revive
 | 
			
		||||
    - staticcheck
 | 
			
		||||
    - testifylint
 | 
			
		||||
    - unconvert
 | 
			
		||||
    - unparam
 | 
			
		||||
    - unused
 | 
			
		||||
    - usetesting
 | 
			
		||||
  settings:
 | 
			
		||||
    govet:
 | 
			
		||||
      enable:
 | 
			
		||||
        - nilness
 | 
			
		||||
    testifylint:
 | 
			
		||||
      enable-all: true
 | 
			
		||||
 | 
			
		||||
formatters:
 | 
			
		||||
  enable:
 | 
			
		||||
    - gofumpt
 | 
			
		||||
    - goimports
 | 
			
		||||
  settings:
 | 
			
		||||
    goimports:
 | 
			
		||||
      local-prefixes:
 | 
			
		||||
        - github.com/cyphar/filepath-securejoin
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user