Skip to content

Commit 8d20aec

Browse files
committed
feat: adds prompt for overwriting when scaffold is rerun
Currently running `scaffold init` a second time results in an error message being propogated to the user as the cli output. This is harmless, as the error regards the files being created already existing, but not a great UX. This adds promptui to run an interactive prompt for overwriting the files. If the user selects to overwite, the command behaves as though it were run with the `-force` option. If the user doesn't, it ends the command but doesn't return an error. Other related changes introduced by this change: * The cli app is wrapped in a new VervetApp struct, allowing config options specific to vervet but not to the CLI interface to be set-- one of these options is a prompting interface. * The vervet app is added to the context passed into the cli app, so that vervet specific info can be used in the commands called by the cli app. Fixes #56
1 parent df6018b commit 8d20aec

File tree

9 files changed

+183
-12
lines changed

9 files changed

+183
-12
lines changed

cmd/cmd.go

+44
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
package cmd
33

44
import (
5+
"context"
6+
"errors"
57
"fmt"
68
"io"
79
"os"
810
"path/filepath"
11+
"strings"
912
"time"
1013

14+
"github.com/manifoldco/promptui"
1115
"github.com/urfave/cli/v2"
1216
)
1317

@@ -16,6 +20,7 @@ type VervetParams struct {
1620
Stdin io.ReadCloser
1721
Stdout io.WriteCloser
1822
Stderr io.WriteCloser
23+
Prompt VervetPrompt
1924
}
2025

2126
// VervetApp contains the cli Application.
@@ -24,6 +29,21 @@ type VervetApp struct {
2429
Params VervetParams
2530
}
2631

32+
// VervetPrompt defines the interface for interactive prompts in vervet.
33+
type VervetPrompt interface {
34+
Confirm(label string) (bool, error)
35+
}
36+
37+
type runKey string
38+
39+
var vervetKey = runKey("vervet")
40+
41+
// Run runs the cli.App with the Vervet config params.
42+
func (v *VervetApp) Run(args []string) error {
43+
context := context.WithValue(context.Background(), vervetKey, v)
44+
return v.App.RunContext(context, args)
45+
}
46+
2747
// NewApp returns a new VervetApp with the provided params.
2848
func NewApp(vp VervetParams) *VervetApp {
2949
return &VervetApp{
@@ -149,11 +169,35 @@ func NewApp(vp VervetParams) *VervetApp {
149169
}
150170
}
151171

172+
// Prompt is the default interactive prompt for vervet.
173+
type Prompt struct{}
174+
175+
// Confirm implements VervetPrompt.Confirm
176+
func (p Prompt) Confirm(label string) (bool, error) {
177+
prompt := promptui.Prompt{
178+
Label: fmt.Sprintf("%v (y/N)?", label),
179+
Default: "N",
180+
Validate: func(input string) error {
181+
input = strings.ToLower(input)
182+
if input != "n" && input != "y" {
183+
return errors.New("you must pick y or n")
184+
}
185+
return nil
186+
},
187+
}
188+
result, err := prompt.Run()
189+
if err != nil {
190+
return false, err
191+
}
192+
return (result == "y"), nil
193+
}
194+
152195
// Vervet is the vervet application with the CLI application.
153196
var Vervet = NewApp(VervetParams{
154197
Stdin: os.Stdin,
155198
Stdout: os.Stdout,
156199
Stderr: os.Stderr,
200+
Prompt: Prompt{},
157201
})
158202

159203
func absPath(path string) (string, error) {

cmd/compiler_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
func TestCompile(t *testing.T) {
1717
c := qt.New(t)
1818
dstDir := c.TempDir()
19-
err := cmd.Vervet.App.Run([]string{"vervet", "compile", testdata.Path("resources"), dstDir})
19+
err := cmd.Vervet.Run([]string{"vervet", "compile", testdata.Path("resources"), dstDir})
2020
c.Assert(err, qt.IsNil)
2121
tests := []struct {
2222
version string
@@ -49,7 +49,7 @@ func TestCompile(t *testing.T) {
4949
func TestCompileInclude(t *testing.T) {
5050
c := qt.New(t)
5151
dstDir := c.TempDir()
52-
err := cmd.Vervet.App.Run([]string{"vervet", "compile", "-I", testdata.Path("resources/include.yaml"), testdata.Path("resources"), dstDir})
52+
err := cmd.Vervet.Run([]string{"vervet", "compile", "-I", testdata.Path("resources/include.yaml"), testdata.Path("resources"), dstDir})
5353
c.Assert(err, qt.IsNil)
5454

5555
tests := []struct {
@@ -87,6 +87,6 @@ func TestCompileInclude(t *testing.T) {
8787
func TestCompileConflict(t *testing.T) {
8888
c := qt.New(t)
8989
dstDir := c.TempDir()
90-
err := cmd.Vervet.App.Run([]string{"vervet", "compile", "../testdata/conflict", dstDir})
90+
err := cmd.Vervet.Run([]string{"vervet", "compile", "../testdata/conflict", dstDir})
9191
c.Assert(err, qt.ErrorMatches, `failed to load spec versions: conflict: .*`)
9292
}

cmd/scaffold.go

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -33,7 +34,28 @@ func ScaffoldInit(ctx *cli.Context) error {
3334
return err
3435
}
3536
err = sc.Organize()
36-
if err != nil {
37+
if err == scaffold.ErrAlreadyInitialized {
38+
// If the project files already exist, prompt the user to see if they want to overwrite them.
39+
// TODO: replace using Context.Value directly with a helper func to be used in other commands.
40+
vervetApp, ok := ctx.Context.Value(vervetKey).(*VervetApp)
41+
if !ok {
42+
return errors.New("could not retrieve vervet app from context")
43+
}
44+
prompt := vervetApp.Params.Prompt
45+
overwrite, err := prompt.Confirm("Scaffold already initialized; do you want to overwrite")
46+
if err != nil {
47+
return err
48+
}
49+
if overwrite {
50+
forceFn := scaffold.Force(true)
51+
forceFn(sc)
52+
// If an error happens with --force enabled, something new has gone wrong.
53+
if err = sc.Organize(); err != nil {
54+
return err
55+
}
56+
}
57+
return nil
58+
} else if err != nil {
3759
return err
3860
}
3961
err = sc.Init()

cmd/scaffold_test.go

+82-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd_test
33
import (
44
"io/ioutil"
55
"os"
6+
"strings"
67
"testing"
78

89
qt "github.com/frankban/quicktest"
@@ -11,15 +12,92 @@ import (
1112
"github.com/snyk/vervet/testdata"
1213
)
1314

15+
var vervetConfigFile = "./.vervet.yaml"
16+
17+
type testPrompt struct {
18+
ReturnVal bool
19+
}
20+
21+
func (tp *testPrompt) Confirm(label string) (bool, error) {
22+
return tp.ReturnVal, nil
23+
}
24+
25+
var filemark = "bad wolf"
26+
27+
// markFile adds a string to a file so we can check if that file is being overwritten.
28+
func markTestFile(filename string) error {
29+
// Write a string to the file; we should see this string removed when
30+
f, err := os.OpenFile(filename, os.O_WRONLY, 0644)
31+
if err != nil {
32+
return err
33+
}
34+
defer f.Close()
35+
36+
if err != nil {
37+
return err
38+
}
39+
_, err = f.Write([]byte(filemark))
40+
return err
41+
}
42+
43+
// markInFile checks if the filemark is present, determining if the file has been
44+
// overwritten.
45+
func markInFile(filename string) (bool, error) {
46+
content, err := ioutil.ReadFile(vervetConfigFile)
47+
if err != nil {
48+
return false, err
49+
}
50+
return strings.Contains(string(content), "bad wolf"), nil
51+
}
52+
1453
func TestScaffold(t *testing.T) {
1554
c := qt.New(t)
1655
dstDir := c.TempDir()
1756
cd(c, dstDir)
18-
// Create an API project from a scaffold
19-
err := cmd.Vervet.App.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
57+
58+
prompt := testPrompt{}
59+
testApp := cmd.NewApp(cmd.VervetParams{
60+
Stdin: os.Stdin,
61+
Stdout: os.Stdout,
62+
Stderr: os.Stderr,
63+
Prompt: &prompt,
64+
})
65+
66+
// Running init creates the project files.
67+
err := testApp.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
68+
c.Assert(err, qt.IsNil)
69+
70+
// Rerunning init asks the user if they want to overwrite; if they say no
71+
// the command ends...
72+
prompt.ReturnVal = false
73+
err = markTestFile(vervetConfigFile)
74+
c.Assert(err, qt.IsNil)
75+
err = testApp.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
76+
c.Assert(err, qt.IsNil)
77+
fileMarked, err := markInFile(vervetConfigFile)
78+
c.Assert(err, qt.IsNil)
79+
c.Assert(fileMarked, qt.IsTrue)
80+
81+
// ...if the user selects yes, it will overwrite the project files.
82+
prompt.ReturnVal = true
83+
err = testApp.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
84+
c.Assert(err, qt.IsNil)
85+
fileMarked, err = markInFile(vervetConfigFile)
86+
c.Assert(err, qt.IsNil)
87+
c.Assert(fileMarked, qt.IsFalse)
88+
89+
// Rerunning init with the force option overwrites the project files.
90+
prompt.ReturnVal = false
91+
err = markTestFile(vervetConfigFile)
2092
c.Assert(err, qt.IsNil)
21-
// Generate a new resource version in the project
22-
err = cmd.Vervet.App.Run([]string{"vervet", "version", "new", "--version", "2021-10-01", "v3", "foo"})
93+
err = testApp.Run([]string{"vervet", "scaffold", "init", "--force", testdata.Path("test-scaffold")})
94+
c.Assert(err, qt.IsNil)
95+
fileMarked, err = markInFile(vervetConfigFile)
96+
c.Assert(err, qt.IsNil)
97+
c.Assert(fileMarked, qt.IsFalse)
98+
99+
// A new resource version can be generated in the project after initialization has completed.
100+
err = testApp.Run([]string{"vervet", "version", "new", "--version", "2021-10-01", "v3", "foo"})
23101
c.Assert(err, qt.IsNil)
24102
for _, item := range []string{".vervet/templates/README.tmpl", ".vervet.yaml", ".vervet/extras/foo", ".vervet/extras/bar/bar"} {
25103
_, err = os.Stat(item)

cmd/version_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestVersionFiles(t *testing.T) {
4141
defer output.Close()
4242
c.Patch(&os.Stdout, output)
4343
cd(c, testdata.Path("."))
44-
err = cmd.Vervet.App.Run([]string{"vervet", "version", "files"})
44+
err = cmd.Vervet.Run([]string{"vervet", "version", "files"})
4545
c.Assert(err, qt.IsNil)
4646
})
4747
out, err := ioutil.ReadFile(tmpFile)
@@ -64,7 +64,7 @@ func TestVersionList(t *testing.T) {
6464
defer output.Close()
6565
c.Patch(&os.Stdout, output)
6666
cd(c, testdata.Path("."))
67-
err = cmd.Vervet.App.Run([]string{"vervet", "version", "list"})
67+
err = cmd.Vervet.Run([]string{"vervet", "version", "list"})
6868
c.Assert(err, qt.IsNil)
6969
})
7070
out, err := ioutil.ReadFile(tmpFile)
@@ -95,7 +95,7 @@ func TestVersionNew(t *testing.T) {
9595
copyToDir(c, testdata.Path(".vervet/resource/version/index.ts.tmpl"), versionTemplateDir)
9696
copyToDir(c, testdata.Path(".vervet/resource/version/spec.yaml.tmpl"), versionTemplateDir)
9797
cd(c, projectDir)
98-
err := cmd.Vervet.App.Run([]string{"vervet", "version", "new", "testdata", "foo"})
98+
err := cmd.Vervet.Run([]string{"vervet", "version", "new", "testdata", "foo"})
9999
c.Assert(err, qt.IsNil)
100100
versions, err := vervet.LoadResourceVersions(filepath.Join(projectDir, "generated", "foo"))
101101
c.Assert(err, qt.IsNil)

cmd/vervet/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func main() {
11-
err := cmd.Vervet.App.Run(os.Args)
11+
err := cmd.Vervet.Run(os.Args)
1212
if err != nil {
1313
log.Fatal(err)
1414
}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/google/go-cmp v0.5.5
1313
github.com/google/uuid v1.3.0
1414
github.com/mailru/easyjson v0.7.7 // indirect
15+
github.com/manifoldco/promptui v0.9.0
1516
github.com/mattn/go-runewidth v0.0.13 // indirect
1617
github.com/mitchellh/reflectwalk v1.0.2
1718
github.com/olekukonko/tablewriter v0.0.5

go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
22
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
33
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
4+
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
5+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
6+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
7+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
8+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
9+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
410
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
511
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
612
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -38,6 +44,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
3844
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
3945
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
4046
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
47+
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
48+
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
4149
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
4250
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
4351
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -63,6 +71,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
6371
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
6472
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
6573
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
74+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
75+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
6676
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
6777
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
6878
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

internal/scaffold/scaffold.go

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
"github.com/ghodss/yaml"
1313
)
1414

15+
// ErrAlreadyInitialized is used when scaffolding is being run on a project that is already setup.
16+
var ErrAlreadyInitialized = fmt.Errorf("project files already exist")
17+
1518
// Scaffold defines a Vervet API project scaffold.
1619
type Scaffold struct {
1720
dst, src string
@@ -85,6 +88,19 @@ func New(dst, src string, options ...Option) (*Scaffold, error) {
8588
func (s *Scaffold) Organize() error {
8689
for dstItem, srcItem := range s.manifest.Organize {
8790
dstPath := filepath.Join(s.dst, dstItem)
91+
// If we're not force overwriting, check if files already exist.
92+
if !s.force {
93+
_, err := os.Stat(dstPath)
94+
if err == nil {
95+
// Project files already exist.
96+
return ErrAlreadyInitialized
97+
}
98+
if !os.IsNotExist(err) {
99+
// Something else went wrong; the file not existing is the desired
100+
// state.
101+
return err
102+
}
103+
}
88104
srcPath := filepath.Join(s.src, srcItem)
89105
err := s.copyItem(dstPath, srcPath)
90106
if err != nil {

0 commit comments

Comments
 (0)