From 4c8e5a1ac4177340edc1515ed1beb1d7e38101b2 Mon Sep 17 00:00:00 2001 From: aryansharma9917 Date: Wed, 28 Jan 2026 22:36:14 +0530 Subject: [PATCH 1/2] Extend config defaults to support environment system --- pkg/config/config.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 64d5df1..140d48c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -18,12 +18,15 @@ type Config struct { User struct { Name string `yaml:"name"` } `yaml:"user"` + Defaults struct { AppName string `yaml:"app_name"` Image string `yaml:"image"` + ImageTag string `yaml:"image_tag"` RepoURL string `yaml:"repo_url"` Namespace string `yaml:"namespace"` Context string `yaml:"context"` + Branch string `yaml:"branch"` } `yaml:"defaults"` } @@ -32,10 +35,12 @@ user: name: aryan defaults: app_name: myapp - image: codewise:latest + image: codewise + image_tag: latest repo_url: https://github.com/example/repo namespace: default context: "" + branch: main `) func InitConfig() (string, error) { From da4070670e1ee135a29c54194983fea3f7d9d10f Mon Sep 17 00:00:00 2001 From: aryansharma9917 Date: Wed, 28 Jan 2026 22:36:24 +0530 Subject: [PATCH 2/2] Add environment subsystem skeleton with create command --- cmd/env.go | 13 +++++ cmd/env_create.go | 104 +++++++++++++++++++++++++++++++++++++ cmd/env_delete.go | 47 +++++++++++++++++ cmd/env_list.go | 34 ++++++++++++ pkg/env/create.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++ pkg/env/delete.go | 23 ++++++++ pkg/env/env.go | 51 ++++++++++++++++++ pkg/env/list.go | 68 ++++++++++++++++++++++++ pkg/env/types.go | 33 ++++++++++++ 9 files changed, 503 insertions(+) create mode 100644 cmd/env.go create mode 100644 cmd/env_create.go create mode 100644 cmd/env_delete.go create mode 100644 cmd/env_list.go create mode 100644 pkg/env/create.go create mode 100644 pkg/env/delete.go create mode 100644 pkg/env/env.go create mode 100644 pkg/env/list.go create mode 100644 pkg/env/types.go diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 0000000..842d605 --- /dev/null +++ b/cmd/env.go @@ -0,0 +1,13 @@ +package cmd + +import "github.com/spf13/cobra" + +var envCmd = &cobra.Command{ + Use: "env", + Short: "Manage deployment environments", + Long: "Create, list, and delete Codewise deployment environments.", +} + +func init() { + rootCmd.AddCommand(envCmd) +} diff --git a/cmd/env_create.go b/cmd/env_create.go new file mode 100644 index 0000000..a5bd479 --- /dev/null +++ b/cmd/env_create.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "fmt" + + survey "github.com/AlecAivazis/survey/v2" + "github.com/aryansharma9917/codewise-cli/pkg/config" + "github.com/aryansharma9917/codewise-cli/pkg/env" + "github.com/spf13/cobra" +) + +var interactive bool + +var envCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a new environment", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + + if interactive { + if err := createEnvInteractive(name); err != nil { + fmt.Println("error:", err) + return + } + fmt.Println("environment", name, "created") + return + } + + if err := env.CreateEnv(name, env.CreateOptions{}); err != nil { + fmt.Println("error:", err) + return + } + fmt.Println("environment", name, "created") + }, +} + +func init() { + envCreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Enable interactive mode") + envCmd.AddCommand(envCreateCmd) +} + +func createEnvInteractive(name string) error { + cfg, _ := config.ReadConfig() + + defaultNs := fallback(cfg.Defaults.Namespace, name) + defaultCtx := fallback(cfg.Defaults.Context, "") + defaultRepo := fallback(cfg.Defaults.RepoURL, "") + defaultBranch := fallback(cfg.Defaults.Branch, "main") + defaultImage := fallback(cfg.Defaults.Image, "codewise") + defaultTag := fallback(cfg.Defaults.ImageTag, "latest") + + answers := struct { + Namespace string + Context string + Repo string + Branch string + Image string + Tag string + }{} + + qs := []*survey.Question{ + {Name: "Namespace", Prompt: &survey.Input{Message: fmt.Sprintf("Namespace (default: %s)", defaultNs)}}, + {Name: "Context", Prompt: &survey.Input{Message: fmt.Sprintf("Kubernetes context (default: %s)", defaultCtx)}}, + {Name: "Repo", Prompt: &survey.Input{Message: fmt.Sprintf("GitOps repo (default: %s)", defaultRepo)}}, + {Name: "Branch", Prompt: &survey.Input{Message: fmt.Sprintf("GitOps branch (default: %s)", defaultBranch)}}, + {Name: "Image", Prompt: &survey.Input{Message: fmt.Sprintf("Image repository (default: %s)", defaultImage)}}, + {Name: "Tag", Prompt: &survey.Input{Message: fmt.Sprintf("Image tag (default: %s)", defaultTag)}}, + } + + if err := survey.Ask(qs, &answers); err != nil { + return err + } + + k8s := env.K8sConfig{ + Namespace: fallback(answers.Namespace, defaultNs), + Context: fallback(answers.Context, defaultCtx), + } + + helm := env.HelmConfig{ + Release: name, + Chart: "./helm/chart", + Values: "./values.yaml", + } + + gitops := env.GitOpsConfig{ + Repo: fallback(answers.Repo, defaultRepo), + Path: "", + Branch: fallback(answers.Branch, defaultBranch), + } + + values := env.ValuesConfig{} + values.Image.Repository = fallback(answers.Image, defaultImage) + values.Image.Tag = fallback(answers.Tag, defaultTag) + + return env.CreateEnvFromParts(name, k8s, helm, gitops, values) +} + +func fallback(input, def string) string { + if input != "" { + return input + } + return def +} diff --git a/cmd/env_delete.go b/cmd/env_delete.go new file mode 100644 index 0000000..1d9202a --- /dev/null +++ b/cmd/env_delete.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + + survey "github.com/AlecAivazis/survey/v2" + "github.com/aryansharma9917/codewise-cli/pkg/env" + "github.com/spf13/cobra" +) + +var yes bool + +var envDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete an environment", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + + if !yes { + var confirm bool + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Delete environment %q?", name), + } + if err := survey.AskOne(prompt, &confirm); err != nil { + fmt.Println("error:", err) + return + } + if !confirm { + fmt.Println("aborted") + return + } + } + + if err := env.DeleteEnv(name, env.DeleteOptions{Force: yes}); err != nil { + fmt.Println("error:", err) + return + } + + fmt.Println("environment", name, "deleted") + }, +} + +func init() { + envDeleteCmd.Flags().BoolVar(&yes, "yes", false, "Skip confirmation") + envCmd.AddCommand(envDeleteCmd) +} diff --git a/cmd/env_list.go b/cmd/env_list.go new file mode 100644 index 0000000..eb4d9ea --- /dev/null +++ b/cmd/env_list.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" + + "github.com/aryansharma9917/codewise-cli/pkg/env" + "github.com/spf13/cobra" +) + +var envListCmd = &cobra.Command{ + Use: "list", + Short: "List environments", + Run: func(cmd *cobra.Command, args []string) { + envs, err := env.ListEnvs() + if err != nil { + fmt.Println("error:", err) + return + } + + if len(envs) == 0 { + fmt.Println("no environments found") + return + } + + for _, e := range envs { + fmt.Printf("%-10s namespace=%s context=%s\n", + e.Name, e.K8s.Namespace, e.K8s.Context) + } + }, +} + +func init() { + envCmd.AddCommand(envListCmd) +} diff --git a/pkg/env/create.go b/pkg/env/create.go new file mode 100644 index 0000000..2f80e21 --- /dev/null +++ b/pkg/env/create.go @@ -0,0 +1,130 @@ +package env + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/aryansharma9917/codewise-cli/pkg/config" + "gopkg.in/yaml.v3" +) + +type CreateOptions struct { + Interactive bool +} + +func CreateEnv(name string, opts CreateOptions) error { + if opts.Interactive { + // handled at CLI layer + return fmt.Errorf("interactive mode not implemented in CreateEnv") + } + return createSilent(name) +} + +func createSilent(name string) error { + if err := ensureBaseDir(); err != nil { + return err + } + + if envExists(name) { + return fmt.Errorf("environment %q already exists", name) + } + + dir, err := envDir(name) + if err != nil { + return err + } + + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + // global config support + cfg, _ := config.ReadConfig() + + k8s := K8sConfig{ + Namespace: inferOrDefault(cfg.Defaults.Namespace, name), + Context: inferOrDefault(cfg.Defaults.Context, ""), + } + + helm := HelmConfig{ + Release: name, + Chart: "./helm/chart", + Values: "./values.yaml", + } + + gitops := GitOpsConfig{ + Repo: "", + Path: "", + Branch: "main", + } + + values := ValuesConfig{} + values.Image.Repository = inferOrDefault(cfg.Defaults.Image, "codewise") + values.Image.Tag = inferOrDefault(cfg.Defaults.ImageTag, "latest") + + if err := writeYAML(filepath.Join(dir, "k8s.yaml"), k8s); err != nil { + return err + } + if err := writeYAML(filepath.Join(dir, "helm.yaml"), helm); err != nil { + return err + } + if err := writeYAML(filepath.Join(dir, "gitops.yaml"), gitops); err != nil { + return err + } + if err := writeYAML(filepath.Join(dir, "values.yaml"), values); err != nil { + return err + } + + return nil +} + +// used by interactive path +func CreateEnvFromParts(name string, k8s K8sConfig, helm HelmConfig, gitops GitOpsConfig, values ValuesConfig) error { + if err := ensureBaseDir(); err != nil { + return err + } + + if envExists(name) { + return fmt.Errorf("environment %q already exists", name) + } + + dir, err := envDir(name) + if err != nil { + return err + } + + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + if err := writeYAML(filepath.Join(dir, "k8s.yaml"), k8s); err != nil { + return err + } + if err := writeYAML(filepath.Join(dir, "helm.yaml"), helm); err != nil { + return err + } + if err := writeYAML(filepath.Join(dir, "gitops.yaml"), gitops); err != nil { + return err + } + if err := writeYAML(filepath.Join(dir, "values.yaml"), values); err != nil { + return err + } + + return nil +} + +func writeYAML(path string, data interface{}) error { + out, err := yaml.Marshal(data) + if err != nil { + return err + } + return os.WriteFile(path, out, 0644) +} + +func inferOrDefault(cfgVal, fallback string) string { + if cfgVal != "" { + return cfgVal + } + return fallback +} diff --git a/pkg/env/delete.go b/pkg/env/delete.go new file mode 100644 index 0000000..9744d12 --- /dev/null +++ b/pkg/env/delete.go @@ -0,0 +1,23 @@ +package env + +import ( + "fmt" + "os" +) + +type DeleteOptions struct { + Force bool // mapped from --yes +} + +func DeleteEnv(name string, opts DeleteOptions) error { + if !envExists(name) { + return fmt.Errorf("environment %q does not exist", name) + } + + dir, err := envDir(name) + if err != nil { + return err + } + + return os.RemoveAll(dir) +} diff --git a/pkg/env/env.go b/pkg/env/env.go new file mode 100644 index 0000000..fbcb48e --- /dev/null +++ b/pkg/env/env.go @@ -0,0 +1,51 @@ +package env + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + codewiseHomeEnv = "CODEWISE_HOME" +) + +func baseEnvPath() (string, error) { + // Check override via env var + if home := os.Getenv(codewiseHomeEnv); home != "" { + return filepath.Join(home, "envs"), nil + } + + // Fallback to ~/.codewise/envs + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to resolve user home: %w", err) + } + + return filepath.Join(home, ".codewise", "envs"), nil +} + +func envDir(name string) (string, error) { + base, err := baseEnvPath() + if err != nil { + return "", err + } + return filepath.Join(base, name), nil +} + +func envExists(name string) bool { + dir, err := envDir(name) + if err != nil { + return false + } + info, err := os.Stat(dir) + return err == nil && info.IsDir() +} + +func ensureBaseDir() error { + base, err := baseEnvPath() + if err != nil { + return err + } + return os.MkdirAll(base, 0755) +} diff --git a/pkg/env/list.go b/pkg/env/list.go new file mode 100644 index 0000000..2f26136 --- /dev/null +++ b/pkg/env/list.go @@ -0,0 +1,68 @@ +package env + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +func ListEnvs() ([]Env, error) { + base, err := baseEnvPath() + if err != nil { + return nil, err + } + + entries, err := os.ReadDir(base) + if err != nil { + return nil, err + } + + var envs []Env + for _, entry := range entries { + if entry.IsDir() { + name := entry.Name() + e, err := LoadEnv(name) + if err != nil { + continue + } + envs = append(envs, *e) + } + } + + return envs, nil +} + +func LoadEnv(name string) (*Env, error) { + dir, err := envDir(name) + if err != nil { + return nil, err + } + + k8s := K8sConfig{} + helm := HelmConfig{} + gitops := GitOpsConfig{} + values := ValuesConfig{} + + // read each file if exists + _ = readYAML(filepath.Join(dir, "k8s.yaml"), &k8s) + _ = readYAML(filepath.Join(dir, "helm.yaml"), &helm) + _ = readYAML(filepath.Join(dir, "gitops.yaml"), &gitops) + _ = readYAML(filepath.Join(dir, "values.yaml"), &values) + + return &Env{ + Name: name, + K8s: k8s, + Helm: helm, + GitOps: gitops, + Values: values, + }, nil +} + +func readYAML(path string, out interface{}) error { + raw, err := os.ReadFile(path) + if err != nil { + return err + } + return yaml.Unmarshal(raw, out) +} diff --git a/pkg/env/types.go b/pkg/env/types.go new file mode 100644 index 0000000..dcf02ce --- /dev/null +++ b/pkg/env/types.go @@ -0,0 +1,33 @@ +package env + +type K8sConfig struct { + Namespace string `yaml:"namespace"` + Context string `yaml:"context"` +} + +type HelmConfig struct { + Release string `yaml:"release"` + Chart string `yaml:"chart"` + Values string `yaml:"values"` +} + +type GitOpsConfig struct { + Repo string `yaml:"repo"` + Path string `yaml:"path"` + Branch string `yaml:"branch"` +} + +type ValuesConfig struct { + Image struct { + Repository string `yaml:"repository"` + Tag string `yaml:"tag"` + } `yaml:"image"` +} + +type Env struct { + Name string + K8s K8sConfig + Helm HelmConfig + GitOps GitOpsConfig + Values ValuesConfig +}