Repository: nats-io/nsc
Pull request #7 (2018-12-02 01:42:45)

Operators


Patch:
diff --git a/.gitignore b/.gitignore
index 6df26f9..8170324 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,5 @@ build/
 cov/
 .idea/
 .vscode/
+coverage.out
 
diff --git a/.travis.yml b/.travis.yml
index a6ae744..3a0edbb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,9 +10,6 @@ env:
 
 install:
   - go get github.com/mattn/goveralls
-  - go get github.com/wadey/gocovmerge
-
-
 
 #  - go get -u honnef.co/go/tools/cmd/megacheck
 #  - go get -u github.com/client9/misspell/cmd/misspell
diff --git a/Makefile b/Makefile
index c08034a..ebfc3bc 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ BUILD_OS:=`go env GOOS`
 BUILD_OS_ARCH:=`go env GOARCH`
 BUILD_OS_GOPATH=`go env GOPATH`
 
-.PHONY: build
+.PHONY: build test
 
 build: fmt compile test
 
@@ -25,15 +25,14 @@ compile:
 install: build
 	cp $(BUILD_DIR)/$(BUILD_OS)_$(BUILD_OS_ARCH)/* $(BUILD_OS_GOPATH)/bin
 
+install-no-test:
+	cp $(BUILD_DIR)/$(BUILD_OS)_$(BUILD_OS_ARCH)/* $(BUILD_OS_GOPATH)/bin
+
 cover: test
-	gocovmerge ./cov/*.out > build/coverage.out
-	go tool cover -html=build/coverage.out
+	go tool cover -html=./coverage.out
 
 test: fmt
 	go vet ./...
-	rm -rf ./cov
-	mkdir cov
-	go test -covermode=atomic -coverprofile=./cov/cli.out ./cli
-	go test -covermode=atomic -coverprofile=./cov/cmd.out ./cmd
-	go test -covermode=atomic -coverprofile=./cov/cmdstore.out ./cmd/store
+	rm -rf ./coverage.out
+	go test -coverpkg=./... -coverprofile=./coverage.out ./...
 
diff --git a/cmd/addaccount.go b/cmd/addaccount.go
index a6597ca..d33a755 100644
--- a/cmd/addaccount.go
+++ b/cmd/addaccount.go
@@ -24,7 +24,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func createAddAccountCmd() *cobra.Command {
+func CreateAddAccountCmd() *cobra.Command {
 	var params AddAccountParams
 	cmd := &cobra.Command{
 		Use:          "account",
@@ -51,7 +51,7 @@ func createAddAccountCmd() *cobra.Command {
 }
 
 func init() {
-	addCmd.AddCommand(createAddAccountCmd())
+	addCmd.AddCommand(CreateAddAccountCmd())
 }
 
 type AddAccountParams struct {
diff --git a/cmd/addserver.go b/cmd/addserver.go
index 4d33507..6c1f219 100644
--- a/cmd/addserver.go
+++ b/cmd/addserver.go
@@ -66,9 +66,6 @@ type AddServerParams struct {
 }
 
 func (p *AddServerParams) SetDefaults(ctx ActionCtx) error {
-	if ctx.StoreCtx().Store.IsManaged() {
-		return fmt.Errorf("servers cannot be created on managed configurations")
-	}
 	p.ClusterContextParams.SetDefaults(ctx)
 	p.SignerParams.SetDefaults(nkeys.PrefixByteCluster, false, ctx)
 	p.create = true
diff --git a/cmd/adduser.go b/cmd/adduser.go
index 0e06586..c650b17 100644
--- a/cmd/adduser.go
+++ b/cmd/adduser.go
@@ -25,7 +25,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func createAddUserCmd() *cobra.Command {
+func CreateAddUserCmd() *cobra.Command {
 	var params AddUserParams
 	cmd := &cobra.Command{
 		Use:          "user",
@@ -71,7 +71,7 @@ nsc add user --name u --tag test,service_a`,
 }
 
 func init() {
-	addCmd.AddCommand(createAddUserCmd())
+	addCmd.AddCommand(CreateAddUserCmd())
 }
 
 type AddUserParams struct {
diff --git a/cmd/common.go b/cmd/common.go
index 1bc54ad..8f290f4 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -74,6 +75,19 @@ func IsStdOut(fp string) bool {
 	return fp == "--"
 }
 
+func WriteJson(fp string, v interface{}) error {
+	data, err := json.Marshal(v)
+	if err != nil {
+		return fmt.Errorf("error marshalling: %v", err)
+	}
+
+	if err := ioutil.WriteFile(fp, data, 0600); err != nil {
+		return fmt.Errorf("error writing %q: %v", fp, err)
+	}
+
+	return nil
+}
+
 func Write(fp string, data []byte) error {
 	var err error
 	var f *os.File
@@ -98,10 +112,22 @@ func Write(fp string, data []byte) error {
 	return nil
 }
 
+func ReadJson(fp string, v interface{}) error {
+	data, err := Read(fp)
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(data, &v)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func Read(fp string) ([]byte, error) {
 	afp, err := filepath.Abs(fp)
 	if err != nil {
-		return nil, fmt.Errorf("error reading %q: %v", fp, err)
+		return nil, fmt.Errorf("error converting abs %q: %v", fp, err)
 	}
 	return ioutil.ReadFile(afp)
 }
@@ -309,3 +335,28 @@ func LoadFromURL(url string) ([]byte, error) {
 	data := buf.Bytes()
 	return data, nil
 }
+
+func IsValidDir(dir string) error {
+	fi, err := os.Stat(dir)
+	if err != nil {
+		return err
+	}
+	if !fi.IsDir() {
+		return fmt.Errorf("not a directory")
+	}
+	return nil
+}
+
+func MaybeMakeDir(dir string) error {
+	fi, err := os.Stat(dir)
+	if err != nil && os.IsNotExist(err) {
+		if err := os.Mkdir(dir, 0700); err != nil {
+			return fmt.Errorf("error creating %q: %v", dir, err)
+		}
+	} else if err != nil {
+		return fmt.Errorf("error stat'ing %q: %v", dir, err)
+	} else if !fi.IsDir() {
+		return fmt.Errorf("%q already exists and it is not a dir", dir)
+	}
+	return nil
+}
diff --git a/cmd/contextconfig.go b/cmd/contextconfig.go
new file mode 100644
index 0000000..bfbafc6
--- /dev/null
+++ b/cmd/contextconfig.go
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/nats-io/nsc/cmd/store"
+)
+
+type ContextConfig struct {
+	StoreRoot string `json:"store_root"` // where the projects are
+	Operator  string `json:"operator"`
+	Account   string `json:"account"`
+	Cluster   string `json:"cluster"`
+}
+
+func NewContextConfig(storeRoot string) (*ContextConfig, error) {
+	var err error
+	ctx := ContextConfig{}
+	if storeRoot != "" {
+		ctx.StoreRoot, err = filepath.Abs(storeRoot)
+		if err != nil {
+			return nil, err
+		}
+		ctx.SetDefaults()
+	}
+	return &ctx, nil
+
+}
+
+// deduce as much context as possible
+func (c *ContextConfig) SetDefaults() {
+	if err := IsValidDir(c.StoreRoot); err == nil {
+		operators := c.listOperators()
+		if len(operators) == 1 {
+			s, err := store.LoadStore(filepath.Join(c.StoreRoot, operators[0]))
+			if err != nil {
+				return
+			}
+			c.Operator = filepath.Base(operators[0])
+			accounts, err := s.ListSubContainers(store.Accounts)
+			if err == nil && len(accounts) == 1 {
+				c.Account = accounts[0]
+			}
+			clusters, err := s.ListSubContainers(store.Clusters)
+			if err == nil && len(clusters) == 1 {
+				c.Cluster = clusters[0]
+			}
+		}
+	}
+}
+
+func (c *ContextConfig) listOperators() []string {
+	infos, err := ioutil.ReadDir(c.StoreRoot)
+	if err != nil {
+		return nil
+	}
+	var operators []string
+	for _, v := range infos {
+		name := store.SafeName(filepath.Base(v.Name()))
+		fp := filepath.Join(c.StoreRoot, name, store.NSCFile)
+		info, err := os.Stat(fp)
+		if err == nil && info != nil {
+			operators = append(operators, v.Name())
+		}
+	}
+	return operators
+}
diff --git a/cmd/defaults.go b/cmd/defaults.go
new file mode 100644
index 0000000..95a6edb
--- /dev/null
+++ b/cmd/defaults.go
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/mitchellh/go-homedir"
+)
+
+//NscHomeEnv the folder for the config file
+const NscHomeEnv = "NSC_HOME"
+
+type ToolConfig struct {
+	ContextConfig
+	GithubUpdates string `json:"github_updates"` // git hub repo
+	LastUpdate    int64  `json:"last_update"`
+}
+
+var config ToolConfig
+var toolHome string
+
+// GetConfig returns the global config
+func GetConfig() *ToolConfig {
+	return &config
+}
+
+func ResetConfigForTests() {
+	config = ToolConfig{}
+}
+
+func SetStoreRoot(fp string) error {
+	config.StoreRoot = fp
+	return nil
+}
+
+func SetOperator(operator string) {
+	config.Operator = operator
+}
+
+func SetAccount(account string) {
+	config.Account = account
+}
+
+func SetCluster(cluster string) {
+	config.Cluster = cluster
+}
+
+func LoadOrInit(github string, toolHomeEnv string) error {
+	var err error
+	if toolHomeEnv == "" {
+		return errors.New("toolHomeEnv is required")
+	}
+
+	toolHome, err = initToolHome(toolHomeEnv)
+	if err != nil {
+		return err
+	}
+
+	if err := config.load(); err != nil {
+		return err
+	}
+
+	// is the struct modified from the file
+	if (ToolConfig{}) == config {
+		config.GithubUpdates = github
+
+		// ~/.ngs_cli/nats
+		config.StoreRoot = filepath.Join(toolHome, "nats")
+		if err := MaybeMakeDir(config.StoreRoot); err != nil {
+			return fmt.Errorf("error creating store root: %v", err)
+		}
+
+		// load any default entries if there are any
+		config.SetDefaults()
+
+		if err := config.Save(); err != nil {
+			return err
+		}
+	}
+	// trigger updating defaults
+
+	return nil
+}
+
+func (d *ToolConfig) load() error {
+	err := ReadJson(d.configFile(), &config)
+	if err != nil && os.IsNotExist(err) {
+		return nil
+	}
+	return err
+}
+
+func (d *ToolConfig) Save() error {
+	return WriteJson(d.configFile(), d)
+}
+
+// GetToolHome returns the . folder used fro this CLIs config and optionally the projects
+func initToolHome(envVarName string) (string, error) {
+	toolHome = os.Getenv(envVarName)
+
+	if toolHome == "" {
+		exeName := filepath.Base(os.Args[0])
+		dir, err := homedir.Dir()
+		if err != nil {
+			// shouldn't prevent if there's an error
+			return "", fmt.Errorf("error getting homedir: %v", err.Error())
+		}
+		toolHome = filepath.Join(dir, fmt.Sprintf(".%scli", exeName))
+	}
+
+	if err := MaybeMakeDir(toolHome); err != nil {
+		return "", fmt.Errorf("error creating tool home %q: %v", toolHome, err)
+	}
+
+	return toolHome, nil
+
+}
+
+func (d *ToolConfig) configFile() string {
+	configFileName := fmt.Sprintf("%s.json", filepath.Base(os.Args[0]))
+	return filepath.Join(toolHome, configFileName)
+}
diff --git a/cmd/env.go b/cmd/env.go
index 4995f9d..94590cd 100644
--- a/cmd/env.go
+++ b/cmd/env.go
@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"os"
+	"path/filepath"
 
 	"github.com/nats-io/nsc/cmd/store"
 	"github.com/spf13/cobra"
@@ -50,7 +51,7 @@ func createEnvCmd() *cobra.Command {
 				table.AddRow("Project Dir", "", "not in a configuration directory")
 			} else {
 				table.AddRow("Name", "", s.Info.EnvironmentName)
-				table.AddRow("Project Dir", "", s.Dir)
+				table.AddRow("Project Dir", "", filepath.Dir(s.Dir))
 
 				var ctx *store.Context
 				if s != nil {
@@ -65,6 +66,13 @@ func createEnvCmd() *cobra.Command {
 				}
 			}
 
+			table.AddSeparator()
+			conf := GetConfig()
+			table.AddRow("Stores Dir", "", conf.StoreRoot)
+			table.AddRow("Default Operator", "", conf.Operator)
+			table.AddRow("Default Account", "", conf.Account)
+			table.AddRow("Default Cluster", "", conf.Cluster)
+
 			cmd.Println(table.Render())
 
 			return nil
diff --git a/cmd/init.go b/cmd/init.go
index 3a0a2ad..aba7d89 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -17,7 +17,6 @@ package cmd
 
 import (
 	"fmt"
-	"os"
 	"path/filepath"
 
 	"github.com/nats-io/nkeys"
@@ -32,16 +31,11 @@ func CreateInitCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "init",
 		Short: "Init a configuration directory",
-		Example: `init --name project
+		Example: `init --name operatorname
 init --interactive
 `,
-		SilenceErrors: true,
-		SilenceUsage:  true,
+		SilenceUsage: true,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			s, _ := GetStore()
-			if s != nil {
-				return fmt.Errorf("%q is already a store", s.Dir)
-			}
 
 			p.SetDefaults()
 
@@ -54,6 +48,7 @@ init --interactive
 			}
 
 			if err := p.Run(); err != nil {
+				fmt.Printf("%v\n", err)
 				return err
 			}
 
@@ -80,15 +75,15 @@ init --interactive
 				cmd.Println(table.Render())
 			}
 
-			cmd.Println("Success! - initialized project directory")
+			if p.operator.create {
+				cmd.Println("Success! - initialized project directory")
+			}
 
 			return nil
 		},
 	}
 
-	cmd.Flags().StringVarP(&p.projectRoot, "dir", "d", ".", "project directory")
-
-	cmd.Flags().StringVarP(&p.environmentName, "name", "n", "", "name for the configuration environment, if not specified uses <dirname>")
+	cmd.Flags().StringVarP(&p.environmentName, "name", "n", "test", "name for the configuration environment")
 
 	cmd.Flags().StringVarP(&p.operator.name, "operator-name", "", "", "operator name (default '<name>_operator')")
 	cmd.Flags().StringVarP(&p.operator.keyPath, "operator-key", "", "", "operator keypath (default generated)")
@@ -122,7 +117,7 @@ func init() {
 }
 
 type InitParams struct {
-	projectRoot     string
+	storeRoot       string
 	environmentName string
 	operator        Entity
 	account         Entity
@@ -143,15 +138,14 @@ func (p *InitParams) SetDefaults() error {
 	p.user.kind = nkeys.PrefixByteUser
 
 	var err error
-	d := p.projectRoot
-	p.projectRoot, err = filepath.Abs(p.projectRoot)
+	p.storeRoot, err = filepath.Abs(GetConfig().StoreRoot)
 	if err != nil {
-		return fmt.Errorf("error calculating the absolute filepath for %q: %v", d, err)
+		return fmt.Errorf("error calculating the absolute filepath for %q: %v", p.storeRoot, err)
 	}
 
 	// if defaults are set we are not prompting
 	if p.environmentName == "" {
-		p.environmentName = filepath.Base(p.projectRoot)
+		p.environmentName = "test"
 	}
 	if !InteractiveFlag {
 		p.SetDefaultNames()
@@ -159,7 +153,7 @@ func (p *InitParams) SetDefaults() error {
 	return nil
 }
 
-func (p *InitParams) SetDefaultNames() error {
+func (p *InitParams) SetDefaultNames() {
 	// set the operator name
 	if p.operator.name == "" {
 		p.operator.name = fmt.Sprintf("%s_operator", p.environmentName)
@@ -180,7 +174,6 @@ func (p *InitParams) SetDefaultNames() error {
 	if p.server.name == "" {
 		p.server.name = "localhost"
 	}
-	return nil
 }
 
 func (p *InitParams) Interactive(cmd *cobra.Command) error {
@@ -209,9 +202,7 @@ func (p *InitParams) Interactive(cmd *cobra.Command) error {
 		return err
 	}
 
-	if err = p.SetDefaultNames(); err != nil {
-		return err
-	}
+	p.SetDefaultNames()
 
 	if err := p.operator.Edit(); err != nil {
 		return err
@@ -309,14 +300,18 @@ func (p *InitParams) Run() error {
 	var operator *store.NamedKey
 	if p.operator.create {
 		operator = &store.NamedKey{Name: p.operator.name, KP: p.operator.kp}
+	} else {
+		operator = &store.NamedKey{Name: p.environmentName}
 	}
-	_, err := store.CreateStore(p.environmentName, p.projectRoot, operator)
+
+	_, err := store.CreateStore(p.environmentName, p.storeRoot, operator)
 	if err != nil {
 		return err
 	}
 
-	if err = os.Chdir(p.projectRoot); err != nil {
-		return fmt.Errorf("error changing dir to %q: %v", p.projectRoot, err)
+	GetConfig().Operator = operator.Name
+	if err := GetConfig().Save(); err != nil {
+		return err
 	}
 
 	containers := p.Containers()
diff --git a/cmd/list.go b/cmd/list.go
new file mode 100644
index 0000000..d49e11a
--- /dev/null
+++ b/cmd/list.go
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+// addCmd represents the add command
+var listCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List assets such as accounts, imports, users, clusters, servers",
+}
+
+func init() {
+	GetRootCmd().AddCommand(listCmd)
+}
diff --git a/cmd/listoperators.go b/cmd/listoperators.go
new file mode 100644
index 0000000..8d1b68a
--- /dev/null
+++ b/cmd/listoperators.go
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+)
+
+func createListOperatorsCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:          "list",
+		Short:        "List operators",
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			config := GetConfig()
+			if config.StoreRoot == "" {
+				return errors.New("no store set - `set store /dir`")
+			}
+
+			return nil
+		},
+	}
+
+	return cmd
+}
diff --git a/cmd/root.go b/cmd/root.go
index 50babd2..7f6e0ff 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -16,7 +16,9 @@
 package cmd
 
 import (
+	"errors"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -29,7 +31,6 @@ import (
 	"github.com/spf13/viper"
 )
 
-const SeedKeyEnv = "NSC_SEED_KEY"
 const TestEnv = "NSC_TEST"
 
 var KeyPathFlag string
@@ -45,16 +46,45 @@ var show, _ = strconv.ParseBool(os.Getenv(TestEnv))
 type InterceptorFn func(ctx ActionCtx, params interface{}) error
 
 func GetStore() (*store.Store, error) {
-	if ngsStore == nil {
-		storeDir, err := FindCurrentStoreDir()
+	config := GetConfig()
+	if config.StoreRoot == "" {
+		return nil, errors.New("no stores available")
+	}
+	if err := IsValidDir(config.StoreRoot); err != nil {
+		return nil, err
+	}
+
+	if config.Operator == "" {
+		infos, err := ioutil.ReadDir(config.StoreRoot)
 		if err != nil {
 			return nil, err
 		}
-		ngsStore, err = store.LoadStore(storeDir)
-		if err != nil {
-			return nil, err
+		var operators []string
+		for _, v := range infos {
+			if v.IsDir() {
+				s, _ := store.LoadStore(v.Name())
+				if s != nil {
+					operators = append(operators, filepath.Dir(v.Name()))
+				}
+			}
+		}
+
+		if len(operators) == 1 {
+			config.Operator = operators[0]
+		} else {
+			return nil, fmt.Errorf("set an operator")
 		}
 	}
+
+	fp := filepath.Join(config.StoreRoot, config.Operator)
+	ngsStore, err := store.LoadStore(fp)
+	if err != nil {
+		return nil, err
+	}
+
+	if config.Account != "" {
+		ngsStore.DefaultAccount = config.Account
+	}
 	return ngsStore, nil
 }
 
@@ -146,34 +176,3 @@ func initConfig() {
 		fmt.Println("Using config file:", viper.ConfigFileUsed())
 	}
 }
-
-// FindCurrentStoreDir tries to find a store director
-// starting with the current working dir
-func FindCurrentStoreDir() (string, error) {
-	wd, err := os.Getwd()
-	if err != nil {
-		return "", err
-	}
-	return FindStoreDir(wd)
-}
-
-// FindStore starts at the directory provided and tries to
-// find a directory containing the public key. This function
-// checks dir and then works its way up the folder path.
-func FindStoreDir(dir string) (string, error) {
-	var err error
-
-	pkp := filepath.Join(dir, store.NSCFile)
-
-	if _, err := os.Stat(pkp); os.IsNotExist(err) {
-		parent := filepath.Dir(dir)
-
-		if parent == dir {
-			return "", fmt.Errorf("no store directory found")
-		}
-
-		return FindStoreDir(parent)
-	}
-
-	return dir, err
-}
diff --git a/cmd/set.go b/cmd/set.go
new file mode 100644
index 0000000..2ac6e12
--- /dev/null
+++ b/cmd/set.go
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+func createSetContextCmd() *cobra.Command {
+	var params SetContextParams
+	cmd := &cobra.Command{
+		Use:          "set",
+		Short:        "Set the context for the stores, operator, account or cluster",
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+
+			copy := *GetConfig()
+
+			//var err error
+			//if params.StoreRoot != "" {
+			//	params.StoreRoot, err = filepath.Abs(params.StoreRoot)
+			//	if err != nil {
+			//		return err
+			//	}
+			//	if err = IsValidDir(params.StoreRoot); err != nil {
+			//		return err
+			//	}
+			//	copy.StoreRoot = params.StoreRoot
+			//}
+			//
+			//if params.Operator != "" {
+			//	copy.Operator = store.SafeName(params.Operator)
+			//}
+			//
+			//if copy.Operator == "" {
+			//	infos, err := ioutil.ReadDir(copy.StoreRoot)
+			//	if err != nil {
+			//		return fmt.Errorf("error listing %q: %v", params.StoreRoot, err)
+			//	}
+			//	var operators []string
+			//	for _, v := range infos {
+			//		name := store.SafeName(filepath.Base(v.Name()))
+			//		fp := filepath.Join(copy.StoreRoot, name, store.NSCFile)
+			//		info, err := os.Stat(fp)
+			//		if err == nil && info != nil {
+			//			operators = append(operators, v.Name())
+			//		}
+			//	}
+			//
+			//	if len(operators) == 0 {
+			//		return fmt.Errorf("no operators in %q", params.StoreRoot)
+			//	}
+			//	if len(operators) == 1 {
+			//		copy.Operator =
+			//	}
+			//}
+			//
+			//s, err := store.LoadStore(sp)
+			//if err != nil {
+			//	return err
+			//}
+			//
+			//if params.Account != "" {
+			//	copy.Account = store.SafeName(params.Account)
+			//
+			//	accounts, err := s.ListEntries(store.Accounts)
+			//	if err != nil {
+			//		return err
+			//	}
+			//	found := false
+			//	for _, v := range accounts {
+			//		if copy.Account == v {
+			//			found = true
+			//			break
+			//		}
+			//	}
+			//	if !found {
+			//		return fmt.Errorf("account %q is not contained by operator %q", copy.Account, copy.Operator)
+			//	}
+			//}
+			//
+			//if params.Cluster != "" {
+			//	copy.Cluster = store.SafeName(params.Cluster)
+			//
+			//	clusters, err := s.ListEntries(store.Clusters)
+			//	if err != nil {
+			//		return err
+			//	}
+			//	found := false
+			//	for _, v := range clusters {
+			//		if copy.Cluster == v {
+			//			found = true
+			//			break
+			//		}
+			//	}
+			//	if !found {
+			//		return fmt.Errorf("cluster %q is not contained by operator %q", copy.Cluster, copy.Operator)
+			//	}
+			//}
+
+			return copy.Save()
+		},
+	}
+
+	cmd.Flags().StringVarP(&params.StoreRoot, "store", "s", "", "store directory")
+	cmd.Flags().StringVarP(&params.StoreRoot, "operator", "o", "", "operator name")
+	cmd.Flags().StringVarP(&params.StoreRoot, "account", "a", "", "account name")
+	cmd.Flags().StringVarP(&params.StoreRoot, "cluster", "c", "", "cluster name")
+
+	return cmd
+}
+
+func init() {
+	rootCmd.AddCommand(createSetContextCmd())
+}
+
+type SetContextParams struct {
+	StoreRoot string
+	Operator  string
+	Account   string
+	Cluster   string
+}
diff --git a/cmd/store/store.go b/cmd/store/store.go
index ea47515..cae935a 100644
--- a/cmd/store/store.go
+++ b/cmd/store/store.go
@@ -43,8 +43,9 @@ var standardDirs = []string{Accounts}
 // Store is a directory that contains nsc assets
 type Store struct {
 	sync.Mutex
-	Dir  string
-	Info Info
+	Dir            string
+	Info           Info
+	DefaultAccount string
 }
 
 type Info struct {
@@ -71,46 +72,44 @@ func SafeName(n string) string {
 
 // CreateStore creates a new Store in the specified directory.
 // CreateStore will create the necessary directories and store the public key.
-func CreateStore(env string, dir string, root *NamedKey) (*Store, error) {
+func CreateStore(env string, operatorsDir string, operator *NamedKey) (*Store, error) {
 	var err error
 
+	root := filepath.Join(operatorsDir, operator.Name)
 	s := &Store{
-		Dir: dir,
+		Dir: root,
 		Info: Info{
-			EnvironmentName: env,
+			EntityName:      operator.Name,
+			EnvironmentName: operator.Name,
 			Version:         Version,
 		},
 	}
-	if root != nil {
-		s.Info.EntityName = root.Name
-	} else {
-		s.Info.Managed = true
-	}
 
-	if _, err := os.Stat(dir); os.IsNotExist(err) {
-		if err := os.MkdirAll(dir, 0700); err != nil {
+	if _, err := os.Stat(root); os.IsNotExist(err) {
+		if err := os.MkdirAll(root, 0700); err != nil {
 			return nil, err
 		}
 	}
 
-	files, err := ioutil.ReadDir(dir)
+	files, err := ioutil.ReadDir(root)
 	if err != nil {
 		return nil, err
 	}
 
 	if len(files) != 0 {
-		return nil, fmt.Errorf("%q is not empty, only an empty folder can be used for a new project", dir)
+		return nil, fmt.Errorf("operator %q already exists in %q", operator.Name, operatorsDir)
 	}
 
-	if !s.Info.Managed {
-		token, err := s.createOperatorToken(root)
+	if operator.KP != nil {
+		token, err := s.createOperatorToken(operator)
 		if err != nil {
 			return nil, err
 		}
-
 		if err := s.StoreClaim([]byte(token)); err != nil {
 			return nil, fmt.Errorf("error writing operator jwt: %v", err)
 		}
+	} else {
+		s.Info.Managed = true
 	}
 
 	d, err := json.Marshal(s.Info)
@@ -263,7 +262,6 @@ func (s *Store) ListEntries(name ...string) ([]string, error) {
 		for _, v := range infos {
 			if !v.IsDir() && IsJwtName(v.Name()) {
 				entries = append(entries, PlainName(v.Name()))
-
 			}
 		}
 	}
@@ -575,14 +573,23 @@ func (s *Store) GetContext() (*Context, error) {
 			return nil, err
 		}
 	}
+
 	// try to set a default account
-	ac, err := s.LoadDefaultEntity(Accounts)
+	var ac *jwt.GenericClaims
+
+	if s.DefaultAccount != "" {
+		ac, err = s.LoadClaim(Accounts, s.DefaultAccount, JwtName(s.DefaultAccount))
+	} else {
+		ac, err = s.LoadDefaultEntity(Accounts)
+	}
+
 	if err != nil {
 		return nil, err
 	}
 	if ac != nil {
 		c.SetContext(ac.Name, ac.Subject)
 	}
+
 	// try to set a default cluster
 	cc, err := s.LoadDefaultEntity(Clusters)
 	if err != nil {
diff --git a/cmd/update.go b/cmd/update.go
index f18e892..c0a6def 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -16,16 +16,11 @@
 package cmd
 
 import (
-	"encoding/json"
 	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
 	"time"
 
 	"github.com/blang/semver"
 	"github.com/briandowns/spinner"
-	"github.com/mitchellh/go-homedir"
 	"github.com/nats-io/nsc/cli"
 	"github.com/rhysd/go-github-selfupdate/selfupdate"
 	"github.com/spf13/cobra"
@@ -61,7 +56,7 @@ update --release-notes
 			wait.Prefix = "Downloading latest version "
 			_ = wait.Color("italic")
 			wait.Start()
-			latest, err := selfupdate.UpdateSelf(v, repository)
+			latest, err := selfupdate.UpdateSelf(v, GetConfig().GithubUpdates)
 			if err != nil {
 				cmd.SilenceErrors = false
 				return err
@@ -88,35 +83,19 @@ func init() {
 	GetRootCmd().AddCommand(createUpdateCommand())
 }
 
-var repository string
-
-func SetUpdateRespository(githubrepo string) {
-	repository = githubrepo
-}
-
 type SelfUpdate struct {
-	LastCheck int64 `json:"last_check"`
 }
 
-// NewSelfUpdate
+// NewSelfUpdate creates a new self update object
 func NewSelfUpdate() (*SelfUpdate, error) {
-	if repository == "" {
+
+	if GetConfig().GithubUpdates == "" {
 		return nil, fmt.Errorf("unable to check for updates - repository not set")
 	}
 
-	var u SelfUpdate
-	fn, err := u.infoFile()
-	d, err := ioutil.ReadFile(fn)
-	if err != nil && os.IsNotExist(err) {
-		// go check
-	} else if err != nil {
-		return nil, fmt.Errorf("error checking self update: %v", err.Error())
-	} else {
-		if err := json.Unmarshal(d, &u); err != nil {
-			return nil, fmt.Errorf("error reading %q: %v", fn, err.Error())
-		}
-	}
-	return &u, nil
+	u := &SelfUpdate{}
+
+	return u, nil
 }
 
 func (u *SelfUpdate) Run() (*semver.Version, error) {
@@ -132,43 +111,20 @@ func (u *SelfUpdate) Run() (*semver.Version, error) {
 }
 
 func (u *SelfUpdate) shouldCheck() bool {
+	config := GetConfig()
 	now := time.Now().Unix()
-	return u.LastCheck == 0 || now-u.LastCheck > int64(time.Hour*24)
+	return config.LastUpdate == 0 || now-config.LastUpdate > int64(time.Hour*24)
 }
 
 func (u *SelfUpdate) updateLastChecked() error {
-	u.LastCheck = time.Now().Unix()
-	d, err := json.Marshal(u)
-	if err != nil {
-		return fmt.Errorf("error serialzing selfupdate info: %v", err)
-	}
-	fp, err := u.infoFile()
-	if err != nil {
-		return fmt.Errorf("error getting path for selfupdate info file: %v", err)
-	}
-	dir := filepath.Dir(fp)
-	if err := os.MkdirAll(dir, 0700); err != nil {
-		return fmt.Errorf("error creating dir %q: %v", dir, err)
-
-	}
-	if err := ioutil.WriteFile(fp, d, 0600); err != nil {
-		return fmt.Errorf("error writing %q: %v", fp, err)
-	}
+	config := GetConfig()
+	config.LastUpdate = time.Now().Unix()
+	config.Save()
 	return nil
 }
 
-func (u *SelfUpdate) infoFile() (string, error) {
-	dir, err := homedir.Dir()
-	if err != nil {
-		// shouldn't prevent if there's an error
-		return "", fmt.Errorf("error getting homedir: %v", err.Error())
-	}
-	exeName := filepath.Base(os.Args[0])
-	fn := filepath.Join(dir, fmt.Sprintf(".%scli/%s.ini", exeName, exeName))
-	return fn, nil
-}
-
 func (u *SelfUpdate) doCheck() (*semver.Version, error) {
+	config := GetConfig()
 	have := semver.MustParse(GetRootCmd().Version)
 	wait := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
 	wait.Prefix = "Checking for latest version "
@@ -176,7 +132,7 @@ func (u *SelfUpdate) doCheck() (*semver.Version, error) {
 	wait.Start()
 	defer wait.Stop()
 
-	latest, found, err := selfupdate.DetectLatest(repository)
+	latest, found, err := selfupdate.DetectLatest(config.GithubUpdates)
 	if err != nil {
 		return nil, fmt.Errorf("error checking version: %v", err)
 	} else if found && latest.Version.GT(have) {
diff --git a/main.go b/main.go
index ce4aeb6..9016689 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"log"
 	"strings"
 
 	"github.com/nats-io/nsc/cmd"
@@ -15,22 +16,26 @@ func main() {
 		version = version[1:]
 	}
 	cmd.GetRootCmd().Version = version
-	cmd.SetUpdateRespository("nats-io/nsc")
+	//cmd.SetUpdateRespository("nats-io/nsc")
 
-	u, err := cmd.NewSelfUpdate()
-	if err != nil {
-		// not fatal
-		cmd.GetRootCmd().Printf("error checking for updates: %v\n", err)
-	}
-	if u != nil {
-		m, err := u.Run()
-		if err != nil {
-			cmd.GetRootCmd().Printf("error checking for updates: %v\n", err)
-		}
-		if m != nil {
-			cmd.GetRootCmd().Printf("new version available %s - `nsc update` to update.\n", m.String())
-		}
+	if err := cmd.LoadOrInit("nats-io/nsc", cmd.NscHomeEnv); err != nil {
+		log.Fatal(err)
 	}
 
+	//u, err := cmd.NewSelfUpdate()
+	//if err != nil {
+	//	// not fatal
+	//	cmd.GetRootCmd().Printf("error checking for updates: %v\n", err)
+	//}
+	//if u != nil {
+	//	m, err := u.Run()
+	//	if err != nil {
+	//		cmd.GetRootCmd().Printf("error checking for updates: %v\n", err)
+	//	}
+	//	if m != nil {
+	//		cmd.GetRootCmd().Printf("new version available %s - `nsc update` to update.\n", m.String())
+	//	}
+	//}
+
 	cmd.Execute()
 }
diff --git a/scripts/cov.sh b/scripts/cov.sh
index fa2db11..2c66892 100755
--- a/scripts/cov.sh
+++ b/scripts/cov.sh
@@ -2,13 +2,8 @@
 # this script is intended for travis - `make cover`
 # Run from directory above via ./scripts/cov.sh
 
-rm -rf ./cov
-mkdir cov
-go test -covermode=atomic -coverprofile=./cov/cli.out ./cli
-go test -covermode=atomic -coverprofile=./cov/cmd.out ./cmd
-go test -covermode=atomic -coverprofile=./cov/cmdstore.out ./cmd/store
-gocovmerge ./cov/*.out > ./coverage.out
-rm -rf ./cov
+rm -rf ./coverage.out
+go test -coverpkg=./... -coverprofile=./coverage.out ./...
 
 # If we have an arg, assume travis run and push to coveralls. Otherwise launch browser results
 if [[ -n $1 ]]; then


Test patch:
diff --git a/cmd/addaccount_test.go b/cmd/addaccount_test.go
index fe8ae7d..b42687e 100644
--- a/cmd/addaccount_test.go
+++ b/cmd/addaccount_test.go
@@ -29,13 +29,13 @@ func Test_AddAccount(t *testing.T) {
 	_, badBar, _ := CreateClusterKey(t)
 
 	tests := CmdTests{
-		{createAddAccountCmd(), []string{"add", "account"}, nil, []string{"account name is required"}, true},
-		{createAddAccountCmd(), []string{"add", "account", "--name", "A"}, nil, []string{"Generated account key", "added account"}, false},
-		{createAddAccountCmd(), []string{"add", "account", "--name", "A"}, nil, []string{"the account \"A\" already exists"}, true},
-		{createAddAccountCmd(), []string{"add", "account", "--name", "B", "--public-key", bar}, nil, nil, false},
-		{createAddAccountCmd(), []string{"add", "account", "--name", "X", "--public-key", badBar}, nil, []string{"invalid account key"}, true},
-		{createAddAccountCmd(), []string{"add", "account", "--name", "badexp", "--expiry", "2018-01-01"}, nil, []string{"expiry \"2018-01-01\" is in the past"}, true},
-		{createAddAccountCmd(), []string{"add", "account", "--name", "badexp", "--expiry", "30d"}, nil, nil, false},
+		{CreateAddAccountCmd(), []string{"add", "account"}, nil, []string{"account name is required"}, true},
+		{CreateAddAccountCmd(), []string{"add", "account", "--name", "A"}, nil, []string{"Generated account key", "added account"}, false},
+		{CreateAddAccountCmd(), []string{"add", "account", "--name", "A"}, nil, []string{"the account \"A\" already exists"}, true},
+		{CreateAddAccountCmd(), []string{"add", "account", "--name", "B", "--public-key", bar}, nil, nil, false},
+		{CreateAddAccountCmd(), []string{"add", "account", "--name", "X", "--public-key", badBar}, nil, []string{"invalid account key"}, true},
+		{CreateAddAccountCmd(), []string{"add", "account", "--name", "badexp", "--expiry", "2018-01-01"}, nil, []string{"expiry \"2018-01-01\" is in the past"}, true},
+		{CreateAddAccountCmd(), []string{"add", "account", "--name", "badexp", "--expiry", "30d"}, nil, nil, false},
 	}
 
 	tests.Run(t, "root", "add")
@@ -43,16 +43,17 @@ func Test_AddAccount(t *testing.T) {
 
 func Test_AddAccountNoStore(t *testing.T) {
 	// reset the store
+	SetStoreRoot("")
 	ngsStore = nil
-	_, _, err := ExecuteCmd(createAddAccountCmd())
-	require.Equal(t, "no store directory found", err.Error())
+	_, _, err := ExecuteCmd(CreateAddAccountCmd())
+	require.Equal(t, "no stores available", err.Error())
 }
 
 func Test_AddAccountValidateOutput(t *testing.T) {
-	ts := NewTestStore(t, "Test_AddAccountValidateOutput")
+	ts := NewTestStore(t, "test")
 	defer ts.Done(t)
 
-	_, _, err := ExecuteCmd(createAddAccountCmd(), "--name", "A", "--start", "2018-01-01", "--expiry", "2050-01-01")
+	_, _, err := ExecuteCmd(CreateAddAccountCmd(), "--name", "A", "--start", "2018-01-01", "--expiry", "2050-01-01")
 	require.NoError(t, err)
 	validateAddAccountClaims(t, ts)
 }
@@ -63,7 +64,7 @@ func Test_AddAccountInteractive(t *testing.T) {
 
 	inputs := []interface{}{"A", true, "2018-01-01", "2050-01-01"}
 
-	cmd := createAddAccountCmd()
+	cmd := CreateAddAccountCmd()
 	HoistRootFlags(cmd)
 	_, _, err := ExecuteInteractiveCmd(cmd, inputs)
 	require.NoError(t, err)
@@ -81,7 +82,7 @@ func validateAddAccountClaims(t *testing.T, ts *TestStore) {
 	pub, err := kp.PublicKey()
 	require.Equal(t, ac.Subject, pub, "public key is subject")
 
-	okp, err := ts.KeyStore.GetOperatorKey("operator")
+	okp, err := ts.KeyStore.GetOperatorKey("test")
 	require.NoError(t, err)
 
 	if okp == nil {
@@ -105,7 +106,7 @@ func Test_AddAccountOperatorLessStore(t *testing.T) {
 	ts := NewTestStoreWithOperator(t, "test", nil)
 	defer ts.Done(t)
 
-	_, _, err := ExecuteCmd(createAddAccountCmd(), "--name", "A", "--start", "2018-01-01", "--expiry", "2050-01-01")
+	_, _, err := ExecuteCmd(CreateAddAccountCmd(), "--name", "A", "--start", "2018-01-01", "--expiry", "2050-01-01")
 	require.NoError(t, err)
 	validateAddAccountClaims(t, ts)
 }
diff --git a/cmd/addclustert_test.go b/cmd/addclustert_test.go
index 111f26f..a5dbfcb 100644
--- a/cmd/addclustert_test.go
+++ b/cmd/addclustert_test.go
@@ -45,8 +45,9 @@ func Test_AddCluster(t *testing.T) {
 func Test_AddClusterNoStore(t *testing.T) {
 	// reset the store
 	ngsStore = nil
+	SetStoreRoot("")
 	_, _, err := ExecuteCmd(createAddClusterCmd())
-	require.Equal(t, "no store directory found", err.Error())
+	require.Equal(t, "no stores available", err.Error())
 }
 
 func Test_AddClusterOutput(t *testing.T) {
@@ -66,7 +67,7 @@ func Test_AddClusterOutput(t *testing.T) {
 	pub, err := kp.PublicKey()
 	require.Equal(t, ac.Subject, pub, "public key is subject")
 
-	okp, err := ts.KeyStore.GetOperatorKey("operator")
+	okp, err := ts.KeyStore.GetOperatorKey("test")
 	require.NoError(t, err)
 
 	oppub, err := okp.PublicKey()
diff --git a/cmd/addserver_test.go b/cmd/addserver_test.go
index ac0c092..769f4d3 100644
--- a/cmd/addserver_test.go
+++ b/cmd/addserver_test.go
@@ -48,8 +48,9 @@ func Test_AddServer(t *testing.T) {
 func Test_AddServerNoStore(t *testing.T) {
 	// reset the store
 	ngsStore = nil
+	SetStoreRoot("")
 	_, _, err := ExecuteCmd(createAddServerCmd())
-	require.Equal(t, "no store directory found", err.Error())
+	require.Equal(t, "no stores available", err.Error())
 }
 
 func Test_AddServerOutput(t *testing.T) {
diff --git a/cmd/adduser_test.go b/cmd/adduser_test.go
index b79af9e..3c3b658 100644
--- a/cmd/adduser_test.go
+++ b/cmd/adduser_test.go
@@ -25,21 +25,21 @@ func Test_AddUser(t *testing.T) {
 	ts := NewTestStore(t, "add_server")
 	defer ts.Done(t)
 
-	_, _, err := ExecuteCmd(createAddAccountCmd(), "--name", "c")
+	_, _, err := ExecuteCmd(CreateAddAccountCmd(), "--name", "c")
 	require.NoError(t, err, "cluster creation")
 
 	_, bar, _ := CreateUserKey(t)
 	_, badBar, _ := CreateAccountKey(t)
 
 	tests := CmdTests{
-		{createAddUserCmd(), []string{"add", "user"}, nil, []string{"user name is required"}, true},
-		{createAddUserCmd(), []string{"add", "user", "--name", "foo"}, nil, []string{"Generated user key", "added user"}, false},
-		{createAddUserCmd(), []string{"add", "user", "--name", "foo"}, nil, []string{"the user \"foo\" already exists"}, true},
-		{createAddUserCmd(), []string{"add", "user", "--name", "foo"}, nil, []string{"the user \"foo\" already exists"}, true},
-		{createAddUserCmd(), []string{"add", "user", "--name", "bar", "--public-key", bar}, nil, nil, false},
-		{createAddUserCmd(), []string{"add", "user", "--name", "badbar", "--public-key", badBar}, nil, []string{"invalid user key"}, true},
-		{createAddUserCmd(), []string{"add", "user", "--name", "badexp", "--expiry", "2018-01-01"}, nil, []string{"expiry \"2018-01-01\" is in the past"}, true},
-		{createAddUserCmd(), []string{"add", "user", "--name", "badexp", "--expiry", "30d"}, nil, nil, false},
+		{CreateAddUserCmd(), []string{"add", "user"}, nil, []string{"user name is required"}, true},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "foo"}, nil, []string{"Generated user key", "added user"}, false},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "foo"}, nil, []string{"the user \"foo\" already exists"}, true},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "foo"}, nil, []string{"the user \"foo\" already exists"}, true},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "bar", "--public-key", bar}, nil, nil, false},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "badbar", "--public-key", badBar}, nil, []string{"invalid user key"}, true},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "badexp", "--expiry", "2018-01-01"}, nil, []string{"expiry \"2018-01-01\" is in the past"}, true},
+		{CreateAddUserCmd(), []string{"add", "user", "--name", "badexp", "--expiry", "30d"}, nil, nil, false},
 	}
 
 	tests.Run(t, "root", "add")
@@ -48,18 +48,19 @@ func Test_AddUser(t *testing.T) {
 func Test_AddUserNoStore(t *testing.T) {
 	// reset the store
 	ngsStore = nil
-	_, _, err := ExecuteCmd(createAddUserCmd())
-	require.Equal(t, "no store directory found", err.Error())
+	SetStoreRoot("")
+	_, _, err := ExecuteCmd(CreateAddUserCmd())
+	require.Equal(t, "no stores available", err.Error())
 }
 
 func Test_AddUserrOutput(t *testing.T) {
 	ts := NewTestStore(t, "test")
 	defer ts.Done(t)
 
-	_, _, err := ExecuteCmd(createAddAccountCmd(), "--name", "A")
+	_, _, err := ExecuteCmd(CreateAddAccountCmd(), "--name", "A")
 	require.NoError(t, err, "account creation")
 
-	_, _, err = ExecuteCmd(createAddUserCmd(), "--name", "U", "--start", "2018-01-01", "--expiry", "2050-01-01")
+	_, _, err = ExecuteCmd(CreateAddUserCmd(), "--name", "U", "--start", "2018-01-01", "--expiry", "2050-01-01")
 	require.NoError(t, err)
 	validateAddUserClaims(t, ts)
 }
@@ -95,10 +96,10 @@ func Test_AddUserOperatorLessStore(t *testing.T) {
 	ts := NewTestStoreWithOperator(t, "test", nil)
 	defer ts.Done(t)
 
-	_, _, err := ExecuteCmd(createAddAccountCmd(), "--name", "A", "--start", "2018-01-01", "--expiry", "2050-01-01")
+	_, _, err := ExecuteCmd(CreateAddAccountCmd(), "--name", "A", "--start", "2018-01-01", "--expiry", "2050-01-01")
 	require.NoError(t, err)
 
-	_, _, err = ExecuteCmd(createAddUserCmd(), "--name", "U", "--start", "2018-01-01", "--expiry", "2050-01-01")
+	_, _, err = ExecuteCmd(CreateAddUserCmd(), "--name", "U", "--start", "2018-01-01", "--expiry", "2050-01-01")
 	require.NoError(t, err)
 
 	validateAddUserClaims(t, ts)
diff --git a/cmd/clustercontextparams_test.go b/cmd/clustercontextparams_test.go
index d6fc9a6..1de386a 100644
--- a/cmd/clustercontextparams_test.go
+++ b/cmd/clustercontextparams_test.go
@@ -16,15 +16,10 @@
 package cmd
 
 import (
-	"os"
-	"path/filepath"
 	"testing"
 
-	"github.com/spf13/cobra"
-
 	"github.com/nats-io/nsc/cli"
-	"github.com/nats-io/nsc/cmd/store"
-
+	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/require"
 )
 
@@ -77,14 +72,12 @@ func TestCCP_DefaultsPath(t *testing.T) {
 	ts.AddCluster(t, "foo")
 	ts.AddCluster(t, "bar")
 
-	require.NoError(t, os.Chdir(filepath.Join(ts.Dir, "store", store.Clusters, "foo")))
-
 	ctx, err := NewActx(nil, nil)
 	require.NoError(t, err)
 
 	ccp := ClusterContextParams{}
 	ccp.SetDefaults(ctx)
-	require.Equal(t, "foo", ccp.Name)
+	require.Equal(t, "", ccp.Name)
 }
 
 func TestCCP_Edit(t *testing.T) {
diff --git a/cmd/common_test.go b/cmd/common_test.go
index 3fe3448..7b8f4ff 100644
--- a/cmd/common_test.go
+++ b/cmd/common_test.go
@@ -26,7 +26,6 @@ import (
 	"time"
 
 	"github.com/nats-io/nkeys"
-
 	"github.com/stretchr/testify/require"
 )
 
@@ -206,3 +205,19 @@ func Test_LoadFromURLTimeout(t *testing.T) {
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "Timeout exceeded")
 }
+
+func TestIsValidDir(t *testing.T) {
+	d := MakeTempDir(t)
+	require.NoError(t, IsValidDir(d))
+
+	tp := filepath.Join(d, "foo")
+	err := IsValidDir(tp)
+	require.Error(t, err)
+	require.True(t, os.IsNotExist(err))
+
+	err = ioutil.WriteFile(tp, []byte("hello"), 0600)
+	require.NoError(t, err)
+	err = IsValidDir(tp)
+	require.Error(t, err)
+	require.Equal(t, "not a directory", err.Error())
+}
diff --git a/cmd/contextconfig_test.go b/cmd/contextconfig_test.go
new file mode 100644
index 0000000..951cc63
--- /dev/null
+++ b/cmd/contextconfig_test.go
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestContextConfig_Empty(t *testing.T) {
+	c, err := NewContextConfig("")
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.True(t, (ContextConfig{}) == *c)
+}
+
+func TestContextConfig_BadDir(t *testing.T) {
+	fp := filepath.Join(MakeTempDir(t), "foo")
+	c, err := NewContextConfig(fp)
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, fp, c.StoreRoot)
+}
+
+func TestContextConfig_DefaultOperator(t *testing.T) {
+	ts := NewTestStore(t, "operator")
+	defer ts.Done(t)
+
+	storesDir := filepath.Dir(ts.Store.Dir)
+	c, err := NewContextConfig(storesDir)
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, storesDir, c.StoreRoot)
+	require.Equal(t, "operator", c.Operator)
+}
+
+func TestContextConfig_Multiple(t *testing.T) {
+	ts := NewTestStore(t, "operator")
+	defer ts.Done(t)
+
+	ts.AddOperator(t, "operator2")
+
+	storesDir := filepath.Dir(ts.Store.Dir)
+	c, err := NewContextConfig(storesDir)
+
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, storesDir, c.StoreRoot)
+	require.Equal(t, "", c.Operator)
+}
+
+func TestContextConfig_Account(t *testing.T) {
+	ts := NewTestStore(t, "operator")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+
+	storesDir := filepath.Dir(ts.Store.Dir)
+	c, err := NewContextConfig(storesDir)
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, storesDir, c.StoreRoot)
+	require.Equal(t, "operator", c.Operator)
+	require.Equal(t, "A", c.Account)
+}
+
+func TestContextConfig_MultipleAccount(t *testing.T) {
+	ts := NewTestStore(t, "operator")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+	ts.AddAccount(t, "B")
+
+	storesDir := filepath.Dir(ts.Store.Dir)
+	c, err := NewContextConfig(storesDir)
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, storesDir, c.StoreRoot)
+	require.Equal(t, "operator", c.Operator)
+	require.Equal(t, "", c.Account)
+}
+
+func TestContextConfig_Cluster(t *testing.T) {
+	ts := NewTestStore(t, "operator")
+	defer ts.Done(t)
+	ts.AddCluster(t, "C")
+
+	storesDir := filepath.Dir(ts.Store.Dir)
+	c, err := NewContextConfig(storesDir)
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, storesDir, c.StoreRoot)
+	require.Equal(t, "operator", c.Operator)
+	require.Equal(t, "", c.Account)
+	require.Equal(t, "C", c.Cluster)
+}
+
+func TestContextConfig_MultipleCluster(t *testing.T) {
+	ts := NewTestStore(t, "operator")
+	defer ts.Done(t)
+	ts.AddCluster(t, "C")
+	ts.AddCluster(t, "CC")
+
+	storesDir := filepath.Dir(ts.Store.Dir)
+	c, err := NewContextConfig(storesDir)
+	require.NoError(t, err)
+	require.NotNil(t, c)
+	require.Equal(t, storesDir, c.StoreRoot)
+	require.Equal(t, "operator", c.Operator)
+	require.Equal(t, "", c.Account)
+	require.Equal(t, "", c.Cluster)
+}
diff --git a/cmd/defaults_test.go b/cmd/defaults_test.go
new file mode 100644
index 0000000..e0569c0
--- /dev/null
+++ b/cmd/defaults_test.go
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 The NATS Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefault_LoadOrInit(t *testing.T) {
+	t.Skip(t.Name())
+	d := MakeTempDir(t)
+	dir := filepath.Join(d, "a")
+	require.NoError(t, os.Setenv("TEST_NAME", dir))
+
+	ResetConfigForTests()
+	err := LoadOrInit("my/foo", "TEST_NAME")
+	require.NoError(t, err)
+
+	tc := GetConfig()
+	require.Equal(t, dir, tc.StoreRoot)
+	require.Equal(t, "", tc.Operator)
+	require.Equal(t, "", tc.Account)
+	require.Equal(t, "", tc.Cluster)
+	require.Equal(t, "my/foo", tc.GithubUpdates)
+}
+
+func TestDefault_LoadNewOnExisting(t *testing.T) {
+	t.Skip(t.Name())
+	ts := NewTestStore(t, "operator")
+	require.NoError(t, os.Setenv("TEST_NAME", ts.Dir))
+	ts.AddAccount(t, "A")
+	ts.AddCluster(t, "C")
+
+	var cc ContextConfig
+	cc.StoreRoot = ts.GetStoresRoot()
+	fp := filepath.Join(ts.Dir, fmt.Sprintf("%s.json", filepath.Base(os.Args[0])))
+	require.NoError(t, WriteJson(fp, cc))
+
+	ResetConfigForTests()
+	err := LoadOrInit("my/foo", "TEST_NAME")
+	require.NoError(t, err)
+
+	tc := GetConfig()
+	require.Equal(t, ts.GetStoresRoot(), tc.StoreRoot)
+	require.Equal(t, "operator", tc.Operator)
+	require.Equal(t, "A", tc.Account)
+	require.Equal(t, "C", tc.Cluster)
+}
diff --git a/cmd/deleteexport_test.go b/cmd/deleteexport_test.go
index c645b27..ed65a6e 100644
--- a/cmd/deleteexport_test.go
+++ b/cmd/deleteexport_test.go
@@ -18,9 +18,8 @@ package cmd
 import (
 	"testing"
 
-	"github.com/stretchr/testify/require"
-
 	"github.com/nats-io/jwt"
+	"github.com/stretchr/testify/require"
 )
 
 func Test_DeleteExport(t *testing.T) {
diff --git a/cmd/generateactivation_test.go b/cmd/generateactivation_test.go
index 2e06df3..777c87c 100644
--- a/cmd/generateactivation_test.go
+++ b/cmd/generateactivation_test.go
@@ -21,9 +21,8 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/stretchr/testify/require"
-
 	"github.com/nats-io/jwt"
+	"github.com/stretchr/testify/require"
 )
 
 func Test_GenerateActivation(t *testing.T) {
diff --git a/cmd/store/keys_test.go b/cmd/store/keys_test.go
index 6558648..611b37e 100644
--- a/cmd/store/keys_test.go
+++ b/cmd/store/keys_test.go
@@ -18,25 +18,25 @@ package store
 import (
 	"io/ioutil"
 	"os"
-	"os/user"
 	"path/filepath"
 	"testing"
 
+	"github.com/mitchellh/go-homedir"
 	"github.com/nats-io/nkeys"
 	"github.com/stretchr/testify/require"
 )
 
 func TestResolveLocal(t *testing.T) {
 	old := os.Getenv(NKeysPathEnv)
-	os.Setenv(NKeysPathEnv, "")
+	require.NoError(t, os.Setenv(NKeysPathEnv, ""))
 
 	dir := GetKeysDir()
 
-	os.Setenv(NKeysPathEnv, old)
+	require.NoError(t, os.Setenv(NKeysPathEnv, old))
 
-	u, err := user.Current()
+	u, err := homedir.Dir()
 	require.NoError(t, err)
-	fp := filepath.Join(u.HomeDir, DEfaultNKeysPath)
+	fp := filepath.Join(u, DEfaultNKeysPath)
 
 	require.Equal(t, dir, fp)
 }
@@ -45,11 +45,11 @@ func TestResolveEnv(t *testing.T) {
 	old := os.Getenv(NKeysPathEnv)
 
 	p := filepath.Join("foo", "bar")
-	os.Setenv(NKeysPathEnv, p)
+	require.NoError(t, os.Setenv(NKeysPathEnv, p))
 
 	dir := GetKeysDir()
 
-	os.Setenv(NKeysPathEnv, old)
+	require.NoError(t, os.Setenv(NKeysPathEnv, old))
 	require.Equal(t, dir, p)
 }
 
@@ -64,7 +64,7 @@ func TestMatchKeys(t *testing.T) {
 func TestGetKeys(t *testing.T) {
 	dir := MakeTempDir(t)
 	old := os.Getenv(NKeysPathEnv)
-	os.Setenv(NKeysPathEnv, dir)
+	require.NoError(t, os.Setenv(NKeysPathEnv, dir))
 
 	ks := NewKeyStore("test_get_keys")
 
@@ -86,20 +86,21 @@ func TestGetKeys(t *testing.T) {
 	require.True(t, Match(opk, ookp))
 	require.True(t, Match(apk, aakp))
 
-	os.Setenv(NKeysPathEnv, old)
+	require.NoError(t, os.Setenv(NKeysPathEnv, old))
 }
 
 func TestGetPrivateKey(t *testing.T) {
 	dir := MakeTempDir(t)
 	old := os.Getenv(NKeysPathEnv)
-	os.Setenv(NKeysPathEnv, dir)
+	require.NoError(t, os.Setenv(NKeysPathEnv, dir))
 
 	_, _, okp := CreateOperatorKey(t)
 	_, _, akp := CreateAccountKey(t)
 
 	ks := NewKeyStore("test_get_private_key")
-	ks.Store("o", okp, "o")
-	ks.Store("a", akp, "o")
+	_, _ = ks.Store("o", okp, "o")
+	_, _ = ks.Store("a", akp, "o")
+
 	ckp, err := ks.GetClusterKey("c")
 	require.Nil(t, err)
 	require.Nil(t, ckp)
diff --git a/cmd/store/store_test.go b/cmd/store/store_test.go
index 46b1e40..0149d18 100644
--- a/cmd/store/store_test.go
+++ b/cmd/store/store_test.go
@@ -57,8 +57,6 @@ func CreateTestStoreForOperator(t *testing.T, name string, operator nkeys.KeyPai
 		tokenName := fmt.Sprintf("%s.jwt", SafeName(name))
 		require.FileExists(t, filepath.Join(s.Dir, tokenName))
 		require.True(t, s.Has("", tokenName))
-	} else {
-		require.True(t, s.Info.Managed)
 	}
 
 	for _, d := range standardDirs {
@@ -231,15 +229,6 @@ func TestStoreUser(t *testing.T) {
 	require.Equal(t, gc.Name, "bar")
 }
 
-func TestOperatorLessStore(t *testing.T) {
-	s := CreateTestStoreForOperator(t, "test", nil)
-	require.NotNil(t, s)
-	ss, err := LoadStore(s.Dir)
-	require.NoError(t, err)
-	require.NotNil(t, ss)
-	require.True(t, s.IsManaged())
-}
-
 func TestStore_ListSubContainers(t *testing.T) {
 	_, _, kp := CreateOperatorKey(t)
 	_, apub, akp := CreateAccountKey(t)
diff --git a/cmd/util_test.go b/cmd/util_test.go
index 2d4495b..1950e1f 100644
--- a/cmd/util_test.go
+++ b/cmd/util_test.go
@@ -22,7 +22,6 @@ import (
 	"testing"
 
 	"github.com/nats-io/jwt"
-
 	"github.com/nats-io/nkeys"
 	"github.com/nats-io/nsc/cli"
 	"github.com/nats-io/nsc/cmd/store"
@@ -30,8 +29,7 @@ import (
 )
 
 type TestStore struct {
-	Dir      string
-	StartDir string
+	Dir string
 
 	Store    *store.Store
 	KeyStore store.KeyStore
@@ -40,72 +38,105 @@ type TestStore struct {
 	OperatorKeyPath string
 }
 
-func NewTestStoreWithOperator(t *testing.T, name string, operator nkeys.KeyPair) *TestStore {
+func NewTestStoreWithOperator(t *testing.T, operatorName string, operator nkeys.KeyPair) *TestStore {
 	var ts TestStore
-	var err error
 
 	// ngsStore is a global - so first test to get it initializes it
 	ngsStore = nil
 
 	ts.OperatorKey = operator
 
-	ts.StartDir, err = os.Getwd()
-	require.NoError(t, err)
 	ts.Dir = MakeTempDir(t)
+	// debug the test that created the store
+	_ = ioutil.WriteFile(filepath.Join(ts.Dir, "test.txt"), []byte(t.Name()), 0700)
+
+	SetStoreRoot(ts.GetStoresRoot())
+	SetOperator(operatorName)
+	ts.AddOperatorWithKey(t, operatorName, operator)
+
+	return &ts
+}
 
-	storeDir := filepath.Join(ts.Dir, "store")
-	err = os.Mkdir(storeDir, 0700)
-	require.NoError(t, err, "error creating %q", storeDir)
+func (ts *TestStore) AddOperator(t *testing.T, operatorName string) *store.Store {
+	_, _, kp := CreateOperatorKey(t)
+	return ts.AddOperatorWithKey(t, operatorName, kp)
+}
+
+func (ts *TestStore) AddOperatorWithKey(t *testing.T, operatorName string, operator nkeys.KeyPair) *store.Store {
+	storeRoot := ts.GetStoresRoot()
+	operatorRoot := filepath.Join(storeRoot, operatorName)
+	err := os.MkdirAll(operatorRoot, 0700)
+	require.NoError(t, err, "error creating %q", operatorRoot)
 
 	nkeysDir := filepath.Join(ts.Dir, "keys")
-	err = os.Mkdir(nkeysDir, 0700)
-	require.NoError(t, err, "error creating %q", nkeysDir)
+	_, err = os.Stat(nkeysDir)
+	if err != nil && os.IsNotExist(err) {
+		err = os.Mkdir(nkeysDir, 0700)
+		require.NoError(t, err, "error creating %q", nkeysDir)
+	}
+	require.NoError(t, err)
+
 	err = os.Setenv(store.NKeysPathEnv, nkeysDir)
 	require.NoError(t, err, "nkeys env")
 
-	var nk *store.NamedKey
+	var nk = &store.NamedKey{}
+	nk.Name = operatorName
 	if ts.OperatorKey != nil {
-		nk = &store.NamedKey{Name: "operator", KP: ts.OperatorKey}
+		nk.KP = ts.OperatorKey
 	}
 
-	ts.Store, err = store.CreateStore(name, storeDir, nk)
+	s, err := store.CreateStore(operatorName, storeRoot, nk)
+	require.NoError(t, err)
+	if ts.Store == nil {
+		ts.Store = s
+	}
 	ctx, err := ts.Store.GetContext()
 	require.NoError(t, err, "getting context")
 
 	ts.KeyStore = ctx.KeyStore
-	if nk != nil {
-		ts.OperatorKeyPath, err = ts.KeyStore.Store("operator", ts.OperatorKey, "")
+	if ts.OperatorKey != nil {
+		ts.OperatorKeyPath, err = ts.KeyStore.Store(operatorName, ts.OperatorKey, "")
 		require.NoError(t, err, "store operator key")
 	}
-
-	require.NoError(t, os.Chdir(ts.Store.Dir))
-
-	return &ts
+	return s
 }
 
-func NewTestStore(t *testing.T, name string) *TestStore {
+func NewTestStore(t *testing.T, operatorName string) *TestStore {
 	_, _, kp := CreateOperatorKey(t)
-	return NewTestStoreWithOperator(t, name, kp)
+	return NewTestStoreWithOperator(t, operatorName, kp)
+}
+
+func TestStoreTree(t *testing.T) {
+	ts := NewTestStore(t, "foo")
+	ts.AddAccount(t, "bar")
+	ts.AddAccount(t, "foo")
+
+	v, err := store.LoadStore(filepath.Join(config.StoreRoot, config.Operator))
+	require.NoError(t, err)
+	require.NotNil(t, v)
 }
 
 func (ts *TestStore) Done(t *testing.T) {
 	cli.ResetPromptLib()
-	require.NoError(t, os.Chdir(ts.StartDir))
 	if t.Failed() {
 		t.Log("test artifacts:", ts.Dir)
 	}
 }
 
+func (ts *TestStore) GetStoresRoot() string {
+	return filepath.Join(ts.Dir, "store")
+}
+
 func (ts *TestStore) AddAccount(t *testing.T, accountName string) {
 	if !ts.Store.Has(store.Accounts, accountName, store.JwtName(accountName)) {
-		_, _, err := ExecuteCmd(createAddAccountCmd(), "--name", accountName)
+		_, _, err := ExecuteCmd(CreateAddAccountCmd(), "--name", accountName)
 		require.NoError(t, err, "account creation")
 	}
 }
 
 func (ts *TestStore) AddUser(t *testing.T, accountName string, userName string) {
 	ts.AddAccount(t, accountName)
-	_, _, err := ExecuteCmd(createAddUserCmd(), "--account", accountName, "--name", userName)
+	_, _, err := ExecuteCmd(CreateAddUserCmd(), "--account", accountName, "--name", userName)
 	require.NoError(t, err, "user creation")
 }