Merge pull request #444 from Masterminds/feat/reworked-init

Rework create/init
diff --git a/action/config_wizard.go b/action/config_wizard.go
new file mode 100644
index 0000000..c00c3d7
--- /dev/null
+++ b/action/config_wizard.go
@@ -0,0 +1,362 @@
+package action
+
+import (
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/Masterminds/glide/cache"
+	"github.com/Masterminds/glide/cfg"
+	"github.com/Masterminds/glide/msg"
+	gpath "github.com/Masterminds/glide/path"
+	"github.com/Masterminds/semver"
+	"github.com/Masterminds/vcs"
+)
+
+// ConfigWizard reads configuration from a glide.yaml file and attempts to suggest
+// improvements. The wizard is interactive.
+func ConfigWizard(base string) {
+	_, err := gpath.Glide()
+	glidefile := gpath.GlideFile
+	if err != nil {
+		msg.Info("Unable to find a glide.yaml file. Would you like to create one now? Yes (Y) or No (N)")
+		bres := msg.PromptUntilYorN()
+		if bres {
+			// Guess deps
+			conf := guessDeps(base, false)
+			// Write YAML
+			if err := conf.WriteFile(glidefile); err != nil {
+				msg.Die("Could not save %s: %s", glidefile, err)
+			}
+		} else {
+			msg.Err("Unable to find configuration file. Please create configuration information to continue.")
+		}
+	}
+
+	conf := EnsureConfig()
+
+	err = cache.Setup()
+	if err != nil {
+		msg.Die("Problem setting up cache: %s", err)
+	}
+
+	msg.Info("Looking for dependencies to make suggestions on")
+	msg.Info("--> Scanning for dependencies not using verion ranges")
+	msg.Info("--> Scanning for dependencies using commit ids")
+	var deps []*cfg.Dependency
+	for _, dep := range conf.Imports {
+		if wizardLookInto(dep) {
+			deps = append(deps, dep)
+		}
+	}
+
+	msg.Info("Gathering information on each dependency")
+	msg.Info("--> This may take a moment. Especially on a codebase with many dependencies")
+	msg.Info("--> Gathering release information for dependencies")
+	msg.Info("--> Looking for dependency imports where versions are commit ids")
+	for _, dep := range deps {
+		wizardFindVersions(dep)
+	}
+
+	var changes int
+	for _, dep := range deps {
+		var remote string
+		if dep.Repository != "" {
+			remote = dep.Repository
+		} else {
+			remote = "https://" + dep.Name
+		}
+
+		// First check, ask if the tag should be used instead of the commit id for it.
+		cur := cache.MemCurrent(remote)
+		if cur != "" && cur != dep.Reference {
+			wizardSugOnce()
+			var dres bool
+			asked, use, val := wizardOnce("current")
+			if !use {
+				dres = wizardAskCurrent(cur, dep)
+			}
+			if !asked {
+				as := wizardRemember()
+				wizardSetOnce("current", as, dres)
+			}
+
+			if asked && use {
+				dres = val.(bool)
+			}
+
+			if dres {
+				msg.Info("Updating %s to use the tag %s instead of commit id %s", dep.Name, cur, dep.Reference)
+				dep.Reference = cur
+				changes++
+			}
+		}
+
+		// Second check, if no version is being used and there's a semver release ask about latest.
+		memlatest := cache.MemLatest(remote)
+		if dep.Reference == "" && memlatest != "" {
+			wizardSugOnce()
+			var dres bool
+			asked, use, val := wizardOnce("latest")
+			if !use {
+				dres = wizardAskLatest(memlatest, dep)
+			}
+			if !asked {
+				as := wizardRemember()
+				wizardSetOnce("latest", as, dres)
+			}
+
+			if asked && use {
+				dres = val.(bool)
+			}
+
+			if dres {
+				msg.Info("Updating %s to use the release %s instead of no release", dep.Name, memlatest)
+				dep.Reference = memlatest
+				changes++
+			}
+		}
+
+		// Third check, if the version is semver offer to use a range instead.
+		sv, err := semver.NewVersion(dep.Reference)
+		if err == nil {
+			wizardSugOnce()
+			var res string
+			asked, use, val := wizardOnce("range")
+			if !use {
+				res = wizardAskRange(sv, dep)
+			}
+			if !asked {
+				as := wizardRemember()
+				wizardSetOnce("range", as, res)
+			}
+
+			if asked && use {
+				res = val.(string)
+			}
+
+			if res == "m" {
+				r := "^" + sv.String()
+				msg.Info("Updating %s to use the range %s instead of commit id %s", dep.Name, r, dep.Reference)
+				dep.Reference = r
+				changes++
+			} else if res == "p" {
+				r := "~" + sv.String()
+				msg.Info("Updating %s to use the range %s instead of commit id %s", dep.Name, r, dep.Reference)
+				dep.Reference = r
+				changes++
+			}
+		}
+	}
+
+	if changes > 0 {
+		msg.Info("Configuration changes have been made. Would you like to write these")
+		msg.Info("changes to your configuration file? Yes (Y) or No (N)")
+		dres := msg.PromptUntilYorN()
+		if dres {
+			msg.Info("Writing updates to configuration file (%s)", glidefile)
+			if err := conf.WriteFile(glidefile); err != nil {
+				msg.Die("Could not save %s: %s", glidefile, err)
+			}
+			msg.Info("You can now edit the glide.yaml file.:")
+			msg.Info("--> For more information on versions and ranges see https://glide.sh/docs/versions/")
+			msg.Info("--> For details on additional metadata see https://glide.sh/docs/glide.yaml/")
+		} else {
+			msg.Warn("Change not written to configuration file")
+		}
+	} else {
+		msg.Info("No proposed changes found. Have a nice day.")
+	}
+}
+
+var wizardOnceVal = make(map[string]interface{})
+var wizardOnceDo = make(map[string]bool)
+var wizardOnceAsked = make(map[string]bool)
+
+var wizardSuggeseOnce bool
+
+func wizardSugOnce() {
+	if !wizardSuggeseOnce {
+		msg.Info("Here are some suggestions...")
+	}
+	wizardSuggeseOnce = true
+}
+
+// Returns if it's you should prompt, if not prompt if you should use stored value,
+// and stored value if it has one.
+func wizardOnce(name string) (bool, bool, interface{}) {
+	return wizardOnceAsked[name], wizardOnceDo[name], wizardOnceVal[name]
+}
+
+func wizardSetOnce(name string, prompt bool, val interface{}) {
+	wizardOnceAsked[name] = true
+	wizardOnceDo[name] = prompt
+	wizardOnceVal[name] = val
+}
+
+func wizardRemember() bool {
+	msg.Info("Would you like to remember the previous decision and apply it to future")
+	msg.Info("dependencies? Yes (Y) or No (N)")
+	return msg.PromptUntilYorN()
+}
+
+func wizardAskRange(ver *semver.Version, d *cfg.Dependency) string {
+	vstr := ver.String()
+	msg.Info("The package %s appears to use semantic versions (http://semver.org).", d.Name)
+	msg.Info("Would you like to track the latest minor or patch releases (major.minor.path)?")
+	msg.Info("Tracking minor version releases would use '>= %s, < %d.0.0' ('^%s'). Tracking patch version", vstr, ver.Major()+1, vstr)
+	msg.Info("releases would use '>= %s, < %d.%d.0' ('~%s'). For more information on Glide versions", vstr, ver.Major(), ver.Minor()+1, vstr)
+	msg.Info("and ranges see https://glide.sh/docs/versions")
+	msg.Info("Minor (M), Patch (P), or Skip Ranges (S)?")
+	res, err := msg.PromptUntil([]string{"minor", "m", "patch", "p", "skip ranges", "s"})
+	if err != nil {
+		msg.Die("Error processing response: %s", err)
+	}
+	if res == "m" || res == "minor" {
+		return "m"
+	} else if res == "p" || res == "patch" {
+		return "p"
+	}
+
+	return "s"
+}
+
+func wizardAskCurrent(cur string, d *cfg.Dependency) bool {
+	msg.Info("The package %s is currently set to use the version %s.", d.Name, d.Reference)
+	msg.Info("There is an equivalent semantic version (http://semver.org) release of %s. Would", cur)
+	msg.Info("you like to use that instead? Yes (Y) or No (N)")
+	return msg.PromptUntilYorN()
+}
+
+func wizardAskLatest(latest string, d *cfg.Dependency) bool {
+	msg.Info("The package %s appears to have Semantic Version releases (http://semver.org). ", d.Name)
+	msg.Info("The latestrelease is %s. You are currently not using a release. Would you like", latest)
+	msg.Info("to use this release? Yes (Y) or No (N)")
+	return msg.PromptUntilYorN()
+}
+
+func wizardLookInto(d *cfg.Dependency) bool {
+	_, err := semver.NewConstraint(d.Reference)
+
+	// The existing version is already a valid semver constraint so we skip suggestions.
+	if err == nil {
+		return false
+	}
+
+	return true
+}
+
+// Note, this really needs a simpler name.
+var createGitParseVersion = regexp.MustCompile(`(?m-s)(?:tags)/(\S+)$`)
+
+func wizardFindVersions(d *cfg.Dependency) {
+	l, err := cache.Location()
+	if err != nil {
+		msg.Debug("Problem detecting cache location: %s", err)
+	}
+	var remote string
+	if d.Repository != "" {
+		remote = d.Repository
+	} else {
+		remote = "https://" + d.Name
+	}
+
+	key, err := cache.Key(remote)
+	if err != nil {
+		msg.Debug("Problem generating cache key for %s: %s", remote, err)
+	}
+
+	local := filepath.Join(l, "src", key)
+	repo, err := vcs.NewRepo(remote, local)
+	if err != nil {
+		msg.Debug("Problem getting repo instance: %s", err)
+	}
+
+	var useLocal bool
+	if _, err = os.Stat(local); err == nil {
+		useLocal = true
+	}
+
+	// Git endpoints allow for querying without fetching the codebase locally.
+	// We try that first to avoid fetching right away. Is this premature
+	// optimization?
+	cc := true
+	if !useLocal && repo.Vcs() == vcs.Git {
+		out, err2 := exec.Command("git", "ls-remote", remote).CombinedOutput()
+		if err2 == nil {
+			cache.MemTouch(remote)
+			cc = false
+			lines := strings.Split(string(out), "\n")
+			for _, i := range lines {
+				ti := strings.TrimSpace(i)
+				if found := createGitParseVersion.FindString(ti); found != "" {
+					tg := strings.TrimPrefix(strings.TrimSuffix(found, "^{}"), "tags/")
+					cache.MemPut(remote, tg)
+					if d.Reference != "" && strings.HasPrefix(ti, d.Reference) {
+						cache.MemSetCurrent(remote, tg)
+					}
+				}
+			}
+		}
+	}
+
+	if cc {
+		cache.Lock(key)
+		cache.MemTouch(remote)
+		if _, err = os.Stat(local); os.IsNotExist(err) {
+			repo.Get()
+			branch := findCurrentBranch(repo)
+			c := cache.RepoInfo{DefaultBranch: branch}
+			err = cache.SaveRepoData(key, c)
+			if err != nil {
+				msg.Debug("Error saving cache repo details: %s", err)
+			}
+		} else {
+			repo.Update()
+		}
+		tgs, err := repo.Tags()
+		if err != nil {
+			msg.Debug("Problem getting tags: %s", err)
+		} else {
+			for _, v := range tgs {
+				cache.MemPut(remote, v)
+			}
+		}
+		if d.Reference != "" && repo.IsReference(d.Reference) {
+			tgs, err = repo.TagsFromCommit(d.Reference)
+			if err != nil {
+				msg.Debug("Problem getting tags for commit: %s", err)
+			} else {
+				if len(tgs) > 0 {
+					for _, v := range tgs {
+						if !(repo.Vcs() == vcs.Hg && v == "tip") {
+							cache.MemSetCurrent(remote, v)
+						}
+					}
+				}
+			}
+		}
+		cache.Unlock(key)
+	}
+}
+
+func findCurrentBranch(repo vcs.Repo) string {
+	msg.Debug("Attempting to find current branch for %s", repo.Remote())
+	// Svn and Bzr don't have default branches.
+	if repo.Vcs() == vcs.Svn || repo.Vcs() == vcs.Bzr {
+		return ""
+	}
+
+	if repo.Vcs() == vcs.Git || repo.Vcs() == vcs.Hg {
+		ver, err := repo.Current()
+		if err != nil {
+			msg.Debug("Unable to find current branch for %s, error: %s", repo.Remote(), err)
+			return ""
+		}
+		return ver
+	}
+
+	return ""
+}
diff --git a/action/create.go b/action/create.go
index b66257c..4d1c0bb 100644
--- a/action/create.go
+++ b/action/create.go
@@ -25,7 +25,7 @@
 // If skipImport is set to true, this will not attempt to import from an existing
 // GPM, Godep, or GB project if one should exist. However, it will still attempt
 // to read the local source to determine required packages.
-func Create(base string, skipImport bool) {
+func Create(base string, skipImport, nonInteractive bool) {
 	glidefile := gpath.GlideFile
 	// Guard against overwrites.
 	guardYAML(glidefile)
@@ -33,9 +33,28 @@
 	// Guess deps
 	conf := guessDeps(base, skipImport)
 	// Write YAML
+	msg.Info("Writing configuration file (%s)", glidefile)
 	if err := conf.WriteFile(glidefile); err != nil {
 		msg.Die("Could not save %s: %s", glidefile, err)
 	}
+
+	var res bool
+	if !nonInteractive {
+		msg.Info("Would you like Glide to help you find ways to improve your glide.yaml configuration?")
+		msg.Info("If you want to revisit this step you can use the config-wizard command at any time.")
+		msg.Info("Yes (Y) or No (N)?")
+		res = msg.PromptUntilYorN()
+		if res {
+			ConfigWizard(base)
+		}
+	}
+
+	if !res {
+		msg.Info("You can now edit the glide.yaml file. Consider:")
+		msg.Info("--> Using versions and ranges. See https://glide.sh/docs/versions/")
+		msg.Info("--> Adding additional metadata. See https://glide.sh/docs/glide.yaml/")
+		msg.Info("--> Running the config-wizard command to improve the versions in your configuration")
+	}
 }
 
 // guardYAML fails if the given file already exists.
@@ -76,6 +95,13 @@
 		guessImportDeps(base, config)
 	}
 
+	importLen := len(config.Imports)
+	if importLen == 0 {
+		msg.Info("Scanning code to look for dependencies")
+	} else {
+		msg.Info("Scanning code to look for dependencies not found in import")
+	}
+
 	// Resolve dependencies by looking at the tree.
 	r, err := dependency.NewResolver(base)
 	if err != nil {
@@ -102,7 +128,7 @@
 		root, subpkg := util.NormalizeName(n)
 
 		if !config.HasDependency(root) && root != config.Name {
-			msg.Info("Found reference to %s\n", n)
+			msg.Info("--> Found reference to %s\n", n)
 			d := &cfg.Dependency{
 				Name: root,
 			}
@@ -115,13 +141,17 @@
 				subpkg = strings.TrimPrefix(subpkg, "/")
 				d := config.Imports.Get(root)
 				if !d.HasSubpackage(subpkg) {
-					msg.Info("Adding sub-package %s to %s\n", subpkg, root)
+					msg.Info("--> Adding sub-package %s to %s\n", subpkg, root)
 					d.Subpackages = append(d.Subpackages, subpkg)
 				}
 			}
 		}
 	}
 
+	if len(config.Imports) == importLen && importLen != 0 {
+		msg.Info("--> Code scanning found no additional imports")
+	}
+
 	return config
 }
 
@@ -146,7 +176,12 @@
 	}
 
 	for _, i := range deps {
-		msg.Info("Found imported reference to %s\n", i.Name)
+		if i.Reference == "" {
+			msg.Info("--> Found imported reference to %s", i.Name)
+		} else {
+			msg.Info("--> Found imported reference to %s at revision %s", i.Name, i.Reference)
+		}
+
 		config.Imports = append(config.Imports, i)
 	}
 }
diff --git a/action/debug.go b/action/debug.go
index 7b8eea6..6af9d26 100644
--- a/action/debug.go
+++ b/action/debug.go
@@ -7,6 +7,15 @@
 // Debug sets the debugging flags across components.
 func Debug(on bool) {
 	msg.Default.IsDebugging = on
+
+	if on == true {
+		msg.Default.IsVerbose = on
+	}
+}
+
+// Verbose sets the verbose flags across components.
+func Verbose(on bool) {
+	msg.Default.IsVerbose = on
 }
 
 // Quiet sets the quiet flags across components.
diff --git a/cache/memory.go b/cache/memory.go
new file mode 100644
index 0000000..3809331
--- /dev/null
+++ b/cache/memory.go
@@ -0,0 +1,139 @@
+package cache
+
+import (
+	"sync"
+
+	"github.com/Masterminds/glide/msg"
+	"github.com/Masterminds/semver"
+)
+
+// Provide an in memory cache of imported project information.
+
+var defaultMemCache = newMemCache()
+
+// MemPut put a version into the in memory cache for a name.
+// This will silently ignore non-semver and make sure the latest
+// is stored.
+func MemPut(name, version string) {
+	defaultMemCache.put(name, version)
+}
+
+// MemTouched returns true if the cache was touched for a name.
+func MemTouched(name string) bool {
+	return defaultMemCache.touched(name)
+}
+
+// MemTouch notes if a name has been looked at.
+func MemTouch(name string) {
+	defaultMemCache.touch(name)
+}
+
+// MemLatest returns the latest, that is most recent, semver release. This
+// may be a blank string if no put value
+func MemLatest(name string) string {
+	return defaultMemCache.getLatest(name)
+}
+
+// MemSetCurrent is used to set the current version in use.
+func MemSetCurrent(name, version string) {
+	defaultMemCache.setCurrent(name, version)
+}
+
+// MemCurrent is used to get the current version in use.
+func MemCurrent(name string) string {
+	return defaultMemCache.current(name)
+}
+
+// An in memory cache.
+type memCache struct {
+	sync.RWMutex
+	latest   map[string]string
+	t        map[string]bool
+	versions map[string][]string
+	c        map[string]string
+}
+
+func newMemCache() *memCache {
+	return &memCache{
+		latest:   make(map[string]string),
+		t:        make(map[string]bool),
+		versions: make(map[string][]string),
+		c:        make(map[string]string),
+	}
+}
+
+func (m *memCache) setCurrent(name, version string) {
+	m.Lock()
+	defer m.Unlock()
+
+	if m.c[name] == "" {
+		m.c[name] = version
+	} else {
+		// If we already have a version try to see if the new or old one is
+		// semver and use that one.
+		_, err := semver.NewVersion(m.c[name])
+		if err != nil {
+			_, err2 := semver.NewVersion(version)
+			if err2 == nil {
+				m.c[name] = version
+			}
+		}
+	}
+}
+
+func (m *memCache) current(name string) string {
+	m.RLock()
+	defer m.RUnlock()
+	return m.c[name]
+}
+
+func (m *memCache) put(name, version string) {
+	m.Lock()
+	defer m.Unlock()
+	m.t[name] = true
+	sv, err := semver.NewVersion(version)
+	if err != nil {
+		msg.Debug("Ignoring %s version %s: %s", name, version, err)
+		return
+	}
+
+	latest, found := m.latest[name]
+	if found {
+		lv, err := semver.NewVersion(latest)
+		if err != nil {
+			if sv.GreaterThan(lv) {
+				m.latest[name] = version
+			}
+		}
+	} else {
+		m.latest[name] = version
+	}
+
+	found = false
+	for _, v := range m.versions[name] {
+		if v == version {
+			found = true
+		}
+	}
+	if !found {
+		m.versions[name] = append(m.versions[name], version)
+	}
+}
+
+func (m *memCache) touch(name string) {
+	m.Lock()
+	defer m.Unlock()
+	m.t[name] = true
+}
+
+func (m *memCache) touched(name string) bool {
+	m.RLock()
+	defer m.RUnlock()
+	return m.t[name]
+}
+
+func (m *memCache) getLatest(name string) string {
+	m.RLock()
+	defer m.RUnlock()
+	return m.latest[name]
+}
diff --git a/gb/gb.go b/gb/gb.go
index 1f3b208..7837a42 100644
--- a/gb/gb.go
+++ b/gb/gb.go
@@ -26,6 +26,7 @@
 	}
 
 	msg.Info("Found GB manifest file in %s", gpath.StripBasepath(dir))
+	msg.Info("--> Parsing GB metadata...")
 	buf := []*cfg.Dependency{}
 	file, err := os.Open(path)
 	if err != nil {
diff --git a/glide.go b/glide.go
index 0e1e355..5bd2894 100644
--- a/glide.go
+++ b/glide.go
@@ -92,8 +92,12 @@
 			Usage: "Quiet (no info or debug messages)",
 		},
 		cli.BoolFlag{
+			Name:  "verbose",
+			Usage: "Print detailed informational messages",
+		},
+		cli.BoolFlag{
 			Name:  "debug",
-			Usage: "Print Debug messages (verbose)",
+			Usage: "Print debug verbose informational messages",
 		},
 		cli.StringFlag{
 			Name:   "home",
@@ -146,9 +150,24 @@
 					Name:  "skip-import",
 					Usage: "When initializing skip importing from other package managers.",
 				},
+				cli.BoolFlag{
+					Name:  "non-interactive",
+					Usage: "Disable interactive prompts.",
+				},
 			},
 			Action: func(c *cli.Context) {
-				action.Create(".", c.Bool("skip-import"))
+				action.Create(".", c.Bool("skip-import"), c.Bool("non-interactive"))
+			},
+		},
+		{
+			Name:      "config-wizard",
+			ShortName: "cw",
+			Usage:     "Wizard that makes optional suggestions to improve config in a glide.yaml file.",
+			Description: `Glide will analyze a projects glide.yaml file and the imported
+		projects to find ways the glide.yaml file can potentially be improved. It
+		will then interactively make suggestions that you can skip or accept.`,
+			Action: func(c *cli.Context) {
+				action.ConfigWizard(".")
 			},
 		},
 		{
@@ -664,6 +683,7 @@
 // so it can be used by any Glide command.
 func startup(c *cli.Context) error {
 	action.Debug(c.Bool("debug"))
+	action.Verbose(c.Bool("verbose"))
 	action.NoColor(c.Bool("no-color"))
 	action.Quiet(c.Bool("quiet"))
 	action.Init(c.String("yaml"), c.String("home"))
diff --git a/godep/godep.go b/godep/godep.go
index a7d0525..71291b3 100644
--- a/godep/godep.go
+++ b/godep/godep.go
@@ -55,6 +55,7 @@
 		return []*cfg.Dependency{}, nil
 	}
 	msg.Info("Found Godeps.json file in %s", gpath.StripBasepath(dir))
+	msg.Info("--> Parsing Godeps metadata...")
 
 	buf := []*cfg.Dependency{}
 
diff --git a/gom/gom.go b/gom/gom.go
index 76305e4..51910b0 100644
--- a/gom/gom.go
+++ b/gom/gom.go
@@ -26,6 +26,7 @@
 	}
 
 	msg.Info("Found Gomfile in %s", gpath.StripBasepath(dir))
+	msg.Info("--> Parsing Gomfile metadata...")
 	buf := []*cfg.Dependency{}
 
 	goms, err := parseGomfile(path)
diff --git a/gpm/gpm.go b/gpm/gpm.go
index 45e316f..e58a81d 100644
--- a/gpm/gpm.go
+++ b/gpm/gpm.go
@@ -31,6 +31,7 @@
 		return []*cfg.Dependency{}, nil
 	}
 	msg.Info("Found Godeps file in %s", gpath.StripBasepath(dir))
+	msg.Info("--> Parsing GPM metadata...")
 
 	buf := []*cfg.Dependency{}
 
diff --git a/msg/msg.go b/msg/msg.go
index af49ecf..34213fd 100644
--- a/msg/msg.go
+++ b/msg/msg.go
@@ -1,6 +1,7 @@
 package msg
 
 import (
+	"bufio"
 	"fmt"
 	"io"
 	"os"
@@ -16,9 +17,12 @@
 	// Quiet, if true, suppresses chatty levels, like Info.
 	Quiet bool
 
-	// IsDebugging, if true, shows verbose levels, like Debug.
+	// IsDebugging, if true, shows Debug.
 	IsDebugging bool
 
+	// IsVerbose, if true, shows detailed informational messages.
+	IsVerbose bool
+
 	// NoColor, if true, will not use color in the output.
 	NoColor bool
 
@@ -28,6 +32,9 @@
 	// Stderr is the location where this prints logs.
 	Stderr io.Writer
 
+	// Stdin is the location where input is read.
+	Stdin io.Reader
+
 	// PanicOnDie if true Die() will panic instead of exiting.
 	PanicOnDie bool
 
@@ -43,9 +50,11 @@
 	m := &Messenger{
 		Quiet:       false,
 		IsDebugging: false,
+		IsVerbose:   false,
 		NoColor:     false,
 		Stdout:      os.Stdout,
 		Stderr:      os.Stderr,
+		Stdin:       os.Stdin,
 		PanicOnDie:  false,
 		ecode:       1,
 	}
@@ -76,7 +85,7 @@
 		return
 	}
 	prefix := "[DEBUG]\t"
-	Msg(prefix+msg, args...)
+	m.Msg(prefix+msg, args...)
 }
 
 // Debug logs debug information using the Default Messenger
@@ -84,6 +93,19 @@
 	Default.Debug(msg, args...)
 }
 
+// Verbose logs detailed information
+func (m *Messenger) Verbose(msg string, args ...interface{}) {
+	if m.Quiet || !m.IsVerbose {
+		return
+	}
+	m.Info(msg, args...)
+}
+
+// Verbose detailed information using the Default Messenger
+func Verbose(msg string, args ...interface{}) {
+	Default.Verbose(msg, args...)
+}
+
 // Warn logs a warning
 func (m *Messenger) Warn(msg string, args ...interface{}) {
 	prefix := m.Color(Yellow, "[WARN]\t")
@@ -238,3 +260,54 @@
 func Color(code, msg string) string {
 	return Default.Color(code, msg)
 }
+
+// PromptUntil provides a prompt until one of the passed in strings has been
+// entered and return is hit. Note, the comparisons are case insensitive meaning
+// Y == y. The returned value is the one from the passed in options (same case).
+func (m *Messenger) PromptUntil(opts []string) (string, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		text, err := reader.ReadString('\n')
+		if err != nil {
+			return "", err
+		}
+
+		for _, c := range opts {
+			if strings.EqualFold(c, strings.TrimSpace(text)) {
+				return c, nil
+			}
+		}
+	}
+}
+
+// PromptUntil provides a prompt until one of the passed in strings has been
+// entered and return is hit. Note, the comparisons are case insensitive meaning
+// Y == y. The returned value is the one from the passed in options (same case).
+// Uses the default setup.
+func PromptUntil(opts []string) (string, error) {
+	return Default.PromptUntil(opts)
+}
+
+// PromptUntilYorN provides a prompt until the user chooses yes or no. This is
+// not case sensitive and they can input other options such as Y or N.
+// In the response Yes is bool true and No is bool false.
+func (m *Messenger) PromptUntilYorN() bool {
+	res, err := m.PromptUntil([]string{"y", "yes", "n", "no"})
+	if err != nil {
+		m.Die("Error processing response: %s", err)
+	}
+
+	if res == "y" || res == "yes" {
+		return true
+	}
+
+	return false
+}
+
+// PromptUntilYorN provides a prompt until the user chooses yes or no. This is
+// not case sensitive and they can input other options such as Y or N.
+// In the response Yes is bool true and No is bool false.
+// Uses the default setup.
+func PromptUntilYorN() bool {
+	return Default.PromptUntilYorN()
+}