Repository: Shopify/themekit (go)

Issue:
Specifying flags through command line and env variables still requires a config.yml 
### What happened
Tried to issue `download` and `deploy` commands by specifying credentials through command line args as well as environment variables without a config.yml file but commands failed with error:
   > Could not find config file at ..

Calling an initial `get` creates a config.yml but looking to issue commands without doing this.

### What I expected to happen
The cli would use the given credentials without checking for the existence of a config.yml file.

### Steps to reproduce

1. From a directory without a config.yml file, specify password, store url, and theme id as command line flags.
2. Setting the corresponding environment variables results in the same error.

### My Environment

OS (including version): Mac OS Sierra 10.12.6
Themekit version: ThemeKit 0.8.1 darwin/amd64
Editor (including version): vim 8.1 
Ignore Files(if any): None


Pull request:
Making theme command execution to not require a config file
fixes #578

This does a couple of things to make the command execution not require a config file.
- [x] warn instead of fail on a config file missing
- [x] take out custom string arg array to simplify code
- [x] add a default on the environments flag

### Warn Checklist
- [ ] This changes the interface and requires a Major/Minor version change.
- [ ] I have :tophat:'d these changes by using the commands I changed by hand.
- [ ] I have added a dependancy to the project.


Patch:
diff --git a/changelog.txt b/changelog.txt
index 1b8be9e..4dc9c1c 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,6 +1,8 @@
 v0.8.2
 ======================
 - Fixed a problem with uploading compiled assets
+- Decoupled config file from command execution so any command can run without
+	a config file.
 
 v0.8.1 (Sept 18, 2018)
 ======================
diff --git a/cmd/configure.go b/cmd/configure.go
index e958989..3f560bd 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -4,7 +4,6 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/Shopify/themekit/src/cmdutil"
-	"github.com/Shopify/themekit/src/env"
 )
 
 var configureCmd = &cobra.Command{
@@ -21,17 +20,10 @@ var configureCmd = &cobra.Command{
 }
 
 func createConfig(ctx *cmdutil.Ctx) error {
-	flagEnvs := ctx.Flags.Environments.Value()
-	if len(flagEnvs) == 0 {
-		if _, err := ctx.Conf.Set(env.Default.Name, *ctx.Env); err != nil {
+	for _, name := range ctx.Flags.Environments {
+		if _, err := ctx.Conf.Set(name, *ctx.Env); err != nil {
 			return err
 		}
-	} else {
-		for _, name := range flagEnvs {
-			if _, err := ctx.Conf.Set(name, *ctx.Env); err != nil {
-				return err
-			}
-		}
 	}
 	return ctx.Conf.Save()
 }
diff --git a/cmd/theme.go b/cmd/theme.go
index 83c52dd..1f0fa27 100644
--- a/cmd/theme.go
+++ b/cmd/theme.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/Shopify/themekit/src/cmdutil"
 	"github.com/Shopify/themekit/src/colors"
+	"github.com/Shopify/themekit/src/env"
 	"github.com/Shopify/themekit/src/release"
 )
 
@@ -74,7 +75,7 @@ func init() {
 	defaultConfigPath := filepath.Join(pwd, "config.yml")
 
 	ThemeCmd.PersistentFlags().StringVarP(&flags.ConfigPath, "config", "c", defaultConfigPath, "path to config.yml")
-	ThemeCmd.PersistentFlags().VarP(&flags.Environments, "env", "e", "environment to run the command")
+	ThemeCmd.PersistentFlags().StringArrayVarP(&flags.Environments, "env", "e", []string{env.Default.Name}, "environment to run the command")
 	ThemeCmd.PersistentFlags().StringVarP(&flags.Directory, "dir", "d", "", "directory that command will take effect. (default current directory)")
 	ThemeCmd.PersistentFlags().StringVarP(&flags.Password, "password", "p", "", "theme password. This will override what is in your config.yml")
 	ThemeCmd.PersistentFlags().StringVarP(&flags.ThemeID, "themeid", "t", "", "theme id. This will override what is in your config.yml")
@@ -83,8 +84,8 @@ func init() {
 	ThemeCmd.PersistentFlags().DurationVar(&flags.Timeout, "timeout", 0, "the timeout to kill any stalled processes. This will override what is in your config.yml")
 	ThemeCmd.PersistentFlags().BoolVarP(&flags.Verbose, "verbose", "v", false, "Enable more verbose output from the running command.")
 	ThemeCmd.PersistentFlags().BoolVarP(&flags.DisableUpdateNotifier, "no-update-notifier", "", false, "Stop theme kit from notifying about updates.")
-	ThemeCmd.PersistentFlags().Var(&flags.IgnoredFiles, "ignored-file", "A single file to ignore, use the flag multiple times to add multiple.")
-	ThemeCmd.PersistentFlags().Var(&flags.Ignores, "ignores", "A path to a file that contains ignore patterns.")
+	ThemeCmd.PersistentFlags().StringArrayVar(&flags.IgnoredFiles, "ignored-file", []string{}, "A single file to ignore, use the flag multiple times to add multiple.")
+	ThemeCmd.PersistentFlags().StringArrayVar(&flags.Ignores, "ignores", []string{}, "A path to a file that contains ignore patterns.")
 	ThemeCmd.PersistentFlags().BoolVar(&flags.DisableIgnore, "no-ignore", false, "Will disable config ignores so that all files can be changed")
 
 	watchCmd.Flags().StringVarP(&flags.NotifyFile, "notify", "n", "", "file to touch when workers have gone idle")
diff --git a/src/cmdutil/string_arg_array.go b/src/cmdutil/string_arg_array.go
deleted file mode 100644
index acbec9d..0000000
--- a/src/cmdutil/string_arg_array.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package cmdutil
-
-import (
-	"strings"
-)
-
-type stringArgArray struct {
-	values []string
-}
-
-func (fa *stringArgArray) String() string {
-	return strings.Join(fa.values, ",")
-}
-
-func (fa *stringArgArray) Set(value string) error {
-	if len(value) > 0 {
-		fa.values = append(fa.values, value)
-	}
-	return nil
-}
-
-func (fa *stringArgArray) Type() string {
-	return "string"
-}
-
-func (fa *stringArgArray) Value() []string {
-	if len(fa.values) == 0 {
-		return nil
-	}
-	return fa.values
-}
diff --git a/src/cmdutil/util.go b/src/cmdutil/util.go
index 6e1bb6f..8d65f56 100644
--- a/src/cmdutil/util.go
+++ b/src/cmdutil/util.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"log"
 	"os"
+	"strings"
 	"sync"
 	"time"
 
@@ -26,7 +27,7 @@ var ErrReload = errors.New("reloading config")
 // command line. Some of the values are used across different commands
 type Flags struct {
 	ConfigPath            string
-	Environments          stringArgArray
+	Environments          []string
 	Directory             string
 	Password              string
 	ThemeID               string
@@ -35,8 +36,8 @@ type Flags struct {
 	Timeout               time.Duration
 	Verbose               bool
 	DisableUpdateNotifier bool
-	IgnoredFiles          stringArgArray
-	Ignores               stringArgArray
+	IgnoredFiles          []string
+	Ignores               []string
 	DisableIgnore         bool
 	NotifyFile            string
 	AllEnvs               bool
@@ -179,21 +180,24 @@ func generateContexts(newClient clientFact, progress *mpb.Progress, flags Flags,
 	flagEnv := getFlagEnv(flags)
 
 	config, err := env.Load(flags.ConfigPath)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return ctxs, fmt.Errorf("Could not find config file at %v", flags.ConfigPath)
-		}
+	if err != nil && os.IsNotExist(err) {
+		colors.ColorStdOut.Printf(
+			"[%s] Could not find config file at %v",
+			colors.Yellow("warn"),
+			colors.Yellow(flags.ConfigPath),
+		)
+	} else if err != nil {
 		return ctxs, err
 	}
 
-	for name := range config.Envs {
-		if !shouldUseEnvironment(flags, name) {
-			continue
-		}
-
+	for _, name := range expandEnvironments(flags, config.Envs) {
 		e, err := config.Get(name, flagEnv)
-		if err != nil {
+		if err != nil && err != env.ErrEnvDoesNotExist {
 			return ctxs, err
+		} else if e == nil {
+			if e, err = config.Set(name, flagEnv); err != nil {
+				return ctxs, err
+			}
 		}
 
 		ctx, err := createCtx(newClient, config, e, flags, args, progress, true)
@@ -204,10 +208,6 @@ func generateContexts(newClient clientFact, progress *mpb.Progress, flags Flags,
 		ctxs = append(ctxs, ctx)
 	}
 
-	if len(ctxs) == 0 {
-		return ctxs, fmt.Errorf("Could not load any valid environments")
-	}
-
 	return ctxs, nil
 }
 
@@ -223,24 +223,36 @@ func getFlagEnv(flags Flags) env.Env {
 	}
 
 	if !flags.DisableIgnore {
-		flagEnv.IgnoredFiles = flags.IgnoredFiles.Value()
-		flagEnv.Ignores = flags.Ignores.Value()
+		flagEnv.IgnoredFiles = flags.IgnoredFiles
+		flagEnv.Ignores = flags.Ignores
 	}
 
 	return flagEnv
 }
 
-func shouldUseEnvironment(flags Flags, envName string) bool {
-	flagEnvs := flags.Environments.Value()
-	if flags.AllEnvs || (len(flagEnvs) == 0 && envName == env.Default.Name) {
-		return true
+func expandEnvironments(flags Flags, confEnvs map[string]*env.Env) []string {
+	envs := []string{}
+
+	if flags.AllEnvs {
+		for env := range confEnvs {
+			envs = append(envs, env)
+		}
+		return envs
 	}
-	for _, env := range flagEnvs {
-		if env == envName || glob.Glob(env, envName) {
-			return true
+
+	for _, flagEnv := range flags.Environments {
+		if strings.Contains(flagEnv, "*") {
+			for confEnv := range confEnvs {
+				if glob.Glob(flagEnv, confEnv) {
+					envs = append(envs, confEnv)
+				}
+			}
+		} else {
+			envs = append(envs, flagEnv)
 		}
 	}
-	return false
+
+	return envs
 }
 
 // ForEachClient will generate a command context for all the available environments
@@ -320,14 +332,14 @@ func forDefaultClient(newClient clientFact, flags Flags, args []string, handler
 	}
 
 	envName := env.Default.Name
-	flagEnvs := flags.Environments.Value()
-	if len(flagEnvs) > 0 {
-		envName = flagEnvs[0]
+	if len(flags.Environments) > 0 {
+		envName = flags.Environments[0]
 	}
 
 	var e *env.Env
-	if e, err = config.Get(envName, getFlagEnv(flags)); err != nil {
-		e, err = config.Set(envName, getFlagEnv(flags))
+	flagEnv := getFlagEnv(flags)
+	if e, err = config.Get(envName, flagEnv); err != nil {
+		e, err = config.Set(envName, flagEnv)
 		if err != nil {
 			return err
 		}


Test patch:
diff --git a/cmd/configure_test.go b/cmd/configure_test.go
index eb29f3b..08ba3c5 100644
--- a/cmd/configure_test.go
+++ b/cmd/configure_test.go
@@ -33,7 +33,7 @@ func TestConfigure(t *testing.T) {
 	}
 
 	ctx, _, conf, _, _ = createTestCtx()
-	ctx.Flags.Environments.Set("test")
+	ctx.Flags.Environments = []string{"test"}
 	ctx.Env.Domain = "my.domain.com"
 	conf.On("Set", "test", env.Env{Domain: "my.domain.com"}).Return(nil, nil)
 	conf.On("Save").Return(nil)
diff --git a/cmd/get_test.go b/cmd/get_test.go
index 46d0664..7fcf39d 100644
--- a/cmd/get_test.go
+++ b/cmd/get_test.go
@@ -34,7 +34,7 @@ func TestGet(t *testing.T) {
 	}
 
 	ctx, client, conf, _, _ = createTestCtx()
-	ctx.Flags.Environments.Set("test")
+	ctx.Flags.Environments = []string{"test"}
 	ctx.Env.Domain = "my.domain.com"
 	conf.On("Set", "test", env.Env{Domain: "my.domain.com"}).Return(nil, nil)
 	conf.On("Save").Return(nil)
diff --git a/cmd/remove_test.go b/cmd/remove_test.go
index 0668997..f3e4d41 100644
--- a/cmd/remove_test.go
+++ b/cmd/remove_test.go
@@ -54,7 +54,9 @@ func createTestCtx() (ctx *cmdutil.Ctx, client *mocks.ShopifyClient, conf *mocks
 		Conf:   conf,
 		Client: client,
 		Env:    &env.Env{},
-		Flags:  cmdutil.Flags{},
+		Flags: cmdutil.Flags{
+			Environments: []string{"development"},
+		},
 		Log:    log.New(stdOut, "", 0),
 		ErrLog: log.New(stdErr, "", 0),
 	}
diff --git a/cmd/theme_test.go b/cmd/theme_test.go
deleted file mode 100644
index c834304..0000000
--- a/cmd/theme_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package cmd
-
-// import (
-//	"bytes"
-//	"log"
-//	"testing"
-
-//	"github.com/Shopify/themekit/kit"
-//	"github.com/Shopify/themekit/kittest"
-// )
-
-// var (
-//	stdOutOutput *bytes.Buffer
-//	stdErrOutput *bytes.Buffer
-// )
-
-// func resetLog() {
-//	stdOutOutput = new(bytes.Buffer)
-//	stdErrOutput = new(bytes.Buffer)
-//	stdOut = log.New(stdOutOutput, "", 0)
-//	stdErr = log.New(stdOutOutput, "", 0)
-// }
-
-// func TestThemePreRun(t *testing.T) {
-//	server := kittest.NewTestServer()
-//	defer server.Close()
-//	defer resetArbiter()
-
-//	kit.ThemeKitReleasesURL = server.URL + "/themekit_update"
-
-//	// just making sure that it does not throw
-//	ThemeCmd.PersistentPreRun(nil, []string{})
-// }
-
-// func TestThemePostRun(t *testing.T) {
-//	defer resetArbiter()
-//	// just making sure that it does not throw
-//	ThemeCmd.PersistentPostRun(nil, []string{})
-// }
diff --git a/src/cmdutil/string_arg_array_test.go b/src/cmdutil/string_arg_array_test.go
deleted file mode 100644
index 310a604..0000000
--- a/src/cmdutil/string_arg_array_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package cmdutil
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestStringArgArray_String(t *testing.T) {
-	saa := stringArgArray{
-		values: []string{"beedrill", "pikachu"},
-	}
-	assert.Equal(t, "beedrill,pikachu", saa.String())
-}
-
-func TestStringArgArray_Set(t *testing.T) {
-	saa := stringArgArray{
-		values: []string{"vaporeon"},
-	}
-	saa.Set("drowzee")
-	assert.Equal(t, "vaporeon,drowzee", saa.String())
-	saa.Set("")
-	assert.Equal(t, "vaporeon,drowzee", saa.String())
-}
-
-func TestStringArgArray_Type(t *testing.T) {
-	saa := stringArgArray{
-		values: []string{},
-	}
-	assert.Equal(t, "string", saa.Type())
-}
-
-func TestStringArgArray_Value(t *testing.T) {
-	saa := stringArgArray{
-		values: []string{},
-	}
-	assert.Nil(t, saa.Value())
-	saa.Set("raichu")
-	assert.Equal(t, "raichu", saa.Value()[0])
-}
diff --git a/src/cmdutil/util_test.go b/src/cmdutil/util_test.go
index c3d34b7..5995542 100644
--- a/src/cmdutil/util_test.go
+++ b/src/cmdutil/util_test.go
@@ -95,25 +95,25 @@ func TestCtx_DoneTask(t *testing.T) {
 
 func TestGenerateContexts(t *testing.T) {
 	factory := func(*env.Env) (shopifyClient, error) { return nil, nil }
-	_, err := generateContexts(factory, nil, Flags{}, []string{})
-	assert.EqualError(t, err, "Could not find config file at ")
+	_, err := generateContexts(factory, nil, Flags{Environments: []string{"development"}}, []string{})
+	assert.EqualError(t, err, "invalid environment [development]: (missing store domain,missing password)")
 
 	client := new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	ctxs, err := generateContexts(factory, nil, Flags{ConfigPath: "_testdata/config.yml"}, []string{})
+	ctxs, err := generateContexts(factory, nil, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{})
 	assert.Nil(t, err)
 	assert.Equal(t, len(ctxs), 1)
 
 	client = new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
-	_, err = generateContexts(factory, nil, Flags{ConfigPath: "_testdata/config.yml", Environments: stringArgArray{[]string{"nope"}}}, []string{})
-	assert.EqualError(t, err, "Could not load any valid environments")
+	_, err = generateContexts(factory, nil, Flags{ConfigPath: "_testdata/config.yml", Environments: []string{"nope"}}, []string{})
+	assert.EqualError(t, err, "invalid environment [nope]: (missing store domain,missing password)")
 
 	client = new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, fmt.Errorf("not today") }
-	_, err = generateContexts(factory, nil, Flags{ConfigPath: "_testdata/config.yml"}, []string{})
+	_, err = generateContexts(factory, nil, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{})
 	assert.EqualError(t, err, "not today")
 }
 
@@ -126,8 +126,8 @@ func TestGetFlagEnv(t *testing.T) {
 		Proxy:        "r",
 		Timeout:      1,
 		NotifyFile:   "n",
-		IgnoredFiles: stringArgArray{[]string{"i"}},
-		Ignores:      stringArgArray{[]string{"c"}},
+		IgnoredFiles: []string{"i"},
+		Ignores:      []string{"c"},
 	}
 
 	e := env.Env{
@@ -160,30 +160,29 @@ func TestGetFlagEnv(t *testing.T) {
 	assert.Equal(t, e, getFlagEnv(flags))
 }
 
-func TestShouldUseEnvironment(t *testing.T) {
+func TestExpandEnvironments(t *testing.T) {
 	testcases := []struct {
-		envs, should, shouldNot []string
-		all                     bool
+		envs, conf, expected []string
+		all                  bool
 	}{
-		{envs: []string{}, should: []string{"development"}, shouldNot: []string{"production"}},
-		{envs: []string{"production"}, should: []string{"production"}, shouldNot: []string{"development"}},
-		{envs: []string{"p*", "other"}, should: []string{"production", "prod", "puddle", "other"}, shouldNot: []string{"development"}},
-		{all: true, envs: []string{}, should: []string{"nope"}, shouldNot: []string{}},
+		{envs: []string{}, conf: []string{"development"}, expected: []string{}},
+		{envs: []string{"production"}, conf: []string{"production", "foob"}, expected: []string{"production"}},
+		{envs: []string{"production"}, conf: []string{}, expected: []string{"production"}},
+		{envs: []string{"p*"}, conf: []string{"production", "prod", "puddle", "other"}, expected: []string{"production", "prod", "puddle"}},
+		{all: true, envs: []string{}, conf: []string{"dev", "prod", "test"}, expected: []string{"dev", "prod", "test"}},
 	}
 
-	for i, testcase := range testcases {
+	for _, testcase := range testcases {
 		flags := Flags{
-			Environments: stringArgArray{testcase.envs},
+			Environments: testcase.envs,
 			AllEnvs:      testcase.all,
 		}
-
-		for _, name := range testcase.should {
-			assert.True(t, shouldUseEnvironment(flags, name), fmt.Sprintf("testcase number %v name: %v", i, name))
-		}
-
-		for _, name := range testcase.shouldNot {
-			assert.False(t, shouldUseEnvironment(flags, name), fmt.Sprintf("testcase number %v name: %v", i, name))
+		envs := map[string]*env.Env{}
+		for _, name := range testcase.conf {
+			envs[name] = nil
 		}
+		got := expandEnvironments(flags, envs)
+		assert.ElementsMatch(t, testcase.expected, got)
 	}
 }
 
@@ -192,22 +191,18 @@ func TestForEachClient(t *testing.T) {
 	safeHandler := func(*Ctx) error { return nil }
 	errHandler := func(*Ctx) error { return gandalfErr }
 
-	factory := func(*env.Env) (shopifyClient, error) { return nil, nil }
-	err := forEachClient(factory, Flags{}, []string{}, safeHandler)
-	assert.EqualError(t, err, "Could not find config file at ")
-
 	client := new(mocks.ShopifyClient)
-	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
+	factory := func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forEachClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, safeHandler)
+	err := forEachClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, safeHandler)
 	assert.Nil(t, err)
 
 	client = new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forEachClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, errHandler)
+	err = forEachClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, errHandler)
 	assert.EqualError(t, err, gandalfErr.Error())
 
 	count := 0
@@ -222,7 +217,7 @@ func TestForEachClient(t *testing.T) {
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forEachClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, handler)
+	err = forEachClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, handler)
 	assert.EqualError(t, err, "nope not at all")
 	assert.Equal(t, 2, count)
 
@@ -238,7 +233,7 @@ func TestForEachClient(t *testing.T) {
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	forEachClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, handler)
+	forEachClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, handler)
 	assert.Contains(t, stdErr.String(), "finished command with errors")
 }
 
@@ -247,29 +242,25 @@ func TestForSingleClient(t *testing.T) {
 	safeHandler := func(*Ctx) error { return nil }
 	errHandler := func(*Ctx) error { return gandalfErr }
 
-	factory := func(*env.Env) (shopifyClient, error) { return nil, nil }
-	err := forSingleClient(factory, Flags{}, []string{}, safeHandler)
-	assert.EqualError(t, err, "Could not find config file at ")
-
 	client := new(mocks.ShopifyClient)
-	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
+	factory := func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forSingleClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, safeHandler)
+	err := forSingleClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, safeHandler)
 	assert.Nil(t, err)
 
 	client = new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forSingleClient(factory, Flags{ConfigPath: "_testdata/config.yml", Environments: stringArgArray{[]string{"*"}}}, []string{}, safeHandler)
+	err = forSingleClient(factory, Flags{ConfigPath: "_testdata/config.yml", Environments: []string{"*"}}, []string{}, safeHandler)
 	assert.EqualError(t, err, "more than one environment specified for a single environment command")
 
 	client = new(mocks.ShopifyClient)
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forSingleClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, errHandler)
+	err = forSingleClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, errHandler)
 	assert.EqualError(t, err, gandalfErr.Error())
 
 	count := 0
@@ -284,7 +275,7 @@ func TestForSingleClient(t *testing.T) {
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	err = forSingleClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, handler)
+	err = forSingleClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, handler)
 	assert.EqualError(t, err, "nope not at all")
 	assert.Equal(t, 2, count)
 
@@ -300,7 +291,7 @@ func TestForSingleClient(t *testing.T) {
 	factory = func(*env.Env) (shopifyClient, error) { return client, nil }
 	client.On("GetShop").Return(shopify.Shop{}, nil)
 	client.On("Themes").Return([]shopify.Theme{}, nil)
-	forSingleClient(factory, Flags{ConfigPath: "_testdata/config.yml"}, []string{}, handler)
+	forSingleClient(factory, Flags{Environments: []string{"development"}, ConfigPath: "_testdata/config.yml"}, []string{}, handler)
 	assert.Contains(t, stdErr.String(), "finished command with errors")
 }