diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0da200f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,106 @@ +# Raid - Distributed Development Orchestration + +## Project Overview + +Raid is a Go-based CLI tool that orchestrates development tasks, environments, and dependencies across distributed repositories. It uses YAML/JSON profile configurations to define multi-repo environments and automates setup/execution workflows. + +## Architecture + +### Core Components +- **CLI Layer**: `src/cmd/` - Cobra-based command structure with subcommands for profiles, installation, and environments +- **Business Logic**: `src/raid/` - High-level API layer that delegates to internal libraries +- **Internal Implementation**: `src/internal/lib/` - Core functionality for profiles, repositories, environments, and task execution +- **System Utilities**: `src/internal/sys/` and `src/utils/` - System-level operations and shared utilities + +### Key Design Patterns + +#### Configuration Management +- Uses **Viper** for configuration with global state in `src/internal/lib/config.go` +- **Context singleton** pattern: `lib.Context` struct caches active profile and environment +- **Lazy loading**: `Load()` uses cached context, `ForceLoad()` rebuilds from scratch +- Configuration path customizable via `--config/-c` flag + +#### Profile System +- Profiles define collections of repositories and environments +- **Multi-document YAML** support using `---` separators for multiple profiles per file +- **JSON Schema validation** against `schemas/raid-profile.schema.json` +- Profile state managed in Viper config under `"profiles"` key + +#### Repository Management +- Concurrent cloning with optional thread limits (`--threads/-t` flag) +- Uses Go routines with semaphore pattern for concurrency control +- Repository validation and error aggregation across parallel operations + +#### Environment Execution +- Environments contain tasks (Shell commands or Script files) and environment variables +- Task execution supports concurrent execution flag per task +- Environment variables set globally during environment execution + +### Key Files & Patterns + +#### Entry Points +- `main.go` - Simple delegator to `cmd.Execute()` +- `src/cmd/raid.go` - Root Cobra command with initialization lifecycle + +#### Command Structure +``` +src/cmd/ +├── raid.go # Root command, global flags, initialization +├── profile/ # Profile management (add, list, use, remove) +├── install/ # Repository installation +└── env/ # Environment execution +``` + +#### Core Business Logic Flow +1. **Initialize**: `raid.Initialize()` → `lib.InitConfig()` → `lib.Load()` +2. **Profile Management**: Viper-backed persistence with JSON schema validation +3. **Repository Installation**: Concurrent git cloning with error aggregation +4. **Environment Execution**: Task orchestration with variable setting + +### Development Workflows + +#### Building & Testing +```bash +go build -o raid # Build binary +go test ./... # Run tests +go test -coverprofile=coverage.out ./... # Generate coverage +``` + +#### JSON Schema Integration +- Schemas in `schemas/` directory define validation rules +- Use `github.com/santhosh-tekuri/jsonschema/v6` for validation +- YAML language server integration with `# yaml-language-server: $schema=...` comments + +#### Configuration Files +- **Profile configs**: YAML/JSON files following `schemas/raid-profile.schema.json` +- **Multi-profile files**: Use YAML `---` document separators +- **Examples**: See `docs/examples/` for reference configurations + +### Common Patterns + +#### Error Handling +- Use `fmt.Errorf()` for wrapped errors with context +- Aggregate errors from concurrent operations into slices +- CLI commands print errors to stderr via `cmd.PrintErrln()` + +#### Concurrent Operations +```go +// Semaphore pattern for limiting concurrency +semaphore := make(chan struct{}, maxThreads) +var wg sync.WaitGroup +errorChan := make(chan error, len(items)) + +// In goroutine: +semaphore <- struct{}{} +defer func() { <-semaphore }() +``` + +#### Viper Configuration +- Global config management via `viper.GetString()`, `viper.Set()` +- Nested keys accessed with dot notation: `viper.GetStringMapString("profiles")` +- Automatic config file discovery and loading + +### Testing & Quality +- Uses standard Go testing with coverage reporting +- GitHub Actions CI/CD pipeline defined (`.github/workflows/build.yml`) +- Codecov integration for coverage tracking \ No newline at end of file diff --git a/.gitignore b/.gitignore index 95706d6..e5103f4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ *.dll *.so *.dylib -raid +/raid # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index ea68f6d..0806070 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ -# Raid - Distributed Development Orchestration [![Build and Test](https://github.com/8bitAlex/raid/actions/workflows/build.yml/badge.svg)](https://github.com/8bitAlex/raid/actions/workflows/build.yml) [![codecov](https://codecov.io/github/8bitAlex/raid/graph/badge.svg?token=Z75V7I2TLW)](https://codecov.io/github/8bitAlex/raid) +# Raid - Distributed Development Orchestration +![Windows](https://img.shields.io/badge/Windows-Yes-blue?logo=windows) +![macOS](https://img.shields.io/badge/macOS-Yes-lightgrey?logo=apple) +![Linux](https://img.shields.io/badge/Linux-Yes-yellow?logo=linux) + + `Raid` is a configurable command-line application that orchestrates common development tasks, environments, and dependencies across distributed code repositories. If you have ever pulled a repo (or repos) that require days of configuration just to get a passing build, or have onboarded to a new team that has no documentation, or have a folder of scripts to automate your tasks but haven't -shared them yet, then you are probably a software engineer in need of this. +shared them yet, then you are probably in need of this. `Raid` handles the pain of error-prone knowledge-dependent tasks and management of your development environment. You no longer need to worry about wasted time onboarding new contributors. Tribal knowledge can be codified into the repo itself. And you will @@ -14,33 +19,45 @@ never miss running that one test ever again. 📖 For a deeper look at the goals and design of raid, see the [design proposal blog post](https://alexsalerno.dev/blog/raid-design-proposal?utm_source=chatgpt.com). -[Getting Started](#getting-started) • [Best Practices](#⚠-best-practices) • [Documentation](#usage--documentation) - ## Key Features -- **Portable YAML Configurations**: Define your development environments, tasks, and dependencies using simple, version-controlled YAML files. +- **Portable YAML Configurations**: Define your development environments, tasks, and dependencies using simple, version-controlled YAML files. Your configurations live alongside your code, making them easy to share and maintain. - **Multiple Profiles**: Easily switch between different project setups or team configurations with isolated profiles. - **Automated Task Execution**: Orchestrate shell commands, scripts, and custom tasks across multiple repositories with a single command. - **Environment Management**: Define, share, and execute complex development environments to ensure consistency for all contributors. -| Platform | Supported | -|----------|:---------:| -| Linux | ✅ | -| Mac | ✅ | -| Windows | ✅ | - ## Development `Raid` is currently in the **prototype stage**. Core functionality is still being explored and iterated on, so expect frequent changes and incomplete features. Feedback, issues, and contributions are welcome as the project takes shape. +--- + +[Getting Started](#getting-started) • [Best Practices](#best-practices) • [Documentation](#usage--documentation) + +--- + ## Getting Started ### Installation +#### MacOS + +```bash +brew install raid # coming soon +``` + +#### Linux + ```bash -# Installation instructions will be added here +# coming soon +``` + +#### Windows + +```bash +# coming soon ``` ### Configuration @@ -57,10 +74,15 @@ raid install # Clone repos and setup environment raid env dev # Execute development environment (if configured) ``` -## ⚠ Best Practices +## Best Practices + + ### Store sensitive profiles securely + + If your raid profile contains sensitive configuration or secrets, keep it in a secure, private location outside of your public codebase. + + ### Never commit secrets -- **Store profiles securely:** If your raid profile contains sensitive configuration or secrets, keep it in a secure, private location outside of your public codebase. -- **Never commit secrets:** Always keep secrets and credentials in private raid profiles. Do not store them in public repositories. + Always keep secrets and credentials in private raid profiles. Do not store them in public repositories. ## Usage & Documentation diff --git a/go.mod b/go.mod index 4731129..eef56cd 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 gopkg.in/yaml.v3 v3.0.1 + github.com/joho/godotenv v1.5.1 ) require ( diff --git a/go.sum b/go.sum index 9dffe49..cfbd5a5 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/src/cmd/env/env.go b/src/cmd/env/env.go index 172fda4..14b0e93 100644 --- a/src/cmd/env/env.go +++ b/src/cmd/env/env.go @@ -1,23 +1,46 @@ package env import ( + "github.com/8bitalex/raid/src/raid" + "github.com/8bitalex/raid/src/raid/env" "github.com/spf13/cobra" ) -var ( - concurrency int -) - func init() { - Command.Flags().IntVarP(&concurrency, "threads", "t", 0, "Maximum number of concurrent task executions (0 = unlimited)") + Command.AddCommand(ListEnvCmd) } var Command = &cobra.Command{ Use: "env [environment-name]", Short: "Execute an environment", Long: "Execute an environment by name. The environment will be searched for in the active profile and all repository configurations. Tasks are executed concurrently and environment variables are set globally.", - Args: cobra.ExactArgs(1), + Args: cobra.RangeArgs(0, 1), Run: func(cmd *cobra.Command, args []string) { - + if len(args) == 0 { + env := env.Get() + if env == "" { + cmd.PrintErrln("No active environment set.") + } else { + cmd.Println("Active environment:", env) + } + } else if len(args) == 1 { + name := args[0] + if !env.Contains(name) { + cmd.PrintErrln("Environment not found:", name) + } else { + cmd.Println("Setting up environment:", name) + if err := env.Set(name); err != nil { + cmd.PrintErrln("Failed to switch environment:", err) + } + raid.ForceLoad() + if err := env.Execute(env.Get()); err != nil { + cmd.PrintErrln("Failed to execute environment:", err) + } else { + cmd.Println("Environment executed successfully.") + } + } + } else { + cmd.PrintErrln("Invalid number of arguments.") + } }, } diff --git a/src/cmd/env/list.go b/src/cmd/env/list.go new file mode 100644 index 0000000..3d85b74 --- /dev/null +++ b/src/cmd/env/list.go @@ -0,0 +1,25 @@ +package env + +import ( + "fmt" + + "github.com/8bitalex/raid/src/raid/env" + "github.com/spf13/cobra" +) + +var ListEnvCmd = &cobra.Command{ + Use: "list", + Short: "List environments", + Run: func(cmd *cobra.Command, args []string) { + envs := env.ListAll() + if len(envs) == 0 { + fmt.Println("No environments found.") + return + } + fmt.Println("Available environments:") + for _, env := range envs { + fmt.Printf("\t%s\n", env) + } + fmt.Print() + }, +} diff --git a/src/cmd/profile/list.go b/src/cmd/profile/list.go index ad44a84..fad7a1e 100644 --- a/src/cmd/profile/list.go +++ b/src/cmd/profile/list.go @@ -11,7 +11,7 @@ var ListProfileCmd = &cobra.Command{ Use: "list", Short: "List profiles", Run: func(cmd *cobra.Command, args []string) { - profiles := pro.GetAll() + profiles := pro.ListAll() activeProfile := pro.Get() if len(profiles) == 0 { diff --git a/src/cmd/profile/profile.go b/src/cmd/profile/profile.go index ed72eb1..a1d61c9 100644 --- a/src/cmd/profile/profile.go +++ b/src/cmd/profile/profile.go @@ -2,6 +2,7 @@ package profile import ( "fmt" + "os" pro "github.com/8bitalex/raid/src/raid/profile" "github.com/spf13/cobra" @@ -10,7 +11,6 @@ import ( func init() { Command.AddCommand(AddProfileCmd) Command.AddCommand(ListProfileCmd) - Command.AddCommand(UseProfileCmd) Command.AddCommand(RemoveProfileCmd) } @@ -18,13 +18,24 @@ var Command = &cobra.Command{ Use: "profile", Aliases: []string{"p"}, Short: "Manage raid profiles", - Args: cobra.NoArgs, + Args: cobra.RangeArgs(0, 1), Run: func(cmd *cobra.Command, args []string) { - profile := pro.Get() - if !profile.IsZero() { - fmt.Println(profile.Name) + if len(args) == 0 { + profile := pro.Get() + if !profile.IsZero() { + fmt.Println(profile.Name) + } else { + fmt.Println("No active profile found. Use 'raid profile use ' to set one.") + } + } else if len(args) == 1 { + name := args[0] + if err := pro.Set(name); err != nil { + fmt.Printf("Profile '%s' not found. Use 'raid profile list' to see available profiles.\n", name) + os.Exit(1) + } + fmt.Printf("Profile '%s' is now active.\n", name) } else { - fmt.Println("No active profile found. Use 'raid profile use ' to set one.") + cmd.PrintErrln("Invalid number of arguments.") } }, } diff --git a/src/cmd/profile/use.go b/src/cmd/profile/use.go deleted file mode 100644 index 380a87c..0000000 --- a/src/cmd/profile/use.go +++ /dev/null @@ -1,26 +0,0 @@ -package profile - -import ( - "fmt" - "os" - - pro "github.com/8bitalex/raid/src/raid/profile" - "github.com/spf13/cobra" -) - -var UseProfileCmd = &cobra.Command{ - Use: "use profile", - Short: "Use a specific profile", - SuggestFor: []string{"set"}, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - name := args[0] - - if err := pro.Set(name); err != nil { - fmt.Printf("Profile '%s' not found. Use 'raid profile list' to see available profiles.\n", name) - os.Exit(1) - } - fmt.Printf("Profile '%s' is now active.\n", name) - fmt.Print() - }, -} diff --git a/src/internal/lib/config.go b/src/internal/lib/config.go index 5136c1a..0ee715f 100644 --- a/src/internal/lib/config.go +++ b/src/internal/lib/config.go @@ -5,6 +5,15 @@ import ( "github.com/spf13/viper" ) +const ( + ConfigDirName = ".raid" + ConfigFileName = "config.toml" + ConfigPathDefault = "~" + sys.Sep + ConfigDirName + sys.Sep + ConfigFileName + ConfigPathFlag = "config" + ConfigPathFlagShort = "c" + ConfigPathFlagDesc = "configuration file path (default is " + ConfigPathDefault + ")" +) + var CfgPath string var defaultConfigPath = sys.GetHomeDir() + sys.Sep + ConfigDirName + sys.Sep diff --git a/src/internal/lib/env.go b/src/internal/lib/env.go index 43f5bce..84bf6ea 100644 --- a/src/internal/lib/env.go +++ b/src/internal/lib/env.go @@ -1,126 +1,115 @@ package lib -// import ( -// "fmt" -// "os" -// "path/filepath" -// "strings" -// ) - -// // EnvironmentManager handles environment execution -// type EnvironmentManager struct { -// taskRunner *TaskRunner -// } - -// // NewEnvironmentManager creates a new environment manager -// func NewEnvironmentManager(concurrency int) *EnvironmentManager { -// return &EnvironmentManager{ -// taskRunner: NewTaskRunner(concurrency), -// } -// } - -// // ExecuteEnvironment executes an environment by name -// func (em *EnvironmentManager) ExecuteEnvironment(envName string) error { -// // Get the active profile -// profile, err := GetActiveProfileContent() -// if err != nil { -// return fmt.Errorf("failed to get active profile: %w", err) -// } - -// fmt.Printf("Executing environment '%s' for profile '%s'\n", envName, profile.Name) - -// // Find and execute environment from profile first -// profileEnv, found := em.findEnvironmentInProfile(profile, envName) -// if found { -// fmt.Println("Found environment in profile, executing...") -// if err := em.executeEnvironment(profileEnv); err != nil { -// return fmt.Errorf("failed to execute profile environment: %w", err) -// } -// } - -// // Find and execute environments from repositories -// for _, repo := range profile.Repositories { -// fmt.Printf("Checking repository '%s' for environment '%s'\n", repo.Name, envName) - -// repoEnv, found := em.findEnvironmentInRepository(repo, envName) -// if found { -// fmt.Printf("Found environment in repository '%s', executing...\n", repo.Name) -// if err := em.executeEnvironment(repoEnv); err != nil { -// return fmt.Errorf("failed to execute repository environment '%s': %w", repo.Name, err) -// } -// } -// } - -// return nil -// } - -// // findEnvironmentInProfile finds an environment in the profile -// func (em *EnvironmentManager) findEnvironmentInProfile(profile *ProfileContent, envName string) (*Environment, bool) { -// for _, env := range profile.Environments { -// if strings.EqualFold(env.Name, envName) { -// return &env, true -// } -// } -// return nil, false -// } - -// // findEnvironmentInRepository finds an environment in a repository -// func (em *EnvironmentManager) findEnvironmentInRepository(repo Repository, envName string) (*Environment, bool) { -// // Read the repository configuration file -// repoConfigPath := filepath.Join(repo.Path, "raid.yaml") -// if _, err := os.Stat(repoConfigPath); os.IsNotExist(err) { -// // Try raid.yml -// repoConfigPath = filepath.Join(repo.Path, "raid.yml") -// if _, err := os.Stat(repoConfigPath); os.IsNotExist(err) { -// // Try raid.json -// repoConfigPath = filepath.Join(repo.Path, "raid.json") -// if _, err := os.Stat(repoConfigPath); os.IsNotExist(err) { -// return nil, false -// } -// } -// } - -// // Read and parse the repository configuration -// repoProfile, err := ReadProfileFile(repoConfigPath) -// if err != nil { -// fmt.Printf("Warning: failed to read repository config %s: %v\n", repoConfigPath, err) -// return nil, false -// } - -// // Look for the environment in the repository -// for _, env := range repoProfile.Environments { -// if strings.EqualFold(env.Name, envName) { -// return &env, true -// } -// } - -// return nil, false -// } - -// // executeEnvironment executes an environment -// func (em *EnvironmentManager) executeEnvironment(env *Environment) error { -// fmt.Printf("Executing environment '%s'\n", env.Name) - -// // Set environment variables first -// if err := em.setEnvironmentVariables(env.Variables); err != nil { -// return fmt.Errorf("failed to set environment variables: %w", err) -// } - -// // Execute tasks -// if err := em.taskRunner.ExecuteTasks(env.Tasks); err != nil { -// return fmt.Errorf("failed to execute tasks: %w", err) -// } - -// return nil -// } - -// // setEnvironmentVariables sets the environment variables globally -// func (em *EnvironmentManager) setEnvironmentVariables(vars []EnvironmentVariable) error { -// for _, v := range vars { -// fmt.Printf("Setting environment variable: %s=%s\n", v.Name, v.Value) -// if err := os.Setenv(v.Name, v.Value); err != nil { -// return fmt.Errorf("failed to set environment variable %s: %w", v.Name, err) -// } -// } -// return nil -// } +import ( + "fmt" + + sys "github.com/8bitalex/raid/src/internal/sys" + "github.com/joho/godotenv" + "github.com/spf13/viper" +) + +const ( + ACTIVE_ENV_KEY = "env" +) + +type Env struct { + Name string `json:"name"` + Variables []EnvVar `json:"variables"` +} + +func (e Env) IsZero() bool { + return e.Name == "" +} + +type EnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func SetEnv(name string) error { + if name == "" || !ContainsEnv(name) { + return fmt.Errorf("environment '%s' not found", name) + } + + Set(ACTIVE_ENV_KEY, name) + return nil +} + +func GetEnv() string { + return viper.GetString(ACTIVE_ENV_KEY) +} + +func ListEnvs() []string { + if context == nil || len(context.Profile.Environments) == 0 { + return []string{} + } + + names := make([]string, 0, len(context.Profile.Environments)) + for _, env := range context.Profile.Environments { + names = append(names, env.Name) + } + return names +} + +func ContainsEnv(name string) bool { + for _, envName := range ListEnvs() { + if envName == name { + return true + } + } + return false +} + +func ExecuteEnv(name string) error { + for _, repo := range context.Profile.Repositories { + fmt.Printf("Setting up environment for repo: %s\n", repo.Name) + + path, err := buildEnvPath(repo.Path) + if err != nil { + return fmt.Errorf("invalid path for repo '%s': %w", repo.Name, err) + } + + pEnv := context.Profile.getEnv(name) + rEnv := repo.getEnv(name) + + err = setEnvVariables(pEnv.Variables, rEnv.Variables, path) + if err != nil { + return fmt.Errorf("failed to set env variables for repo '%s': %w", repo.Name, err) + } + } + return nil +} + +func buildEnvPath(path string) (string, error) { + filepath := sys.ExpandPath(path) + sys.Sep + ".env" + // create file if it does not exist + file, err := sys.CreateFile(filepath) + if err != nil { + return "", err + } + file.Close() + return filepath, nil +} + +func setEnvVariables(profVars []EnvVar, repoVars []EnvVar, path string) error { + envMap, err := godotenv.Read(path) + if err != nil { + return err + } + + for _, v := range profVars { + envMap[v.Name] = v.Value + } + + for _, v := range repoVars { + fmt.Printf("Setting variable %s=%s\n", v.Name, v.Value) + envMap[v.Name] = v.Value + } + + err = godotenv.Write(envMap, path) + if err != nil { + return err + } + return nil +} + diff --git a/src/internal/lib/lib.go b/src/internal/lib/lib.go index 87bb37c..e249d9b 100644 --- a/src/internal/lib/lib.go +++ b/src/internal/lib/lib.go @@ -12,24 +12,27 @@ const ( type Context struct { Profile Profile + Env string } var context *Context -func Compile() error { +func Load() error { if context == nil { - return ForceCompile() + return ForceLoad() } return nil } -func ForceCompile() error { - profile, err := BuildProfile(GetProfile()) +func ForceLoad() error { + profile, err := buildProfile(GetProfile()) if err != nil { return err } + context = &Context{ Profile: profile, + Env: GetEnv(), } return nil } diff --git a/src/internal/lib/profile.go b/src/internal/lib/profile.go index f9bc602..ea46510 100644 --- a/src/internal/lib/profile.go +++ b/src/internal/lib/profile.go @@ -15,20 +15,32 @@ import ( "gopkg.in/yaml.v3" ) -const ACTIVE_PROFILE_KEY = "profile" -const ALL_PROFILES_KEY = "profiles" -const SCHEMA_PATH = "schemas/raid-profile.schema.json" +const ( + ACTIVE_PROFILE_KEY = "profile" + ALL_PROFILES_KEY = "profiles" + PROFILE_SCHEMA_PATH = "schemas/raid-profile.schema.json" +) type Profile struct { Name string `json:"name"` Path string `json:"path"` Repositories []Repo `json:"repositories"` + Environments []Env `json:"environments"` } func (p Profile) IsZero() bool { return p.Name == "" || p.Path == "" } +func (p Profile) getEnv(name string) Env { + for _, env := range p.Environments { + if env.Name == name { + return env + } + } + return Env{} +} + func SetProfile(name string) error { if !ContainsProfile(name) { return fmt.Errorf("profile '%s' not found", name) @@ -38,6 +50,10 @@ func SetProfile(name string) error { } func GetProfile() Profile { + if context != nil && !context.Profile.IsZero() { + return context.Profile + } + name := viper.GetString(ACTIVE_PROFILE_KEY) paths := getProfilePaths() return Profile{ @@ -62,7 +78,7 @@ func AddProfiles(profiles []Profile) { } } -func GetProfiles() []Profile { +func ListProfiles() []Profile { profilesMap := getProfilePaths() results := make([]Profile, 0, len(profilesMap)) for name, path := range profilesMap { @@ -195,7 +211,7 @@ func ValidateProfile(path string) error { } c := jsonschema.NewCompiler() - sch, err := c.Compile(SCHEMA_PATH) + sch, err := c.Compile(PROFILE_SCHEMA_PATH) if err != nil { return err } @@ -237,7 +253,7 @@ func yamlToJSON(file io.Reader) ([]byte, error) { return json.Marshal(data) } -func BuildProfile(profile Profile) (Profile, error) { +func buildProfile(profile Profile) (Profile, error) { if profile.IsZero() { return Profile{}, fmt.Errorf("invalid profile: %v", profile) } @@ -253,87 +269,3 @@ func BuildProfile(profile Profile) (Profile, error) { } return profile, nil } - -// // ProfileContent represents the content of a profile file -// type ProfileContent struct { -// Name string `json:"name" yaml:"name"` -// Repositories []Repository `json:"repositories" yaml:"repositories"` -// Environments []Environment `json:"environments" yaml:"environments"` -// } - -// // Repository represents a repository in a profile -// type Repository struct { -// Name string `json:"name" yaml:"name"` -// Path string `json:"path" yaml:"path"` -// URL string `json:"url" yaml:"url"` -// } - -// // Environment represents an environment configuration -// type Environment struct { -// Name string `json:"name" yaml:"name"` -// Tasks []Task `json:"tasks" yaml:"tasks"` -// Variables []EnvironmentVariable `json:"variables" yaml:"variables"` -// } - -// // Task represents a task to be executed -// type Task struct { -// Type string `json:"type" yaml:"type"` -// Cmd string `json:"cmd,omitempty" yaml:"cmd,omitempty"` -// Path string `json:"path,omitempty" yaml:"path,omitempty"` -// } - -// // EnvironmentVariable represents an environment variable -// type EnvironmentVariable struct { -// Name string `json:"name" yaml:"name"` -// Value string `json:"value" yaml:"value"` -// } - -// // GetActiveProfileContent reads and parses the active profile file -// func GetActiveProfileContent() (*ProfileContent, error) { -// activeProfile := GetProfile() -// if activeProfile == "" { -// return nil, fmt.Errorf("no active profile set. Use 'raid profile use ' to set an active profile") -// } - -// profilePath, err := GetProfilePath(activeProfile) -// if err != nil { -// return nil, fmt.Errorf("failed to get profile path for '%s': %w", activeProfile, err) -// } - -// return ReadProfileFile(profilePath) -// } - -// // ReadProfileFile reads and parses a profile file -// func ReadProfileFile(filePath string) (*ProfileContent, error) { -// // Read the profile file -// profileData, err := os.ReadFile(filePath) -// if err != nil { -// return nil, fmt.Errorf("failed to read profile file: %w", err) -// } - -// // Check file extension to determine format -// ext := strings.ToLower(filepath.Ext(filePath)) -// var profile ProfileContent - -// switch ext { -// case ".yaml", ".yml": -// // Parse YAML -// if err := yaml.Unmarshal(profileData, &profile); err != nil { -// return nil, fmt.Errorf("invalid YAML format: %w", err) -// } -// case ".json": -// // Parse JSON -// if err := json.Unmarshal(profileData, &profile); err != nil { -// return nil, fmt.Errorf("invalid JSON format: %w", err) -// } -// default: -// return nil, fmt.Errorf("unsupported file format: %s. Supported formats are .yaml, .yml, and .json", ext) -// } - -// // Validate required fields -// if profile.Name == "" { -// return nil, fmt.Errorf("profile file is missing required 'name' field") -// } - -// return &profile, nil -// } diff --git a/src/internal/lib/repo.go b/src/internal/lib/repo.go index 4170371..ecb9d75 100644 --- a/src/internal/lib/repo.go +++ b/src/internal/lib/repo.go @@ -10,16 +10,26 @@ import ( ) type Repo struct { - Name string - Path string - URL string + Name string `json:"name"` + Path string `json:"path"` + URL string `json:"url"` + Environments []Env `json:"environments"` } func (r Repo) IsZero() bool { return r.Name == "" || r.Path == "" || r.URL == "" } -func BuildRepo(repo Repo) (Repo, error) { +func (r Repo) getEnv(name string) Env { + for _, env := range r.Environments { + if env.Name == name { + return env + } + } + return Env{} +} + +func buildRepo(repo Repo) (Repo, error) { return repo, nil } diff --git a/src/internal/lib/strings.go b/src/internal/lib/strings.go deleted file mode 100644 index b8b65d1..0000000 --- a/src/internal/lib/strings.go +++ /dev/null @@ -1,12 +0,0 @@ -package lib - -import "github.com/8bitalex/raid/src/internal/sys" - -const ( - ConfigDirName = ".raid" - ConfigFileName = "config.toml" - ConfigPathDefault = "~" + sys.Sep + ConfigDirName + sys.Sep + ConfigFileName - ConfigPathFlag = "config" - ConfigPathFlagShort = "c" - ConfigPathFlagDesc = "configuration file path (default is " + ConfigPathDefault + ")" -) diff --git a/src/internal/sys/system.go b/src/internal/sys/system.go index 6cf99c9..f002d24 100644 --- a/src/internal/sys/system.go +++ b/src/internal/sys/system.go @@ -20,15 +20,14 @@ func GetHomeDir() string { return home } -func CreateFile(filePath string) *os.File { +func CreateFile(filePath string) (*os.File, error) { pathEx := ExpandPath(filePath) - os.MkdirAll(path.Dir(pathEx), os.ModeDir|0755) - - file, err := os.Create(pathEx) - if err != nil { - log.Fatalf("Failed to create file '%s': %v", pathEx, err) + if FileExists(pathEx) { + return os.Open(pathEx) } - return file + + os.MkdirAll(path.Dir(pathEx), os.ModeDir|0755) + return os.Create(pathEx) } func FileExists(path string) bool { diff --git a/src/internal/utils/Common.go b/src/internal/utils/Common.go new file mode 100644 index 0000000..273762e --- /dev/null +++ b/src/internal/utils/Common.go @@ -0,0 +1,15 @@ +package utils + +import "fmt" + +func MergeErr(errs []error) error { + var result string + for _, err := range errs { + if len(result) == 0 { + result = err.Error() + } else { + result = result + ", " + err.Error() + } + } + return fmt.Errorf("%s", result) +} diff --git a/src/internal/utils/cobra-ext.go b/src/internal/utils/cobra-ext.go index b61c138..c24d6f9 100644 --- a/src/internal/utils/cobra-ext.go +++ b/src/internal/utils/cobra-ext.go @@ -1,8 +1,6 @@ package utils import ( - "fmt" - "github.com/spf13/cobra" ) @@ -17,18 +15,6 @@ func MatchOne(pargs ...cobra.PositionalArgs) cobra.PositionalArgs { errors = append(errors, err) } } - return mergeErr(errors) - } -} - -func mergeErr(errs []error) error { - var result string - for _, err := range errs { - if len(result) == 0 { - result = err.Error() - } else { - result = result + ", " + err.Error() - } + return MergeErr(errors) } - return fmt.Errorf("%s", result) } diff --git a/src/raid/env/env.go b/src/raid/env/env.go new file mode 100644 index 0000000..006294e --- /dev/null +++ b/src/raid/env/env.go @@ -0,0 +1,26 @@ +// Manage raid environments. +package env + +import "github.com/8bitalex/raid/src/internal/lib" + +type Env = lib.Env + +func Set(name string) error { + return lib.SetEnv(name) +} + +func Get() string { + return lib.GetEnv() +} + +func ListAll() []string { + return lib.ListEnvs() +} + +func Contains(name string) bool { + return lib.ContainsEnv(name) +} + +func Execute(env string) error { + return lib.ExecuteEnv(env) +} diff --git a/src/raid/profile/profile.go b/src/raid/profile/profile.go index f342239..85717d1 100644 --- a/src/raid/profile/profile.go +++ b/src/raid/profile/profile.go @@ -1,6 +1,4 @@ -/* -Manage raid profiles. -*/ +// Manage raid profiles. package profile import "github.com/8bitalex/raid/src/internal/lib" @@ -13,8 +11,8 @@ func Get() Profile { } // Returns a slice of all added profiles -func GetAll() []Profile { - return lib.GetProfiles() +func ListAll() []Profile { + return lib.ListProfiles() } // Adds a profile to the available profile list diff --git a/src/raid/raid.go b/src/raid/raid.go index 1447ab0..5c03d04 100644 --- a/src/raid/raid.go +++ b/src/raid/raid.go @@ -37,19 +37,19 @@ func Initialize() { if err := lib.InitConfig(); err != nil { log.Fatalf("Failed to initialize configuration: %v", err) } - if err := Compile(); err != nil { + if err := Load(); err != nil { log.Fatalf("Failed to compile configurations: %v", err) } } -// Compile the raid configurations for execution. Uses cached results if available. -func Compile() error { - return lib.Compile() +// Load the raid configurations for execution. Uses cached results if available. +func Load() error { + return lib.Load() } -// Force compile the raid configurations for execution. Ignores cache. -func ForceCompile() error { - return lib.ForceCompile() +// Force load the raid configurations for execution. Ignores cache. +func ForceLoad() error { + return lib.ForceLoad() } // Install the active profile