diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33936d1..20a7e71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,19 +23,10 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - name: 'Caches: go' - uses: actions/cache@v4 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: go-${{ runner.os }}-${{ github.job }}-${{ hashFiles(inputs.key-files) }} - restore-keys: | - go-${{ runner.os }}-${{ github.job }}- - go-${{ runner.os }}- - name: Set up Go uses: actions/setup-go@v4 with: + cache: true go-version-file: go.mod - shell: bash run: | diff --git a/cli.go b/cli.go index 2b49094..422866c 100644 --- a/cli.go +++ b/cli.go @@ -3,10 +3,18 @@ package cli import ( "context" "fmt" + "go/build" + "net/url" + "os" + "regexp" "runtime" + "strings" + "github.com/anchordotdev/cli/models" "github.com/anchordotdev/cli/ui" + "github.com/cli/browser" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var Version = struct { @@ -76,7 +84,8 @@ type Config struct { } `cmd:"lcl"` Test struct { - SkipRunE bool `desc:"skip RunE for testing purposes"` + Browserless bool `desc:"run as though browserless"` + SkipRunE bool `desc:"skip RunE for testing purposes"` } Trust struct { @@ -130,3 +139,101 @@ func ConfigFromCmd(cmd *cobra.Command) *Config { func ContextWithConfig(ctx context.Context, cfg *Config) context.Context { return context.WithValue(ctx, ContextKey("Config"), cfg) } + +var ( + stackHexRegexp = regexp.MustCompile(`0x[0-9a-f]{2,}\??`) + stackNilRegexp = regexp.MustCompile(`0x0`) + + stackPathReplacer *strings.Replacer +) + +func init() { + goPaths := strings.Split(os.Getenv("GOPATH"), string(os.PathListSeparator)) + if len(goPaths) == 0 { + goPaths = append(goPaths, build.Default.GOPATH) + } + if goPaths[0] == "" { + goPaths[0] = build.Default.GOPATH + } + + joinedGoPaths := strings.Join(goPaths, ",,") + "," + replacements := strings.Split(joinedGoPaths, ",") + replacements = append(replacements, runtime.GOROOT(), "") + + if pwd, _ := os.Getwd(); pwd != "" { + replacements = append(replacements, pwd, "") + } + + stackPathReplacer = strings.NewReplacer(replacements...) +} + +func normalizeStack(stack string) string { + // TODO: more nuanced replace for other known values like true/false, maybe empty string? + stack = stackPathReplacer.Replace(stack) + stack = stackHexRegexp.ReplaceAllString(stack, "") + stack = stackNilRegexp.ReplaceAllString(stack, "") + stack = strings.TrimRight(stack, "\n") + + lines := strings.Split(stack, "\n") + for i, line := range lines { + if strings.Contains(line, "") { + // for lines like: `/src/runtime/debug/stack.go:24 +` + lines[i] = fmt.Sprintf("%s: +", strings.Split(line, ":")[0]) + } + if strings.Contains(line, "in goroutine") { + lines[i] = fmt.Sprintf("%s in gouroutine ", strings.Split(line, " in goroutine ")[0]) + } + } + + return strings.Join(lines, "\n") +} + +func ReportError(ctx context.Context, drv *ui.Driver, cmd *cobra.Command, args []string, msg any, stack string) { + cfg := ConfigFromContext(ctx) + + var flags []string + cmd.Flags().Visit(func(flag *pflag.Flag) { + flags = append(flags, flag.Name) + }) + + q := url.Values{} + q.Add("title", fmt.Sprintf("Error: %s", msg)) + + var body strings.Builder + + fmt.Fprintf(&body, "**Are there any additional details you would like to share?**\n") + fmt.Fprintf(&body, "\n") + fmt.Fprintf(&body, "---\n") + fmt.Fprintf(&body, "\n") + fmt.Fprintf(&body, "**Command:** `%s`\n", cmd.CalledAs()) + fmt.Fprintf(&body, "**Version:** `%s`\n", VersionString()) + fmt.Fprintf(&body, "**Arguments:** `[%s]`\n", strings.Join(args, ", ")) + fmt.Fprintf(&body, "**Flags:** `[%s]`\n", strings.Join(flags, ", ")) + if stack != "" { + fmt.Fprintf(&body, "**Stack:**\n```\n%s\n```\n", normalizeStack(stack)) + } + fmt.Fprintf(&body, "**Stdout:**\n```\n%s\n```\n", strings.TrimRight(string(drv.FinalOut()), "\n")) + q.Add("body", body.String()) + + reportErrorConfirmCh := make(chan struct{}) + drv.Activate(ctx, &models.ReportError{ + ConfirmCh: reportErrorConfirmCh, + Cmd: cmd, + Args: args, + Msg: msg, + }) + + if !cfg.NonInteractive { + <-reportErrorConfirmCh + } + + newIssueURL := fmt.Sprintf("https://github.com/anchordotdev/cli/issues/new?%s", q.Encode()) + // FIXME: ? remove config val, switch to mocking this to always err in tests + if cfg.Test.Browserless { + drv.Activate(ctx, &models.Browserless{Url: newIssueURL}) + } else { + if err := browser.OpenURL(newIssueURL); err != nil { + drv.Activate(ctx, &models.Browserless{Url: newIssueURL}) + } + } +} diff --git a/cli_test.go b/cli_test.go new file mode 100644 index 0000000..a76ef89 --- /dev/null +++ b/cli_test.go @@ -0,0 +1,103 @@ +package cli_test + +import ( + "context" + "errors" + "fmt" + "runtime" + "testing" + "time" + + "github.com/anchordotdev/cli" + "github.com/anchordotdev/cli/ui" + "github.com/anchordotdev/cli/ui/uitest" + "github.com/charmbracelet/x/exp/teatest" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +var CmdError = cli.NewCmd[ErrorCommand](nil, "error", func(cmd *cobra.Command) {}) + +type ErrorCommand struct{} + +func (c ErrorCommand) UI() cli.UI { + return cli.UI{ + RunTUI: c.run, + } +} + +var testErr = errors.New("test error") + +func (c *ErrorCommand) run(ctx context.Context, drv *ui.Driver) error { + return testErr +} + +func TestError(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := cli.Config{} + cfg.NonInteractive = true + cfg.Test.Browserless = true + ctx = cli.ContextWithConfig(ctx, &cfg) + + t.Run(fmt.Sprintf("golden-%s_%s", runtime.GOOS, runtime.GOARCH), func(t *testing.T) { + var returnedError error + + drv, tm := uitest.TestTUI(ctx, t) + + cmd := ErrorCommand{} + + defer func() { + require.Error(t, returnedError) + require.EqualError(t, returnedError, testErr.Error()) + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) + teatest.RequireEqualOutput(t, drv.FinalOut()) + }() + defer cli.Cleanup(&returnedError, nil, ctx, drv, CmdError, []string{}) + returnedError = cmd.UI().RunTUI(ctx, drv) + }) +} + +var CmdPanic = cli.NewCmd[PanicCommand](nil, "error", func(cmd *cobra.Command) {}) + +type PanicCommand struct{} + +func (c PanicCommand) UI() cli.UI { + return cli.UI{ + RunTUI: c.run, + } +} + +func (c *PanicCommand) run(ctx context.Context, drv *ui.Driver) error { + panic("test panic") +} + +func TestPanic(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg := cli.Config{} + cfg.NonInteractive = true + cfg.Test.Browserless = true + ctx = cli.ContextWithConfig(ctx, &cfg) + + t.Run(fmt.Sprintf("golden-%s_%s", runtime.GOOS, runtime.GOARCH), func(t *testing.T) { + var returnedError error + + drv, tm := uitest.TestTUI(ctx, t) + + cmd := PanicCommand{} + + defer func() { + require.Error(t, returnedError) + require.EqualError(t, returnedError, "test panic") + + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*3)) + teatest.RequireEqualOutput(t, drv.FinalOut()) + }() + defer cli.Cleanup(&returnedError, nil, ctx, drv, CmdPanic, []string{}) + _ = cmd.UI().RunTUI(ctx, drv) + }) +} diff --git a/cmd.go b/cmd.go index 92acf55..8d8d873 100644 --- a/cmd.go +++ b/cmd.go @@ -2,6 +2,9 @@ package cli import ( "context" + "fmt" + "runtime/debug" + "strings" "github.com/MakeNowJust/heredoc" "github.com/anchordotdev/cli/ui" @@ -182,8 +185,6 @@ var cmdDefByCommands = map[*cobra.Command]*CmdDef{} var constructorByCommands = map[*cobra.Command]func() *cobra.Command{} -var version string - type UIer interface { UI() UI } @@ -216,11 +217,12 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) } cmd := &cobra.Command{ - Use: def.Use, - Args: def.Args, - Short: def.Short, - Long: def.Long, - SilenceUsage: true, + Use: def.Use, + Args: def.Args, + Short: def.Short, + Long: def.Long, + SilenceErrors: true, + SilenceUsage: true, } ctx := ContextWithConfig(context.Background(), cfg) @@ -245,7 +247,7 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) panic(err) } - cmd.RunE = func(cmd *cobra.Command, args []string) error { + cmd.RunE = func(cmd *cobra.Command, args []string) (returnedError error) { cfg := ConfigFromCmd(cmd) if cfg.Test.SkipRunE { return nil @@ -263,6 +265,7 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) defer cancel(nil) drv, prg := ui.NewDriverTUI(ctx) + errc := make(chan error) go func() { @@ -274,16 +277,12 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) errc <- err }() + defer Cleanup(&returnedError, errc, ctx, drv, cmd, args) if err := t.UI().RunTUI(ctx, drv); err != nil && err != context.Canceled { - prg.Quit() - - <-errc // TODO: report this somewhere? return err } - prg.Quit() - - return <-errc // TODO: special handling for a UI error + return nil } return cmd @@ -298,6 +297,32 @@ func NewCmd[T UIer](parent *cobra.Command, name string, fn func(*cobra.Command)) return cmd } +func Cleanup(returnedError *error, errc chan error, ctx context.Context, drv *ui.Driver, cmd *cobra.Command, args []string) { + var stack string + + panicMsg := recover() + if panicMsg != nil { + lines := strings.Split(string(debug.Stack()), "\n") + // omit lines: 0 go routine, 2-3 stack call, 4-5 cleanup call + stack = strings.Join(lines[5:], "\n") + + *returnedError = fmt.Errorf("%s", panicMsg) + } + + if *returnedError != nil { + ReportError(ctx, drv, cmd, args, *returnedError, stack) + } + + // release/restore + drv.Program.Quit() + drv.Program.Wait() + + // FIXME: handle UI errors? + if errc != nil { + *returnedError = <-errc + } +} + func NewTestCmd(cmd *cobra.Command) *cobra.Command { return constructorByCommands[cmd]() } diff --git a/go.mod b/go.mod index 551ed74..f07b6c8 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,13 @@ require ( github.com/muesli/termenv v0.15.2 github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/cobra v1.8.0 - github.com/stretchr/testify v1.8.4 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.9.0 github.com/zalando/go-keyring v0.2.4 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.22.0 golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 - golang.org/x/sync v0.6.0 - golang.org/x/sys v0.18.0 + golang.org/x/sync v0.7.0 + golang.org/x/sys v0.19.0 howett.net/plist v1.0.1 ) @@ -63,11 +64,10 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect - github.com/spf13/pflag v1.0.5 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.19.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 6c60075..c661999 100644 --- a/go.sum +++ b/go.sum @@ -135,14 +135,15 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= @@ -153,8 +154,8 @@ github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9 github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= @@ -171,8 +172,8 @@ golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -181,12 +182,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/models/cli.go b/models/cli.go new file mode 100644 index 0000000..0246b71 --- /dev/null +++ b/models/cli.go @@ -0,0 +1,73 @@ +package models + +import ( + "fmt" + "strings" + + "github.com/anchordotdev/cli/ui" + tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/cobra" +) + +type ReportError struct { + ConfirmCh chan<- struct{} + + Cmd *cobra.Command + Args []string + Msg any +} + +func (m *ReportError) Init() tea.Cmd { return nil } + +func (m *ReportError) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEnter: + if m.ConfirmCh != nil { + close(m.ConfirmCh) + m.ConfirmCh = nil + } + } + } + + return m, nil +} + +func (m *ReportError) View() string { + var b strings.Builder + + fmt.Fprintln(&b, ui.Header(fmt.Sprintf("%s %s %s", + ui.Error("Error!"), + m.Msg, + ui.Whisper(fmt.Sprintf("`%s`", m.Cmd.CalledAs())), + ))) + + fmt.Fprintln(&b, ui.StepHint("We are sorry you encountered this error.")) + + if m.ConfirmCh != nil { + fmt.Fprintln(&b, ui.StepAlert(fmt.Sprintf("%s to open an issue on Github.", + ui.Action("Press Enter"), + ))) + } else { + fmt.Fprintln(&b, ui.StepDone("Opened an issue on Github.")) + } + + return b.String() +} + +type Browserless struct { + Url string +} + +func (m *Browserless) Init() tea.Cmd { return nil } + +func (m *Browserless) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + +func (m *Browserless) View() string { + var b strings.Builder + + fmt.Fprintln(&b, ui.StepAlert(fmt.Sprintf("Unable to open browser. Please open this URL in a browser to continue: %s", m.Url))) + + return b.String() +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9c6b7e2 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "anchor", + "private": true, + "dependencies": {}, + "scripts": {}, + "devDependencies": { + "@stoplight/prism-cli": "5.7.0" + } +} diff --git a/root.go b/root.go index 314b9f8..749a292 100644 --- a/root.go +++ b/root.go @@ -6,6 +6,8 @@ import ( var CmdRoot = NewCmd[ShowHelp](nil, "anchor", func(cmd *cobra.Command) {}) +// ShowHelp calls cmd.HelpFunc() inside RunE instead of RunTUI + type ShowHelp struct{} func (c ShowHelp) UI() UI { diff --git a/testdata/TestError/golden-darwin_arm64.golden b/testdata/TestError/golden-darwin_arm64.golden new file mode 100644 index 0000000..6b4e251 --- /dev/null +++ b/testdata/TestError/golden-darwin_arm64.golden @@ -0,0 +1,9 @@ +─── ReportError ──────────────────────────────────────────────────────────────── +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +─── Browserless ──────────────────────────────────────────────────────────────── +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. + ! Unable to open browser. Please open this URL in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28darwin%2Farm64%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%0A%60%60%60%0A&title=Error%3A+test+error diff --git a/testdata/TestError/golden-linux_amd64.golden b/testdata/TestError/golden-linux_amd64.golden new file mode 100644 index 0000000..9857458 --- /dev/null +++ b/testdata/TestError/golden-linux_amd64.golden @@ -0,0 +1,9 @@ +─── ReportError ──────────────────────────────────────────────────────────────── +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +─── Browserless ──────────────────────────────────────────────────────────────── +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. + ! Unable to open browser. Please open this URL in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28linux%2Famd64%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%0A%60%60%60%0A&title=Error%3A+test+error diff --git a/testdata/TestError/golden-windows_amd64.golden b/testdata/TestError/golden-windows_amd64.golden new file mode 100644 index 0000000..9ab9fcc --- /dev/null +++ b/testdata/TestError/golden-windows_amd64.golden @@ -0,0 +1,9 @@ +─── ReportError ──────────────────────────────────────────────────────────────── +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +─── Browserless ──────────────────────────────────────────────────────────────── +# Error! test error `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. + ! Unable to open browser. Please open this URL in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28windows%2Famd64%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%0A%60%60%60%0A&title=Error%3A+test+error diff --git a/testdata/TestPanic/golden-darwin_arm64.golden b/testdata/TestPanic/golden-darwin_arm64.golden new file mode 100644 index 0000000..c59f5bc --- /dev/null +++ b/testdata/TestPanic/golden-darwin_arm64.golden @@ -0,0 +1,9 @@ +─── ReportError ──────────────────────────────────────────────────────────────── +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +─── Browserless ──────────────────────────────────────────────────────────────── +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. + ! Unable to open browser. Please open this URL in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28darwin%2Farm64%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28...%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A74%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A101+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%0A%60%60%60%0A&title=Error%3A+test+panic diff --git a/testdata/TestPanic/golden-linux_amd64.golden b/testdata/TestPanic/golden-linux_amd64.golden new file mode 100644 index 0000000..1b7a8e0 --- /dev/null +++ b/testdata/TestPanic/golden-linux_amd64.golden @@ -0,0 +1,9 @@ +─── ReportError ──────────────────────────────────────────────────────────────── +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +─── Browserless ──────────────────────────────────────────────────────────────── +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. + ! Unable to open browser. Please open this URL in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28linux%2Famd64%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09%3Cgoroot%3E%2Fsrc%2Fruntime%2Fpanic.go%3A%3Cline%3E+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28...%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A74%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09%3Cpwd%3E%2Fcli_test.go%3A101+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09%3Cgoroot%3E%2Fsrc%2Ftesting%2Ftesting.go%3A%3Cline%3E+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%0A%60%60%60%0A&title=Error%3A+test+panic diff --git a/testdata/TestPanic/golden-windows_amd64.golden b/testdata/TestPanic/golden-windows_amd64.golden new file mode 100644 index 0000000..5a1e0b6 --- /dev/null +++ b/testdata/TestPanic/golden-windows_amd64.golden @@ -0,0 +1,9 @@ +─── ReportError ──────────────────────────────────────────────────────────────── +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. +─── Browserless ──────────────────────────────────────────────────────────────── +# Error! test panic `` + | We are sorry you encountered this error. + ! Press Enter to open an issue on Github. + ! Unable to open browser. Please open this URL in a browser to continue: https://github.com/anchordotdev/cli/issues/new?body=%2A%2AAre+there+any+additional+details+you+would+like+to+share%3F%2A%2A%0A%0A---%0A%0A%2A%2ACommand%3A%2A%2A+%60%60%0A%2A%2AVersion%3A%2A%2A+%60dev+%28windows%2Famd64%29+Commit%3A+none+BuildDate%3A+unknown%60%0A%2A%2AArguments%3A%2A%2A+%60%5B%5D%60%0A%2A%2AFlags%3A%2A%2A+%60%5B%5D%60%0A%2A%2AStack%3A%2A%2A%0A%60%60%60%0Apanic%28%7B%3Chex%3E%2C+%3Chex%3E%7D%29%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Fruntime%2Fpanic.go%3A920+%2B%3Chex%3E%0Agithub.com%2Fanchordotdev%2Fcli_test.%28%2APanicCommand%29.run%28...%29%0A%09D%3A%2Fa%2Fanchor%2Fanchor%2Fcli%2Fcli_test.go%3A74%0Agithub.com%2Fanchordotdev%2Fcli_test.TestPanic.func1%28%3Chex%3E%29%0A%09D%3A%2Fa%2Fanchor%2Fanchor%2Fcli%2Fcli_test.go%3A101+%2B%3Chex%3E%0Atesting.tRunner%28%3Chex%3E%2C+%3Chex%3E%29%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Ftesting%2Ftesting.go%3A1595+%2B%3Chex%3E%0Acreated+by+testing.%28%2AT%29.Run+in+gouroutine+%3Cint%3E%0A%09C%3A%2Fhostedtoolcache%2Fwindows%2Fgo%2F1.21.9%2Fx64%2Fsrc%2Ftesting%2Ftesting.go%3A1648+%2B%3Chex%3E%0A%60%60%60%0A%2A%2AStdout%3A%2A%2A%0A%60%60%60%0A%0A%60%60%60%0A&title=Error%3A+test+panic diff --git a/trust/audit.go b/trust/audit.go index e35c794..3355258 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -34,7 +34,6 @@ func (c *Audit) RunTUI(ctx context.Context, drv *ui.Driver) error { cfg := cli.ConfigFromContext(ctx) drv.Activate(ctx, &models.TrustAuditHeader{}) - drv.Activate(ctx, &models.TrustAuditScan{}) anc, err := api.NewClient(cfg) diff --git a/ui/driver.go b/ui/driver.go index e6bc10f..b9c3643 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -26,6 +26,7 @@ type Program interface { Quit() Run() (tea.Model, error) Send(tea.Msg) + Wait() } type Driver struct { @@ -67,6 +68,9 @@ func NewDriverTUI(ctx context.Context) (*Driver, Program) { tea.WithInputTTY(), tea.WithContext(ctx), } + + drv.out = &safeReadWriter{rw: new(bytes.Buffer)} + drv.Out = io.TeeReader(drv.out, &drv.finalOut) drv.Program = tea.NewProgram(drv, opts...) return drv, drv.Program @@ -168,8 +172,8 @@ func (d *Driver) View() string { out += mdl.View() } normalizedOut := spinnerReplacer.Replace(out) - if d.test && out != "" && normalizedOut != d.lastView { - separator := "─── " + reflect.TypeOf(d.active).Elem().Name() + " " + if out != "" && normalizedOut != d.lastView { + separator := fmt.Sprintf("─── %s ", reflect.TypeOf(d.active).Elem().Name()) separator = separator + strings.Repeat("─", 80-utf8.RuneCountInString(separator)) fmt.Fprintln(d.out, separator) fmt.Fprint(d.out, normalizedOut) diff --git a/ui/styles.go b/ui/styles.go index 453fd19..4db3927 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -15,7 +15,6 @@ var ( header = lipgloss.NewStyle().Bold(true) hint = lipgloss.NewStyle().Faint(true).SetString("|") - Error = lipgloss.NewStyle().Bold(true).Foreground(colorDanger).SetString("! Error:").Render Header = header.Copy().SetString("#").Render Skip = header.Copy().Faint(true).SetString("# Skipped:").Render Hint = hint.Copy().Render @@ -32,6 +31,7 @@ var ( Accentuate = lipgloss.NewStyle().Italic(true).Render Action = lipgloss.NewStyle().Bold(true).Foreground(colorBrandPrimary).Render Announce = lipgloss.NewStyle().Background(colorBrandSecondary).Render + Error = lipgloss.NewStyle().Bold(true).Foreground(colorDanger).Render Emphasize = lipgloss.NewStyle().Bold(true).Render EmphasizeUnderline = lipgloss.NewStyle().Bold(true).Underline(true).Render Titlize = lipgloss.NewStyle().Bold(true).Render diff --git a/ui/uitest/uitest.go b/ui/uitest/uitest.go index 43f8726..e1e5b41 100644 --- a/ui/uitest/uitest.go +++ b/ui/uitest/uitest.go @@ -35,13 +35,20 @@ type program struct { } func (p program) Quit() { - panic("TODO") + err := p.TestModel.Quit() + if err != nil { + panic(err) + } } func (p program) Run() (tea.Model, error) { panic("TODO") } +func (p program) Wait() { + // no-op, for TestError and since TestModel doesn't provide a Wait without needing a t.testing +} + func TestTUIError(ctx context.Context, t *testing.T, tui cli.UI, msgAndArgs ...interface{}) { _, errc := testTUI(ctx, t, tui) err := <-errc