diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..660f54c --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,32 @@ +name: Integration Tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + contents: read + +env: + # Disable Ryuk - GitHub Actions cleans up containers automatically + TESTCONTAINERS_RYUK_DISABLED: true + +jobs: + integration: + name: Integration Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version-file: go.mod + cache: true + + - name: Run integration tests + run: go test -v -tags=integration -race -timeout=10m ./integration/... diff --git a/cmd/cat.go b/cmd/cat.go index d138de7..c47c70f 100644 --- a/cmd/cat.go +++ b/cmd/cat.go @@ -42,7 +42,7 @@ func runCat(cmd *cobra.Command, args []string) error { resolvedRef := cfg.ResolveAlias(inputRef) // 4. Create client (lazy - only downloads manifest + index) - client, err := blob.NewClient(blob.WithDockerConfig()) + client, err := newClient(cfg) if err != nil { return fmt.Errorf("creating client: %w", err) } diff --git a/cmd/client.go b/cmd/client.go new file mode 100644 index 0000000..1feff2f --- /dev/null +++ b/cmd/client.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/meigma/blob" + + internalcfg "github.com/meigma/blob-cli/internal/config" +) + +// newClient creates a new blob client with options from config. +func newClient(cfg *internalcfg.Config, opts ...blob.Option) (*blob.Client, error) { + allOpts := append(clientOpts(cfg), opts...) + return blob.NewClient(allOpts...) +} + +// clientOpts returns the base client options from config. +// This is useful when passing options to functions that create their own client. +func clientOpts(cfg *internalcfg.Config) []blob.Option { + opts := []blob.Option{blob.WithDockerConfig()} + if cfg.PlainHTTP { + opts = append(opts, blob.WithPlainHTTP(true)) + } + return opts +} diff --git a/cmd/cp.go b/cmd/cp.go index eae5fcf..5ac8b86 100644 --- a/cmd/cp.go +++ b/cmd/cp.go @@ -107,7 +107,7 @@ func runCp(cmd *cobra.Command, args []string) error { resolvedSources := make([]cpResolvedSource, 0, len(sources)) for _, src := range sources { - rsrc, resolveErr := resolveSource(ctx, src, archiveCache) + rsrc, resolveErr := resolveSource(ctx, cfg, src, archiveCache) if resolveErr != nil { return resolveErr } @@ -148,11 +148,11 @@ func runCp(cmd *cobra.Command, args []string) error { } // resolveSource pulls the archive (if not cached) and detects if the source is a file or directory. -func resolveSource(ctx context.Context, src cpSource, cache map[string]*blob.Archive) (cpResolvedSource, error) { +func resolveSource(ctx context.Context, cfg *internalcfg.Config, src cpSource, cache map[string]*blob.Archive) (cpResolvedSource, error) { // Get or create archive for this ref blobArchive, ok := cache[src.ref] if !ok { - client, clientErr := blob.NewClient(blob.WithDockerConfig()) + client, clientErr := newClient(cfg) if clientErr != nil { return cpResolvedSource{}, fmt.Errorf("creating client: %w", clientErr) } diff --git a/cmd/inspect.go b/cmd/inspect.go index 9160c84..aa2fada 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -78,7 +78,7 @@ func runInspect(cmd *cobra.Command, args []string) error { inputRef := args[0] resolvedRef := cfg.ResolveAlias(inputRef) - result, err := archive.Inspect(cmd.Context(), resolvedRef) + result, err := archive.Inspect(cmd.Context(), resolvedRef, clientOpts(cfg)...) if err != nil { return err } diff --git a/cmd/ls.go b/cmd/ls.go index a229e9b..388ed12 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -80,7 +80,7 @@ func runLs(cmd *cobra.Command, args []string) error { return err } - result, err := archive.Inspect(cmd.Context(), ref) + result, err := archive.Inspect(cmd.Context(), ref, clientOpts(cfg)...) if err != nil { return err } diff --git a/cmd/pull.go b/cmd/pull.go index b04719e..bba7c06 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -95,11 +95,11 @@ func runPull(cmd *cobra.Command, args []string) error { } // 6. Create client with policies - clientOpts := []blob.Option{blob.WithDockerConfig()} + policyOpts := make([]blob.Option, 0, len(policies)) for _, p := range policies { - clientOpts = append(clientOpts, blob.WithPolicy(p)) + policyOpts = append(policyOpts, blob.WithPolicy(p)) } - client, err := blob.NewClient(clientOpts...) + client, err := newClient(cfg, policyOpts...) if err != nil { return fmt.Errorf("creating client: %w", err) } diff --git a/cmd/push.go b/cmd/push.go index 3cf048b..ba7934d 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -74,7 +74,7 @@ func runPush(cmd *cobra.Command, args []string) error { return err } - client, err := blob.NewClient(blob.WithDockerConfig()) + client, err := newClient(cfg) if err != nil { return fmt.Errorf("creating client: %w", err) } diff --git a/cmd/root.go b/cmd/root.go index 542ea08..22c9f48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,6 +56,7 @@ func init() { rootCmd.PersistentFlags().CountP("verbose", "v", "increase verbosity (can be repeated: -vv, -vvv)") rootCmd.PersistentFlags().BoolP("quiet", "q", false, "suppress non-error output") rootCmd.PersistentFlags().Bool("no-color", false, "disable colored output") + rootCmd.PersistentFlags().Bool("plain-http", false, "use plain HTTP instead of HTTPS for registries") // Bind flags to Viper // Note: "config" is NOT bound to Viper to avoid BLOB_CONFIG env var affecting @@ -64,6 +65,7 @@ func init() { viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet")) viper.BindPFlag("no-color", rootCmd.PersistentFlags().Lookup("no-color")) + viper.BindPFlag("plain-http", rootCmd.PersistentFlags().Lookup("plain-http")) // Add core commands rootCmd.AddCommand(pushCmd) diff --git a/cmd/sign.go b/cmd/sign.go index bf44c53..6fb7b30 100644 --- a/cmd/sign.go +++ b/cmd/sign.go @@ -8,7 +8,6 @@ import ( "os" "strings" - "github.com/meigma/blob" "github.com/meigma/blob/policy/sigstore" "github.com/meigma/blob/registry/oras" "github.com/spf13/cobra" @@ -90,7 +89,7 @@ func runSign(cmd *cobra.Command, args []string) error { } // Normal mode: sign and upload - client, err := blob.NewClient(blob.WithDockerConfig()) + client, err := newClient(cfg) if err != nil { return fmt.Errorf("creating client: %w", err) } diff --git a/cmd/tag.go b/cmd/tag.go index 0cbe679..8f61322 100644 --- a/cmd/tag.go +++ b/cmd/tag.go @@ -6,7 +6,6 @@ import ( "fmt" "os" - "github.com/meigma/blob" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -49,7 +48,7 @@ func runTag(cmd *cobra.Command, args []string) error { resolvedSrcRef := cfg.ResolveAlias(srcRef) resolvedDstRef := cfg.ResolveAlias(dstRef) - client, err := blob.NewClient(blob.WithDockerConfig()) + client, err := newClient(cfg) if err != nil { return fmt.Errorf("creating client: %w", err) } diff --git a/cmd/tree.go b/cmd/tree.go index 3ad4312..1244c98 100644 --- a/cmd/tree.go +++ b/cmd/tree.go @@ -71,7 +71,7 @@ func runTree(cmd *cobra.Command, args []string) error { return err } - result, err := archive.Inspect(cmd.Context(), ref) + result, err := archive.Inspect(cmd.Context(), ref, clientOpts(cfg)...) if err != nil { return err } diff --git a/cmd/verify.go b/cmd/verify.go index 0d33160..55c75eb 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -108,7 +108,7 @@ func runVerify(cmd *cobra.Command, args []string) error { // 7. Handle no-policies case if len(policies) == 0 { // No policies - vacuous success with warning - inspectResult, inspectErr := archive.Inspect(cmd.Context(), resolvedRef) + inspectResult, inspectErr := archive.Inspect(cmd.Context(), resolvedRef, clientOpts(cfg)...) if inspectErr != nil { return fmt.Errorf("inspecting archive: %w", inspectErr) } @@ -129,11 +129,11 @@ func runVerify(cmd *cobra.Command, args []string) error { } // 8. Create client with policies for verification - clientOpts := []blob.Option{blob.WithDockerConfig()} + policyOpts := make([]blob.Option, 0, len(policies)) for _, p := range policies { - clientOpts = append(clientOpts, blob.WithPolicy(p)) + policyOpts = append(policyOpts, blob.WithPolicy(p)) } - client, err := blob.NewClient(clientOpts...) + client, err := newClient(cfg, policyOpts...) if err != nil { return fmt.Errorf("creating client: %w", err) } diff --git a/go.mod b/go.mod index 8d7a9f7..85f1b52 100644 --- a/go.mod +++ b/go.mod @@ -3,34 +3,62 @@ module github.com/meigma/blob-cli go 1.25.5 require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 github.com/meigma/blob v1.1.0 github.com/meigma/blob/policy/opa v0.0.0-20260121212824-972ce5f91c94 github.com/meigma/blob/policy/sigstore v0.0.0-20260121212824-972ce5f91c94 github.com/meigma/blob/policy/slsa v0.0.0-20260121212824-972ce5f91c94 + github.com/rogpeppe/go-internal v1.14.1 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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/blang/semver v3.5.1+incompatible // 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.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.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 v1.0.0-rc.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/analysis v0.24.1 // indirect github.com/go-openapi/errors v0.22.6 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect @@ -75,6 +103,23 @@ require ( github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // 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/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + 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/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/open-policy-agent/opa v1.12.3 // indirect @@ -84,16 +129,19 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect github.com/sigstore/rekor v1.5.0 // indirect github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect @@ -109,15 +157,20 @@ require ( github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.3.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/vektah/gqlparser/v2 v2.5.31 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver v1.17.6 // 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.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/sdk v1.39.0 // indirect @@ -132,6 +185,7 @@ require ( golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect google.golang.org/grpc v1.78.0 // indirect diff --git a/go.sum b/go.sum index 63b4b98..a16fd47 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= @@ -44,6 +46,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +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.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= @@ -76,6 +80,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= @@ -88,6 +94,20 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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/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.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -105,6 +125,8 @@ github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSw github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= @@ -138,6 +160,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -232,6 +256,7 @@ github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEz github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= @@ -324,6 +349,8 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/letsencrypt/boulder v0.20251110.0 h1:J8MnKICeilO91dyQ2n5eBbab24neHzUpYMUIOdOtbjc= github.com/letsencrypt/boulder v0.20251110.0/go.mod h1:ogKCJQwll82m7OVHWyTuf8eeFCjuzdRQlgnZcCl0V+8= +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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= @@ -332,6 +359,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/meigma/blob v1.1.0 h1:8aAN+SmGFvm5meWNND2XoCyqyrp8zFHcJVzts4SdFBA= github.com/meigma/blob v1.1.0/go.mod h1:/vMKwvjRGVuTzvykLUeWRZ4yHjm02mjp3HrAa7kpLOQ= github.com/meigma/blob/policy/opa v0.0.0-20260121212824-972ce5f91c94 h1:+f/FSoBWnpjGpV+HvofNBkrROUWwR9EgpB66f0zXJXs= @@ -352,6 +383,8 @@ github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -362,6 +395,12 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= @@ -395,6 +434,9 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -452,6 +494,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -495,6 +539,8 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= @@ -561,8 +607,15 @@ golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= @@ -573,6 +626,7 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4= @@ -593,6 +647,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= diff --git a/integration/helpers.go b/integration/helpers.go new file mode 100644 index 0000000..0f3ca67 --- /dev/null +++ b/integration/helpers.go @@ -0,0 +1,25 @@ +//go:build integration + +package integration + +import ( + "fmt" + "sync/atomic" + + "github.com/rogpeppe/go-internal/testscript" +) + +var tagCounter uint64 + +// cmdGenTag generates unique tag for test isolation. +// Usage: gentag VARNAME +func cmdGenTag(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("gentag does not support negation") + } + if len(args) != 1 { + ts.Fatalf("usage: gentag VARNAME") + } + tag := fmt.Sprintf("t%d", atomic.AddUint64(&tagCounter, 1)) + ts.Setenv(args[0], tag) +} diff --git a/integration/integration_test.go b/integration/integration_test.go new file mode 100644 index 0000000..eb99dd2 --- /dev/null +++ b/integration/integration_test.go @@ -0,0 +1,77 @@ +//go:build integration + +package integration + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/rogpeppe/go-internal/testscript" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +var registryHost string + +func TestMain(m *testing.M) { + ctx := context.Background() + + // Start registry container + registry, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "registry:2", + ExposedPorts: []string{"5000/tcp"}, + WaitingFor: wait.ForHTTP("/v2/").WithPort("5000/tcp").WithStartupTimeout(30 * time.Second), + Env: map[string]string{ + "REGISTRY_STORAGE_DELETE_ENABLED": "true", + }, + }, + Started: true, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to start registry: %v\n", err) + os.Exit(1) + } + + host, err := registry.Host(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get registry host: %v\n", err) + registry.Terminate(ctx) + os.Exit(1) + } + + port, err := registry.MappedPort(ctx, "5000") + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get registry port: %v\n", err) + registry.Terminate(ctx) + os.Exit(1) + } + + registryHost = fmt.Sprintf("%s:%s", host, port.Port()) + + // Run testscript + exitCode := testscript.RunMain(m, map[string]func() int{ + "blob": run, + }) + + registry.Terminate(ctx) + os.Exit(exitCode) +} + +func TestCLI(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata/scripts", + Setup: func(env *testscript.Env) error { + env.Setenv("REGISTRY", registryHost) + env.Setenv("XDG_CONFIG_HOME", env.WorkDir+"/.config") + env.Setenv("XDG_CACHE_HOME", env.WorkDir+"/.cache") + return copyTestData(env.WorkDir) + }, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "gentag": cmdGenTag, + }, + }) +} diff --git a/integration/testdata.go b/integration/testdata.go new file mode 100644 index 0000000..66c7d10 --- /dev/null +++ b/integration/testdata.go @@ -0,0 +1,69 @@ +//go:build integration + +package integration + +import ( + "embed" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/meigma/blob-cli/cmd" +) + +//go:embed testdata/sample-project +var sampleProject embed.FS + +// run is the main entry point for the CLI, returning an exit code. +func run() int { + if err := cmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + + var exitErr *cmd.ExitError + if errors.As(err, &exitErr) { + return exitErr.Code + } + + return 1 + } + return 0 +} + +// copyTestData copies the embedded sample-project to the given work directory. +func copyTestData(workDir string) error { + return fs.WalkDir(sampleProject, "testdata/sample-project", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Calculate relative path from "testdata/sample-project" + relPath, err := filepath.Rel("testdata/sample-project", path) + if err != nil { + return err + } + + // Skip the root directory itself + if relPath == "." { + return nil + } + + destPath := filepath.Join(workDir, "sample-project", relPath) + + if d.IsDir() { + return os.MkdirAll(destPath, 0755) + } + + data, err := sampleProject.ReadFile(path) + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return err + } + + return os.WriteFile(destPath, data, 0644) + }) +} diff --git a/integration/testdata/sample-project/README.md b/integration/testdata/sample-project/README.md new file mode 100644 index 0000000..ca108c4 --- /dev/null +++ b/integration/testdata/sample-project/README.md @@ -0,0 +1,8 @@ +# Sample Project + +A sample project for testing blob-cli commands. + +## Structure +- `config/` - Configuration files +- `src/` - Source code +- `docs/` - Documentation diff --git a/integration/testdata/sample-project/config/app.yaml b/integration/testdata/sample-project/config/app.yaml new file mode 100644 index 0000000..3cca689 --- /dev/null +++ b/integration/testdata/sample-project/config/app.yaml @@ -0,0 +1,9 @@ +name: sample-app +version: 1.0.0 +environment: production +features: + caching: true + logging: verbose +database: + host: localhost + port: 5432 diff --git a/integration/testdata/sample-project/config/database.json b/integration/testdata/sample-project/config/database.json new file mode 100644 index 0000000..88ea42c --- /dev/null +++ b/integration/testdata/sample-project/config/database.json @@ -0,0 +1,12 @@ +{ + "connection": { + "host": "db.example.com", + "port": 5432, + "database": "myapp", + "ssl": true + }, + "pool": { + "min": 5, + "max": 20 + } +} diff --git a/integration/testdata/sample-project/docs/api/reference.md b/integration/testdata/sample-project/docs/api/reference.md new file mode 100644 index 0000000..315401e --- /dev/null +++ b/integration/testdata/sample-project/docs/api/reference.md @@ -0,0 +1,9 @@ +# API Reference + +## Endpoints + +### GET /health +Returns the health status. + +### GET /version +Returns version information. diff --git a/integration/testdata/sample-project/docs/guide.md b/integration/testdata/sample-project/docs/guide.md new file mode 100644 index 0000000..50a82f3 --- /dev/null +++ b/integration/testdata/sample-project/docs/guide.md @@ -0,0 +1,7 @@ +# User Guide + +## Getting Started + +1. Install the application +2. Configure settings +3. Run the service diff --git a/integration/testdata/sample-project/src/main.go b/integration/testdata/sample-project/src/main.go new file mode 100644 index 0000000..8428fb3 --- /dev/null +++ b/integration/testdata/sample-project/src/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, blob-cli!") +} diff --git a/integration/testdata/sample-project/src/utils/helpers.go b/integration/testdata/sample-project/src/utils/helpers.go new file mode 100644 index 0000000..2eaa183 --- /dev/null +++ b/integration/testdata/sample-project/src/utils/helpers.go @@ -0,0 +1,6 @@ +package utils + +// Helper provides utility functions. +func Helper() string { + return "helping" +} diff --git a/integration/testdata/scripts/alias_workflow.txtar b/integration/testdata/scripts/alias_workflow.txtar new file mode 100644 index 0000000..492442b --- /dev/null +++ b/integration/testdata/scripts/alias_workflow.txtar @@ -0,0 +1,8 @@ +# Test alias list/set/remove +exec blob alias set mytest $REGISTRY/alias-test +exec blob alias list +stdout 'mytest' + +exec blob alias remove mytest +exec blob alias list +! stdout 'mytest' diff --git a/integration/testdata/scripts/cache_commands.txtar b/integration/testdata/scripts/cache_commands.txtar new file mode 100644 index 0000000..3683366 --- /dev/null +++ b/integration/testdata/scripts/cache_commands.txtar @@ -0,0 +1,4 @@ +# Test cache subcommands +# Note: cache commands currently output empty - just verify they run successfully +exec blob cache path +exec blob cache status diff --git a/integration/testdata/scripts/cat_basic.txtar b/integration/testdata/scripts/cat_basic.txtar new file mode 100644 index 0000000..668bbde --- /dev/null +++ b/integration/testdata/scripts/cat_basic.txtar @@ -0,0 +1,8 @@ +# Test cat outputs file contents +gentag TAG +exec blob --plain-http push $REGISTRY/cat:$TAG sample-project +exec blob --plain-http cat $REGISTRY/cat:$TAG /config/app.yaml + +stdout 'name: sample-app' +stdout 'version: 1\.0\.0' +stdout 'environment: production' diff --git a/integration/testdata/scripts/cat_multiple.txtar b/integration/testdata/scripts/cat_multiple.txtar new file mode 100644 index 0000000..5337e50 --- /dev/null +++ b/integration/testdata/scripts/cat_multiple.txtar @@ -0,0 +1,7 @@ +# Test cat with multiple files concatenated +gentag TAG +exec blob --plain-http push $REGISTRY/cat-multi:$TAG sample-project +exec blob --plain-http cat $REGISTRY/cat-multi:$TAG /README.md /config/app.yaml + +stdout 'Sample Project' +stdout 'name: sample-app' diff --git a/integration/testdata/scripts/config_commands.txtar b/integration/testdata/scripts/config_commands.txtar new file mode 100644 index 0000000..612b048 --- /dev/null +++ b/integration/testdata/scripts/config_commands.txtar @@ -0,0 +1,6 @@ +# Test config subcommands +exec blob config path +stdout 'config\.yaml' + +exec blob config show +stdout 'output:' diff --git a/integration/testdata/scripts/cp_directory.txtar b/integration/testdata/scripts/cp_directory.txtar new file mode 100644 index 0000000..174457a --- /dev/null +++ b/integration/testdata/scripts/cp_directory.txtar @@ -0,0 +1,10 @@ +# Test cp copies directory recursively +gentag TAG +exec blob --plain-http push $REGISTRY/cp-dir:$TAG sample-project + +mkdir output +exec blob --plain-http cp $REGISTRY/cp-dir:$TAG:/config/ output/ + +# Files are copied with directory structure preserved +exists output/config/app.yaml +exists output/config/database.json diff --git a/integration/testdata/scripts/cp_file.txtar b/integration/testdata/scripts/cp_file.txtar new file mode 100644 index 0000000..5265505 --- /dev/null +++ b/integration/testdata/scripts/cp_file.txtar @@ -0,0 +1,7 @@ +# Test cp copies single file +gentag TAG +exec blob --plain-http push $REGISTRY/cp:$TAG sample-project +exec blob --plain-http cp $REGISTRY/cp:$TAG:/config/app.yaml ./copied.yaml + +exists copied.yaml +cmp copied.yaml sample-project/config/app.yaml diff --git a/integration/testdata/scripts/cp_multiple.txtar b/integration/testdata/scripts/cp_multiple.txtar new file mode 100644 index 0000000..52d74bb --- /dev/null +++ b/integration/testdata/scripts/cp_multiple.txtar @@ -0,0 +1,10 @@ +# Test cp copies multiple files +gentag TAG +exec blob --plain-http push $REGISTRY/cp-multi:$TAG sample-project + +mkdir output +exec blob --plain-http cp $REGISTRY/cp-multi:$TAG:/config/app.yaml $REGISTRY/cp-multi:$TAG:/config/database.json output/ + +# Files are copied with directory structure preserved +exists output/config/app.yaml +exists output/config/database.json diff --git a/integration/testdata/scripts/error_handling.txtar b/integration/testdata/scripts/error_handling.txtar new file mode 100644 index 0000000..bcc5935 --- /dev/null +++ b/integration/testdata/scripts/error_handling.txtar @@ -0,0 +1,11 @@ +# Test error cases +! exec blob push +stderr 'accepts 2 arg' + +! exec blob --plain-http cat $REGISTRY/nonexistent:v1 /file.txt +stderr 'not found' + +gentag TAG +exec blob --plain-http push $REGISTRY/err:$TAG sample-project +! exec blob --plain-http cat $REGISTRY/err:$TAG /nonexistent.txt +stderr 'not found' diff --git a/integration/testdata/scripts/inspect_basic.txtar b/integration/testdata/scripts/inspect_basic.txtar new file mode 100644 index 0000000..5a2bcae --- /dev/null +++ b/integration/testdata/scripts/inspect_basic.txtar @@ -0,0 +1,10 @@ +# Test inspect shows metadata +gentag TAG +exec blob --plain-http push $REGISTRY/inspect:$TAG sample-project +exec blob --plain-http inspect $REGISTRY/inspect:$TAG + +stdout 'Reference:' +stdout 'Digest:' +stdout 'Files:' +stdout 'Size:' +stdout 'Compression:' diff --git a/integration/testdata/scripts/inspect_json.txtar b/integration/testdata/scripts/inspect_json.txtar new file mode 100644 index 0000000..cd9918e --- /dev/null +++ b/integration/testdata/scripts/inspect_json.txtar @@ -0,0 +1,9 @@ +# Test inspect JSON output +gentag TAG +exec blob --plain-http push $REGISTRY/inspect-json:$TAG sample-project +exec blob --plain-http inspect --output json $REGISTRY/inspect-json:$TAG + +stdout '"ref":' +stdout '"digest":' +stdout '"files":' +stdout '"size":' diff --git a/integration/testdata/scripts/json_output.txtar b/integration/testdata/scripts/json_output.txtar new file mode 100644 index 0000000..b76265f --- /dev/null +++ b/integration/testdata/scripts/json_output.txtar @@ -0,0 +1,11 @@ +# Test JSON output for multiple commands +gentag TAG +exec blob --plain-http push $REGISTRY/json:$TAG sample-project + +exec blob --plain-http ls --output json $REGISTRY/json:$TAG +stdout '"entries":' + +exec blob --plain-http tree --output json $REGISTRY/json:$TAG +stdout '"root":' +stdout '"directory_count":' +stdout '"file_count":' diff --git a/integration/testdata/scripts/ls_basic.txtar b/integration/testdata/scripts/ls_basic.txtar new file mode 100644 index 0000000..39265c7 --- /dev/null +++ b/integration/testdata/scripts/ls_basic.txtar @@ -0,0 +1,9 @@ +# Test ls lists directory contents +gentag TAG +exec blob --plain-http push $REGISTRY/ls:$TAG sample-project +exec blob --plain-http ls $REGISTRY/ls:$TAG + +stdout 'README\.md' +stdout 'config/' +stdout 'src/' +stdout 'docs/' diff --git a/integration/testdata/scripts/ls_long.txtar b/integration/testdata/scripts/ls_long.txtar new file mode 100644 index 0000000..c8359c8 --- /dev/null +++ b/integration/testdata/scripts/ls_long.txtar @@ -0,0 +1,7 @@ +# Test ls with long format shows permissions and sizes +gentag TAG +exec blob --plain-http push $REGISTRY/ls-long:$TAG sample-project +exec blob --plain-http ls -lh $REGISTRY/ls-long:$TAG + +stdout '-rw' +stdout 'README\.md' diff --git a/integration/testdata/scripts/ls_subdir.txtar b/integration/testdata/scripts/ls_subdir.txtar new file mode 100644 index 0000000..3baeaed --- /dev/null +++ b/integration/testdata/scripts/ls_subdir.txtar @@ -0,0 +1,8 @@ +# Test ls with subdirectory path +gentag TAG +exec blob --plain-http push $REGISTRY/ls-subdir:$TAG sample-project +exec blob --plain-http ls $REGISTRY/ls-subdir:$TAG /config + +stdout 'app\.yaml' +stdout 'database\.json' +! stdout 'README\.md' diff --git a/integration/testdata/scripts/pull_basic.txtar b/integration/testdata/scripts/pull_basic.txtar new file mode 100644 index 0000000..e943889 --- /dev/null +++ b/integration/testdata/scripts/pull_basic.txtar @@ -0,0 +1,13 @@ +# Test pull extracts files correctly +gentag TAG +exec blob --plain-http push $REGISTRY/pull:$TAG sample-project + +mkdir output +exec blob --plain-http pull $REGISTRY/pull:$TAG output +stdout 'Pulled' + +exists output/README.md +exists output/config/app.yaml +exists output/src/main.go +exists output/docs/api/reference.md +cmp output/config/app.yaml sample-project/config/app.yaml diff --git a/integration/testdata/scripts/pull_to_dir.txtar b/integration/testdata/scripts/pull_to_dir.txtar new file mode 100644 index 0000000..6e21603 --- /dev/null +++ b/integration/testdata/scripts/pull_to_dir.txtar @@ -0,0 +1,11 @@ +# Test pull to specific directory creates correct structure +gentag TAG +exec blob --plain-http push $REGISTRY/pull-dir:$TAG sample-project + +mkdir custom-output +exec blob --plain-http pull $REGISTRY/pull-dir:$TAG custom-output +stdout 'Pulled' + +exists custom-output/README.md +exists custom-output/config/app.yaml +exists custom-output/src/main.go diff --git a/integration/testdata/scripts/push_annotations.txtar b/integration/testdata/scripts/push_annotations.txtar new file mode 100644 index 0000000..0da588e --- /dev/null +++ b/integration/testdata/scripts/push_annotations.txtar @@ -0,0 +1,7 @@ +# Test push with annotations +gentag TAG +exec blob --plain-http push --annotation version=1.0.0 --annotation author=test $REGISTRY/annotated:$TAG sample-project +stdout 'Pushed' +exec blob --plain-http inspect --output json $REGISTRY/annotated:$TAG +stdout '"version".*"1.0.0"' +stdout '"author".*"test"' diff --git a/integration/testdata/scripts/push_basic.txtar b/integration/testdata/scripts/push_basic.txtar new file mode 100644 index 0000000..fc3f7d7 --- /dev/null +++ b/integration/testdata/scripts/push_basic.txtar @@ -0,0 +1,5 @@ +# Test basic push command +gentag TAG +exec blob --plain-http push $REGISTRY/test:$TAG sample-project +stdout 'Pushed' +! stderr . diff --git a/integration/testdata/scripts/push_compression.txtar b/integration/testdata/scripts/push_compression.txtar new file mode 100644 index 0000000..41f7b80 --- /dev/null +++ b/integration/testdata/scripts/push_compression.txtar @@ -0,0 +1,13 @@ +# Test push with compression options +# Note: Small test files may show as "none" even with zstd compression +# because the skip-compressed heuristic prevents compressing very small files +gentag TAG1 +gentag TAG2 + +exec blob --plain-http push $REGISTRY/zstd:$TAG1 sample-project +exec blob --plain-http inspect $REGISTRY/zstd:$TAG1 +stdout 'Compression:' + +exec blob --plain-http push --compression none $REGISTRY/none:$TAG2 sample-project +exec blob --plain-http inspect $REGISTRY/none:$TAG2 +stdout 'Compression:.*none' diff --git a/integration/testdata/scripts/tag_basic.txtar b/integration/testdata/scripts/tag_basic.txtar new file mode 100644 index 0000000..efc790f --- /dev/null +++ b/integration/testdata/scripts/tag_basic.txtar @@ -0,0 +1,10 @@ +# Test tag creates new reference (same repo, different tag) +gentag TAG +exec blob --plain-http push $REGISTRY/tag-test:$TAG sample-project +exec blob --plain-http tag $REGISTRY/tag-test:$TAG $REGISTRY/tag-test:latest + +stdout 'Tagged' +stdout 'Digest:' + +exec blob --plain-http inspect $REGISTRY/tag-test:latest +stdout 'Digest:' diff --git a/integration/testdata/scripts/tree_basic.txtar b/integration/testdata/scripts/tree_basic.txtar new file mode 100644 index 0000000..e0cabf3 --- /dev/null +++ b/integration/testdata/scripts/tree_basic.txtar @@ -0,0 +1,14 @@ +# Test tree shows full hierarchy +gentag TAG +exec blob --plain-http push $REGISTRY/tree:$TAG sample-project +exec blob --plain-http tree $REGISTRY/tree:$TAG + +stdout 'config' +stdout 'src' +stdout 'docs' +stdout 'utils' +stdout 'api' +stdout 'helpers\.go' +stdout 'reference\.md' +stdout 'directories' +stdout 'files' diff --git a/integration/testdata/scripts/tree_dirsfirst.txtar b/integration/testdata/scripts/tree_dirsfirst.txtar new file mode 100644 index 0000000..069f18f --- /dev/null +++ b/integration/testdata/scripts/tree_dirsfirst.txtar @@ -0,0 +1,8 @@ +# Test tree with directories first +gentag TAG +exec blob --plain-http push $REGISTRY/tree-dirs:$TAG sample-project +exec blob --plain-http tree --dirsfirst $REGISTRY/tree-dirs:$TAG + +stdout 'config' +stdout 'src' +stdout 'docs' diff --git a/integration/testdata/scripts/tree_level.txtar b/integration/testdata/scripts/tree_level.txtar new file mode 100644 index 0000000..7d85990 --- /dev/null +++ b/integration/testdata/scripts/tree_level.txtar @@ -0,0 +1,11 @@ +# Test tree with depth limit +gentag TAG +exec blob --plain-http push $REGISTRY/tree-level:$TAG sample-project +exec blob --plain-http tree -L 1 $REGISTRY/tree-level:$TAG + +stdout 'config' +stdout 'src' +stdout 'docs' +stdout 'README\.md' +! stdout 'helpers\.go' +! stdout 'reference\.md' diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 543cca5..347e130 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -31,8 +31,10 @@ type DirEntry struct { } // Inspect fetches archive metadata from a registry without downloading file data. -func Inspect(ctx context.Context, ref string) (*blob.InspectResult, error) { - client, err := blob.NewClient(blob.WithDockerConfig()) +// Additional client options can be passed to customize the client behavior. +func Inspect(ctx context.Context, ref string, opts ...blob.Option) (*blob.InspectResult, error) { + clientOpts := append([]blob.Option{blob.WithDockerConfig()}, opts...) + client, err := blob.NewClient(clientOpts...) if err != nil { return nil, fmt.Errorf("creating client: %w", err) } diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 2da2193..da72530 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -21,6 +21,7 @@ func Default() *Config { Verbose: 0, Quiet: false, NoColor: false, + PlainHTTP: false, Compression: CompressionZstd, Cache: CacheConfig{ Enabled: true, @@ -38,6 +39,7 @@ func SetDefaults(v *viper.Viper) { v.SetDefault("verbose", 0) v.SetDefault("quiet", false) v.SetDefault("no-color", false) + v.SetDefault("plain-http", false) v.SetDefault("compression", CompressionZstd) v.SetDefault("cache.enabled", true) v.SetDefault("cache.max_size", "5GB") diff --git a/internal/config/types.go b/internal/config/types.go index 54ccd5f..6c6cc1e 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -14,6 +14,9 @@ type Config struct { // NoColor disables colored output. NoColor bool `mapstructure:"no-color" json:"no_color"` + // PlainHTTP enables plain HTTP (no TLS) for registries. + PlainHTTP bool `mapstructure:"plain-http" json:"plain_http"` + // Compression type for push: "none" or "zstd". Compression string `mapstructure:"compression" json:"compression"` diff --git a/justfile b/justfile index 91aff09..acd8c38 100644 --- a/justfile +++ b/justfile @@ -30,6 +30,11 @@ test: @echo "Running tests..." go test -race -cover ./... +# Run integration tests (requires Docker) +test-integration: + @echo "Running integration tests..." + go test -v -tags=integration ./integration/... + # Build the binary build: @echo "Building..." diff --git a/main.go b/main.go index 23271db..f82b89d 100644 --- a/main.go +++ b/main.go @@ -9,15 +9,20 @@ import ( ) func main() { + os.Exit(run()) +} + +func run() int { if err := cmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) // Check for specific exit codes var exitErr *cmd.ExitError if errors.As(err, &exitErr) { - os.Exit(exitErr.Code) + return exitErr.Code } - os.Exit(1) + return 1 } + return 0 }