diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..05d722f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + - name: Build binaries + run: | + cd apps/cli + mkdir -p release + GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o release/radas-linux-amd64 . + GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o release/radas-darwin-amd64 . + GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o release/radas-darwin-arm64 . + GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o release/radas-windows-amd64.exe . + + - name: Compress + run: | + cd apps/cli/release + tar czf radas-linux-amd64.tar.gz radas-linux-amd64 + tar czf radas-darwin-amd64.tar.gz radas-darwin-amd64 + tar czf radas-darwin-arm64.tar.gz radas-darwin-arm64 + zip radas-windows-amd64.zip radas-windows-amd64.exe + rm -f radas-linux-amd64 radas-darwin-amd64 radas-darwin-arm64 radas-windows-amd64.exe + + - name: GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: apps/cli/release/* + + - name: Discord Notification + uses: Ilshidur/action-discord@master + env: + DISCORD_WEBHOOK: https://discord.com/api/webhooks/1417629457224569014/AcwTbsQRb5-psBaLJ8fUuXt2gaJRmNMWg9N3xwxLvVot6IOYmMgmWOPjRb3eJK89pgza + with: + args: | + 🚀 **New Release: ${{ github.ref_name }}** + + Published by ${{ github.actor }} + + ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }} diff --git a/apps/cli/.gitignore b/apps/cli/.gitignore index 4db98d6..d995bf6 100644 --- a/apps/cli/.gitignore +++ b/apps/cli/.gitignore @@ -5,4 +5,5 @@ .env.production.local # Build artifacts -bin/ \ No newline at end of file +bin/ +release/ \ No newline at end of file diff --git a/apps/cli/README.md b/apps/cli/README.md index d352548..6fc7638 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -275,6 +275,39 @@ If you want to contribute to the project, please read the [contributing guide](h +## Security scanning + +Detect committed secrets in the repo or any subdirectory using gitleaks. + +```bash +# Emit SARIF 2.1.0 (default; pipe into GitHub Code Scanning or any SARIF viewer) +radas scan secrets > radas.sarif + +# Human-readable table +radas scan secrets --format=table + +# Limit to staged files (pre-commit hook) +radas scan secrets --staged + +# Use a custom .gitleaks.toml +radas scan secrets --config=./.gitleaks.toml +``` + +Exit code 0 means no secrets found; exit code 1 means findings; exit code 2 means a scan error. + +### GitHub Actions + +```yaml +- name: Scan for secrets + run: radas scan secrets --format=sarif > radas.sarif || true +- name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: radas.sarif +``` + +Requires the `gitleaks/v8` Go library (handled by `go build` automatically). + ## ⚠️ License [`The Radas CLI`][repo_url] is free and open-source software licensed under the [Apache 2.0 License][repo_license_url], created and supported by [TreonStudio][author_url] with 🩵 for people and robots. Use it confidently in both personal and commercial projects. Official logo distributed under the [Creative Commons License][repo_cc_license_url] (CC BY-SA 4.0 International). diff --git a/apps/cli/cmd/backend/backend.go b/apps/cli/cmd/backend/backend.go index 7dbea76..e108323 100644 --- a/apps/cli/cmd/backend/backend.go +++ b/apps/cli/cmd/backend/backend.go @@ -18,4 +18,6 @@ func init() { Cmd.AddCommand(InstallCmd) Cmd.AddCommand(CleanCmd) Cmd.AddCommand(FreshCmd) + Cmd.AddCommand(BeIgnoreCmd) + Cmd.AddCommand(DevCmd) } \ No newline at end of file diff --git a/apps/cli/cmd/backend/dev.go b/apps/cli/cmd/backend/dev.go new file mode 100644 index 0000000..3d8a778 --- /dev/null +++ b/apps/cli/cmd/backend/dev.go @@ -0,0 +1,218 @@ +package backend + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/raizora/radas/v4/internal/config" + "github.com/raizora/radas/v4/internal/utils" +) + +// DevCmd is the command to run the backend dev server +var DevCmd = &cobra.Command{ + Use: "dev [--watch]", + Short: "Run backend dev server", + Long: `Start the backend development server. Auto-detects stack (Go, Elixir, PHP, Laravel) +and picks the right run command. Uses radas.yml run config if available. + +Flags: + --watch enable hot-reload (auto-detects air, gow, reflex) + --tool force a specific watch tool (air, gow, reflex, nodemon) + --port override server port +`, + Run: func(cmd *cobra.Command, args []string) { + stack, dir := detectBackendStack() + if stack == "" { + fmt.Println("Could not detect backend stack. Supported: Golang, Elixir, PHP, Laravel.") + os.Exit(1) + } + fmt.Printf("Detected backend stack: %s (at %s)\n", stack, dir) + + watch := cmd.Flags().Changed("watch") + watchTool, _ := cmd.Flags().GetString("tool") + port, _ := cmd.Flags().GetInt("port") + + // Try to load radas.yml config for run settings + runCmd := "" + if cfgPath, err := config.FindConfig(); err == nil { + if cfg, err := config.ParseConfig(cfgPath); err == nil { + if cfg.Run.Command != "" { + runCmd = cfg.Run.Command + } + if !watch && cfg.Run.Watch { + watch = true + } + if watchTool == "" && cfg.Run.WatchTool != "" { + watchTool = cfg.Run.WatchTool + } + if port == 0 && cfg.Server.Port != 0 { + port = cfg.Server.Port + } + } + } + + runDevServer(stack, dir, runCmd, watch, watchTool, port) + }, +} + +func init() { + DevCmd.Flags().BoolP("watch", "w", false, "enable hot-reload") + DevCmd.Flags().String("tool", "", "watch tool (air, gow, reflex, nodemon)") + DevCmd.Flags().Int("port", 0, "override server port") +} + +func runDevServer(stack, dir, runCmd string, watch bool, watchTool string, port int) { + switch stack { + case "golang": + runGoDev(dir, runCmd, watch, watchTool, port) + case "elixir": + runElixirDev(dir, runCmd, port) + case "laravel", "php": + runPhpDev(dir, runCmd, stack, port) + } +} + +func findMainPackage(dir string) string { + // Look for common main package locations + candidates := []string{ + filepath.Join(dir, "cmd", "server"), + filepath.Join(dir, "cmd", "api"), + filepath.Join(dir, "cmd", "app"), + filepath.Join(dir, "cmd"), + dir, + } + for _, c := range candidates { + mainFile := filepath.Join(c, "main.go") + if _, err := os.Stat(mainFile); err == nil { + rel, _ := filepath.Rel(dir, c) + return rel + } + } + return "." +} + +func findWatchTool() string { + for _, tool := range []string{"air", "gow", "reflex", "nodemon", "entr"} { + if utils.CheckIfCommandExists(tool) { + return tool + } + } + return "" +} + +func runGoDev(dir, runCmd string, watch bool, watchTool string, port int) { + if runCmd == "" { + mainPkg := findMainPackage(dir) + runCmd = fmt.Sprintf("go run ./%s", mainPkg) + } + + if watch && watchTool == "" { + watchTool = findWatchTool() + } + + parts := strings.Fields(runCmd) + + if watch && watchTool != "" { + // Wrap with watch tool + var cmd *exec.Cmd + switch watchTool { + case "air": + // air reads .air.toml; just run it + cmd = exec.Command("air") + case "gow": + cmd = exec.Command("gow", parts...) + case "reflex": + reflexArgs := []string{"-r", `\.go$`, "--"} + reflexArgs = append(reflexArgs, parts...) + cmd = exec.Command("reflex", reflexArgs...) + case "nodemon": + cmd = exec.Command("nodemon", "--exec", parts[0], strings.Join(parts[1:], " ")) + case "entr": + fmt.Println("For entr, use: ls *.go | entr -r", runCmd) + fmt.Println("Falling back to direct run...") + cmd = exec.Command(parts[0], parts[1:]...) + } + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fmt.Printf("Running: %s (via %s)\n", runCmd, watchTool) + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Dev server failed: %v\n", err) + os.Exit(1) + } + return + } + + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fmt.Printf("Running: %s\n", runCmd) + if port > 0 { + cmd.Env = append(os.Environ(), fmt.Sprintf("PORT=%d", port)) + } + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Dev server failed: %v\n", err) + os.Exit(1) + } +} + +func runElixirDev(dir, runCmd string, port int) { + if runCmd == "" { + runCmd = "mix phx.server" + } + parts := strings.Fields(runCmd) + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if port > 0 { + cmd.Env = append(os.Environ(), fmt.Sprintf("PORT=%d", port)) + } + fmt.Printf("Running: %s\n", runCmd) + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Dev server failed: %v\n", err) + os.Exit(1) + } +} + +func runPhpDev(dir, runCmd string, stack string, port int) { + if runCmd == "" { + if stack == "laravel" { + runCmd = "php artisan serve" + } else { + runCmd = "php -S localhost:8080 -t public" + } + } + parts := strings.Fields(runCmd) + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if port > 0 { + // Override port for artisan serve or php -S + if stack == "laravel" { + cmd.Env = append(os.Environ(), fmt.Sprintf("PORT=%d", port)) + } else { + // Replace port in php -S command + for i, p := range parts { + if p == "localhost:8080" { + parts[i] = fmt.Sprintf("localhost:%d", port) + } + } + cmd = exec.Command(parts[0], parts[1:]...) + cmd.Dir = dir + } + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fmt.Printf("Running: %s\n", strings.Join(parts, " ")) + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Dev server failed: %v\n", err) + os.Exit(1) + } +} diff --git a/apps/cli/cmd/backend/doctor.go b/apps/cli/cmd/backend/doctor.go index d2bf9a9..d26fd7c 100644 --- a/apps/cli/cmd/backend/doctor.go +++ b/apps/cli/cmd/backend/doctor.go @@ -8,7 +8,7 @@ import ( // DoctorCmd is the command to check Backend tools var DoctorCmd = &cobra.Command{ Use: "doctor", - Short: "Check Backend tools installation", + Short: "Check Backend tools installation & project configuration", Long: `Check if Go, Elixir, and other backend tools are installed and ready to use.`, Run: func(cmd *cobra.Command, args []string) { runBackendDoctor() @@ -21,4 +21,7 @@ func runBackendDoctor() { checker.CheckElixir() checker.CheckRust() checker.CheckMaven() + + // Detect project database + checker.PrintDatabaseResult(".") } \ No newline at end of file diff --git a/apps/cli/cmd/backend/ignore.go b/apps/cli/cmd/backend/ignore.go new file mode 100644 index 0000000..c9e42ad --- /dev/null +++ b/apps/cli/cmd/backend/ignore.go @@ -0,0 +1,64 @@ +package backend + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/raizora/radas/v4/internal/ignore" +) + +var BeIgnoreCmd = &cobra.Command{ + Use: "ignore [--force]", + Short: "Generate or merge .gitignore for a backend (Go) project", + Run: runBeIgnore, +} + +var beIgnoreForce bool + +func init() { + BeIgnoreCmd.Flags().BoolVar(&beIgnoreForce, "force", false, "overwrite existing .gitignore without merging") +} + +func runBeIgnore(cmd *cobra.Command, args []string) { + dest, err := os.MkdirTemp("", "radas-ignore-") + if err != nil { + beFatal("create temp dir: %v", err) + } + defer os.RemoveAll(dest) + + files, err := ignore.Fetch("be", "default", dest) + if err != nil { + beFatal("%v", err) + } + + for name, template := range files { + if err := beWriteOrMerge(name, template, beIgnoreForce); err != nil { + beFatal("%v", err) + } + } + fmt.Println("✓ Generated .gitignore for be") +} + +func beWriteOrMerge(name, template string, force bool) error { + existing := "" + if data, err := os.ReadFile(name); err == nil { + binary, _ := ignore.IsBinary(name) + if binary { + return fmt.Errorf("%s is binary; refusing to merge (use --force to overwrite)", name) + } + existing = string(data) + } else if !os.IsNotExist(err) { + return fmt.Errorf("read %s: %w", name, err) + } + merged, err := ignore.MergePatterns(existing, template, force) + if err != nil { + return fmt.Errorf("merge %s: %w", name, err) + } + return os.WriteFile(name, []byte(merged), 0644) +} + +func beFatal(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "be ignore: "+format+"\n", args...) + os.Exit(1) +} diff --git a/apps/cli/cmd/frontend/config.go b/apps/cli/cmd/frontend/config.go index d110fa8..3e86930 100644 --- a/apps/cli/cmd/frontend/config.go +++ b/apps/cli/cmd/frontend/config.go @@ -1,106 +1,23 @@ package frontend import ( - "fmt" - "os" - "path/filepath" - "strings" - - "gopkg.in/yaml.v3" + "github.com/raizora/radas/v4/internal/config" ) -// RadasConfig represents the structure of radas.yml -type RadasConfig struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - Type string `yaml:"type"` - Stacks []string `yaml:"stacks"` - Contract struct { - Design []struct { - Path string `yaml:"path"` - Type string `yaml:"type"` - } `yaml:"design"` - API []struct { - Path string `yaml:"path"` - Type string `yaml:"type"` - } `yaml:"api"` - } `yaml:"contract"` -} +// RadasConfig re-exports internal/config.RadasConfig for legacy compatibility. +type RadasConfig = config.RadasConfig -// ParseConfig reads and parses the radas.yml file +// ParseConfig delegates to internal/config. func ParseConfig(configPath string) (*RadasConfig, error) { - // If configPath is a directory, look for radas.yml inside it - if stat, err := os.Stat(configPath); err == nil && stat.IsDir() { - configPath = filepath.Join(configPath, "radas.yml") - } - - // Read the YAML file - data, err := os.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("failed to read config file: %w", err) - } - - // Parse the YAML data - var config RadasConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to parse config file: %w", err) - } - - return &config, nil + return config.ParseConfig(configPath) } -// FindConfig looks for radas.yml in the current directory and parent directories +// FindConfig delegates to internal/config. func FindConfig() (string, error) { - dir, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get current directory: %w", err) - } - - for { - configPath := filepath.Join(dir, "radas.yml") - if _, err := os.Stat(configPath); err == nil { - return configPath, nil - } - - // Stop if we're at the root directory - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - - return "", fmt.Errorf("radas.yml not found in current directory or any parent directory") + return config.FindConfig() } -// ResolvePath resolves a path from the configuration file -// If the path starts with ${RADAS_PLAYGROUND}, it will be replaced with the value of the RADAS_PLAYGROUND environment variable -// Otherwise, the path is assumed to be relative to the configuration file's directory +// ResolvePath delegates to internal/config. func ResolvePath(basePath, configPath string) string { - // Get the RADAS_PLAYGROUND environment variable - playgroundDir := os.Getenv("RADAS_PLAYGROUND") - - // Replace ${RADAS_PLAYGROUND} with the actual value - if strings.Contains(configPath, "${RADAS_PLAYGROUND}") && playgroundDir != "" { - return strings.Replace(configPath, "${RADAS_PLAYGROUND}", playgroundDir, 1) - } - - // If the path is absolute, return it as is - if filepath.IsAbs(configPath) { - return configPath - } - - // For generated directories (like __generated__), we want them to be relative to the config file location - if strings.HasPrefix(configPath, "__generated__") { - return filepath.Join(basePath, configPath) - } - - // If there's a playground directory and the path doesn't explicitly use it, - // interpret paths that are not output directories as relative to the playground - if playgroundDir != "" { - return filepath.Join(playgroundDir, configPath) - } - - // Otherwise, interpret the path as relative to the config file's directory - return filepath.Join(basePath, configPath) + return config.ResolvePath(basePath, configPath) } diff --git a/apps/cli/cmd/frontend/frontend.go b/apps/cli/cmd/frontend/frontend.go index db2adc6..db02b00 100644 --- a/apps/cli/cmd/frontend/frontend.go +++ b/apps/cli/cmd/frontend/frontend.go @@ -26,4 +26,5 @@ func init() { Cmd.AddCommand(genAPICmd) Cmd.AddCommand(genStylesCmd) Cmd.AddCommand(genAllCmd) + Cmd.AddCommand(FeIgnoreCmd) } \ No newline at end of file diff --git a/apps/cli/cmd/frontend/ignore.go b/apps/cli/cmd/frontend/ignore.go new file mode 100644 index 0000000..13387f9 --- /dev/null +++ b/apps/cli/cmd/frontend/ignore.go @@ -0,0 +1,76 @@ +package frontend + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/raizora/radas/v4/internal/ignore" +) + +var FeIgnoreCmd = &cobra.Command{ + Use: "ignore [--stack=nextjs|vite|remix] [--force]", + Short: "Generate or merge ignore files (.gitignore, .biomeignore, .prettierignore) for a frontend project", + Long: `Fetches the FE ignore-file templates from the radas templates +repo and writes them to the current directory. If a file already +exists, existing patterns are preserved and template patterns are +appended (deduplicated). Pass --force to overwrite without merging.`, + Run: runFeIgnore, +} + +var ( + feIgnoreStack string + feIgnoreForce bool +) + +func init() { + FeIgnoreCmd.Flags().StringVar(&feIgnoreStack, "stack", "nextjs", "stack variant (nextjs, vite, remix)") + FeIgnoreCmd.Flags().BoolVar(&feIgnoreForce, "force", false, "overwrite existing files without merging") +} + +func runFeIgnore(cmd *cobra.Command, args []string) { + dest, err := os.MkdirTemp("", "radas-ignore-") + if err != nil { + feFatal("create temp dir: %v", err) + } + defer os.RemoveAll(dest) + + files, err := ignore.Fetch("fe", feIgnoreStack, dest) + if err != nil { + feFatal("%v", err) + } + + for name, template := range files { + if err := feWriteOrMerge(name, template, feIgnoreForce); err != nil { + feFatal("%v", err) + } + } + fmt.Printf("✓ Generated ignore files for fe (stack=%s)\n", feIgnoreStack) +} + +func feWriteOrMerge(name, template string, force bool) error { + existing := "" + if data, err := os.ReadFile(name); err == nil { + binary, _ := ignore.IsBinary(name) + if binary { + return fmt.Errorf("%s is binary; refusing to merge (use --force to overwrite)", name) + } + existing = string(data) + } else if !os.IsNotExist(err) { + return fmt.Errorf("read %s: %w", name, err) + } + + merged, err := ignore.MergePatterns(existing, template, force) + if err != nil { + return fmt.Errorf("merge %s: %w", name, err) + } + if err := os.WriteFile(name, []byte(merged), 0644); err != nil { + return fmt.Errorf("write %s: %w", name, err) + } + return nil +} + +func feFatal(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "fe ignore: "+format+"\n", args...) + os.Exit(1) +} diff --git a/apps/cli/cmd/infra/ignore.go b/apps/cli/cmd/infra/ignore.go new file mode 100644 index 0000000..844c3d8 --- /dev/null +++ b/apps/cli/cmd/infra/ignore.go @@ -0,0 +1,68 @@ +package infra + +import ( + "fmt" + "os" + + "github.com/raizora/radas/v4/internal/ignore" + "github.com/spf13/cobra" +) + +var InfraIgnoreCmd = &cobra.Command{ + Use: "ignore [--stack=docker|terraform|k8s] [--force]", + Short: "Generate or merge .gitignore and .dockerignore for an infra project", + Run: runInfraIgnore, +} + +var ( + infraIgnoreStack string + infraIgnoreForce bool +) + +func init() { + InfraIgnoreCmd.Flags().StringVar(&infraIgnoreStack, "stack", "docker", "stack variant (docker, terraform, k8s)") + InfraIgnoreCmd.Flags().BoolVar(&infraIgnoreForce, "force", false, "overwrite existing files without merging") +} + +func runInfraIgnore(cmd *cobra.Command, args []string) { + dest, err := os.MkdirTemp("", "radas-ignore-") + if err != nil { + infraFatal("create temp dir: %v", err) + } + defer os.RemoveAll(dest) + + files, err := ignore.Fetch("infra", infraIgnoreStack, dest) + if err != nil { + infraFatal("%v", err) + } + + for name, template := range files { + if err := infraWriteOrMerge(name, template, infraIgnoreForce); err != nil { + infraFatal("%v", err) + } + } + fmt.Printf("✓ Generated ignore files for infra (stack=%s)\n", infraIgnoreStack) +} + +func infraWriteOrMerge(name, template string, force bool) error { + existing := "" + if data, err := os.ReadFile(name); err == nil { + binary, _ := ignore.IsBinary(name) + if binary { + return fmt.Errorf("%s is binary; refusing to merge (use --force to overwrite)", name) + } + existing = string(data) + } else if !os.IsNotExist(err) { + return fmt.Errorf("read %s: %w", name, err) + } + merged, err := ignore.MergePatterns(existing, template, force) + if err != nil { + return fmt.Errorf("merge %s: %w", name, err) + } + return os.WriteFile(name, []byte(merged), 0644) +} + +func infraFatal(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "infra ignore: "+format+"\n", args...) + os.Exit(1) +} diff --git a/apps/cli/cmd/infra/infra.go b/apps/cli/cmd/infra/infra.go index e0b9b53..13e0f47 100644 --- a/apps/cli/cmd/infra/infra.go +++ b/apps/cli/cmd/infra/infra.go @@ -14,4 +14,5 @@ var Cmd = &cobra.Command{ func init() { // Register all infra subcommands Cmd.AddCommand(DockerCmd) + Cmd.AddCommand(InfraIgnoreCmd) } diff --git a/apps/cli/cmd/root.go b/apps/cli/cmd/root.go index 956fc8e..414a3c7 100644 --- a/apps/cli/cmd/root.go +++ b/apps/cli/cmd/root.go @@ -41,4 +41,5 @@ func init() { rootCmd.AddCommand(rootcmd.UpdateCmd) rootCmd.AddCommand(rootcmd.RebuildCmd) rootCmd.AddCommand(rootcmd.PullCmd) + rootCmd.AddCommand(rootcmd.ScanCmd) } \ No newline at end of file diff --git a/apps/cli/cmd/rootcmd/config.go b/apps/cli/cmd/rootcmd/config.go index a9d2349..4478041 100644 --- a/apps/cli/cmd/rootcmd/config.go +++ b/apps/cli/cmd/rootcmd/config.go @@ -12,6 +12,24 @@ import ( "github.com/raizora/radas/v4/internal/config" ) +// dbDriver describes a selectable database driver in config init. +type dbDriver struct { + Label string // display label (e.g. "Supabase (Postgres platform)") + Name string // yaml value (e.g. "supabase") + DSN string // default DSN hint + Stack string // extra stack name, if any +} + +var dbDrivers = []dbDriver{ + {Label: "PostgreSQL", Name: "postgres", DSN: "postgres://user:pass@localhost:5432/dbname?sslmode=disable"}, + {Label: "Supabase (Postgres platform)", Name: "supabase", DSN: "postgres://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"}, + {Label: "Turso (Edge SQLite)", Name: "turso", DSN: "libsql://[DB-NAME]-[ORG].turso.io?authToken=[TOKEN]", Stack: "libsql"}, + {Label: "MySQL", Name: "mysql", DSN: "user:pass@tcp(localhost:3306)/dbname?parseTime=true"}, + {Label: "SQLite", Name: "sqlite", DSN: "./data.db"}, + {Label: "MongoDB", Name: "mongodb", DSN: "mongodb://localhost:27017/dbname"}, + {Label: "None", Name: "none"}, +} + var ConfigCmd = &cobra.Command{ Use: "config", Short: "Config file utilities (read/set radas.yml)", @@ -123,22 +141,307 @@ var ConfigInitCmd = &cobra.Command{ } _ = survey.AskOne(descPrompt, &description) // Allow empty, no exit on error - template := `# Last updated: 2025-05-02 -# Version: 1.0.0 + // For backend types, ask which database driver to use + var selectedDriver dbDriver + isBackend := selectedType == "backend-api" || selectedType == "monorepo-backend" + if isBackend { + labels := make([]string, len(dbDrivers)) + for i, d := range dbDrivers { + labels[i] = d.Label + } + var dbLabel string + dbPrompt := &survey.Select{ + Message: "Select database driver:", + Options: labels, + Default: "PostgreSQL", + } + err = survey.AskOne(dbPrompt, &dbLabel) + if err != nil { + fmt.Println("Prompt cancelled.") + os.Exit(1) + } + for _, d := range dbDrivers { + if d.Label == dbLabel { + selectedDriver = d + break + } + } + } + + if selectedType == "backend-api" { + var content string + if selectedDriver.Name == "none" { + content = fmt.Sprintf(`name: "%s" +description: "%s" +type: backend-api +stacks: [go] + +build: + main: ./cmd/server + output: ./bin/app + +gen: + handler: + template: templates/handler.gotpl + output: internal/handler + service: + template: templates/service.gotpl + output: internal/service + model: + template: templates/model.gotpl + output: internal/model + +server: + port: 8080 + +run: + command: go run ./cmd/server + watch: true + watch_tool: air + +test: + cover_threshold: 80 + flags: -race -count=1 +`, name, description) + } else { + stacks := "[go]" + if selectedDriver.Stack != "" { + stacks = fmt.Sprintf("[go, %s]", selectedDriver.Stack) + } + content = fmt.Sprintf(`name: "%s" +description: "%s" +type: backend-api +stacks: %s + +build: + main: ./cmd/server + output: ./bin/app + +db: + driver: %s + default_dsn: %s + migrations: ./migrations + seeds: ./seeds + +gen: + handler: + template: templates/handler.gotpl + output: internal/handler + service: + template: templates/service.gotpl + output: internal/service + model: + template: templates/model.gotpl + output: internal/model + +server: + port: 8080 + +run: + command: go run ./cmd/server + watch: true + watch_tool: air + +test: + cover_threshold: 80 + flags: -race -count=1 +`, name, description, stacks, selectedDriver.Name, selectedDriver.DSN) + } + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } + + if selectedType == "monorepo-backend" { + var content string + if selectedDriver.Name != "none" { + stacks := "[go, proto]" + if selectedDriver.Stack != "" { + stacks = fmt.Sprintf("[go, proto, %s]", selectedDriver.Stack) + } + content = fmt.Sprintf(`name: "%s" +description: "%s" +type: monorepo-backend +stacks: %s + +build: + main: ./cmd/server + output: ./bin + +db: + driver: %s + default_dsn: %s + migrations: ./migrations + seeds: ./seeds + +gen: + handler: + template: templates/handler.gotpl + output: internal/handler + service: + template: templates/service.gotpl + output: internal/service ---- -# Repository metadata -metadata: - name: "%s" - description: "%s" - version: "0.1.2y" - maintained_by: "Engineering Team" - documentation: "https://tech.raizora.com" +server: + port: 8080 + +run: + command: go run ./cmd/server + watch: true + watch_tool: air + +test: + cover_threshold: 80 +`, name, description, stacks, selectedDriver.Name, selectedDriver.DSN) + } else { + content = fmt.Sprintf(`name: "%s" +description: "%s" +type: monorepo-backend +stacks: [go, proto] + +build: + main: ./cmd/server + output: ./bin + +gen: + handler: + template: templates/handler.gotpl + output: internal/handler + service: + template: templates/service.gotpl + output: internal/service + +server: + port: 8080 + +run: + command: go run ./cmd/server + watch: true + watch_tool: air + +test: + cover_threshold: 80 +`, name, description) + } + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } + + if selectedType == "frontend-web" { + content := fmt.Sprintf(`name: "%s" +description: "%s" +type: frontend-web +stacks: [react, typescript] + +contract: + design: + - path: tokens + type: figma + api: + - path: spec/openapi.yaml + type: openapi3 +`, name, description) + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } + + if selectedType == "frontend-app" { + content := fmt.Sprintf(`name: "%s" +description: "%s" +type: frontend-app +stacks: [react-native, typescript] + +contract: + api: + - path: spec/openapi.yaml + type: openapi3 +`, name, description) + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } + + if selectedType == "frontend-desktop" { + content := fmt.Sprintf(`name: "%s" +description: "%s" +type: frontend-desktop +stacks: [electron, typescript] + +contract: + api: + - path: spec/openapi.yaml + type: openapi3 +`, name, description) + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } + + if selectedType == "monorepo-frontend" { + content := fmt.Sprintf(`name: "%s" +description: "%s" +type: monorepo-frontend +stacks: [react, typescript, nextjs] + +contract: + design: + - path: tokens + type: figma + api: + - path: spec/openapi.yaml + type: openapi3 +`, name, description) + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } + + if selectedType == "docs" { + content := fmt.Sprintf(`name: "%s" +description: "%s" +type: docs +stacks: [markdown] +`, name, description) + err = os.WriteFile(filename, []byte(content), 0644) + if err != nil { + fmt.Printf("Failed to write %s: %v\n", filename, err) + os.Exit(1) + } + fmt.Printf("%s created successfully!\n", filename) + return + } -# Repository type + // Fallback for unknown types + content := fmt.Sprintf(`name: "%s" +description: "%s" type: %s -` - content := fmt.Sprintf(template, name, description, selectedType) +`, name, description, selectedType) err = os.WriteFile(filename, []byte(content), 0644) if err != nil { fmt.Printf("Failed to write %s: %v\n", filename, err) diff --git a/apps/cli/cmd/rootcmd/scan.go b/apps/cli/cmd/rootcmd/scan.go new file mode 100644 index 0000000..d5ee8ef --- /dev/null +++ b/apps/cli/cmd/rootcmd/scan.go @@ -0,0 +1,82 @@ +package rootcmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/raizora/radas/v4/constants" + "github.com/raizora/radas/v4/internal/scan" +) + +var ScanCmd = &cobra.Command{ + Use: "scan ", + Short: "Security and quality scans (secrets, vuln)", +} + +var ScanSecretsCmd = &cobra.Command{ + Use: "secrets [path]", + Short: "Scan for committed secrets (gitleaks). Emits SARIF 2.1.0 by default.", + Long: `Walks the given path (default: .) and reports any secrets +detected. Default output is SARIF 2.1.0 JSON, suitable for piping +into GitHub Code Scanning via 'radas scan secrets > radas.sarif'. +Use --format=table for a human-readable summary.`, + Run: runScanSecrets, +} + +var ( + scanFormat string + scanStaged bool + scanAll bool + scanConfig string +) + +func init() { + ScanSecretsCmd.Flags().StringVar(&scanFormat, "format", "sarif", "output format: sarif (default) or table") + ScanSecretsCmd.Flags().BoolVar(&scanStaged, "staged", false, "scan only staged files") + ScanSecretsCmd.Flags().BoolVar(&scanAll, "all", false, "scan full git history (slow)") + ScanSecretsCmd.Flags().StringVar(&scanConfig, "config", "", "path to .gitleaks.toml (default: built-in ruleset)") + ScanCmd.AddCommand(ScanSecretsCmd) +} + +func runScanSecrets(cmd *cobra.Command, args []string) { + dir := "." + if len(args) > 0 { + dir = args[0] + } + absDir, err := filepath.Abs(dir) + if err != nil { + scanFail("resolve path: %v", err) + } + + s := scan.NewGitleaksScanner() + findings, scanErr := s.Scan(absDir, scan.ScanOptions{ + Staged: scanStaged, + All: scanAll, + Config: scanConfig, + }) + + switch scanFormat { + case "sarif": + out := scan.ToSARIF(findings, constants.Version) + os.Stdout.Write(out) + case "table": + fmt.Fprint(os.Stdout, scan.ToTable(findings)) + default: + scanFail("unknown --format %q (valid: sarif, table)", scanFormat) + } + + if scanErr != nil { + scanFail("scan error: %v", scanErr) + } + if len(findings) > 0 { + os.Exit(1) + } +} + +func scanFail(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "scan: "+format+"\n", args...) + os.Exit(2) +} diff --git a/apps/cli/cmd/rootcmd/scan_vuln.go b/apps/cli/cmd/rootcmd/scan_vuln.go new file mode 100644 index 0000000..68e25e7 --- /dev/null +++ b/apps/cli/cmd/rootcmd/scan_vuln.go @@ -0,0 +1,118 @@ +package rootcmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/raizora/radas/v4/internal/scan" + "github.com/raizora/radas/v4/internal/utils" +) + +var scanVulnFormat string + +var ScanVulnCmd = &cobra.Command{ + Use: "vuln [path]", + Short: "Scan for dependency vulnerabilities (govulncheck + pnpm/npm audit)", + Long: `Runs vulnerability scanners on the project at the given path (default: .). + +Detects which scanners to use based on project files: + - go.mod → govulncheck + - pnpm-lock → pnpm audit + - package-lock → npm audit + +Output formats: table (default) or json.`, + Run: runScanVuln, +} + +func init() { + ScanVulnCmd.Flags().StringVar(&scanVulnFormat, "format", "table", "output format: table (default) or json") + ScanCmd.AddCommand(ScanVulnCmd) +} + +func runScanVuln(cmd *cobra.Command, args []string) { + dir := "." + if len(args) > 0 { + dir = args[0] + } + absDir, err := filepath.Abs(dir) + if err != nil { + vulnFail("resolve path: %v", err) + } + + var results []*scan.VulnResult + + // Go vulnerabilities + r := scan.RunGovulncheck(absDir) + results = append(results, r) + + // JS/TS vulnerabilities + r2 := scan.RunPnpmAudit(absDir) + results = append(results, r2) + + // Fallback: if pnpm wasn't used, try npm + if strings.Contains(r2.Summary, "skipped") { + r3 := scan.RunNpmAudit(absDir) + if !strings.Contains(r3.Summary, "skipped") { + results = append(results, r3) + } + } + + switch scanVulnFormat { + case "json": + out, _ := json.MarshalIndent(results, "", " ") + os.Stdout.Write(out) + os.Stdout.Write([]byte{'\n'}) + case "table": + printVulnTable(results) + default: + vulnFail("unknown --format %q (valid: table, json)", scanVulnFormat) + } + + // Exit 1 if any scan found issues + failed := false + for _, r := range results { + if !r.Pass { + failed = true + } + } + if failed { + os.Exit(1) + } +} + +func printVulnTable(results []*scan.VulnResult) { + rows := make([][]string, 0, len(results)) + for _, r := range results { + status := colorStatus(r.Pass) + rows = append(rows, []string{r.Tool, status, r.Summary}) + } + fmt.Fprintln(os.Stdout, "Vulnerability Scan Results") + fmt.Fprintln(os.Stdout, "==========================") + utils.PrintTableTo(os.Stdout, []string{"Tool", "Status", "Summary"}, rows) + + for _, r := range results { + if r.Output != "" && !r.Pass { + fmt.Fprintf(os.Stdout, "\n--- %s output ---\n", r.Tool) + os.Stdout.Write([]byte(r.Output)) + os.Stdout.Write([]byte{'\n'}) + } + } +} + +func colorStatus(pass bool) string { + if pass { + return color.New(color.FgGreen).Sprint("✓") + } + return color.New(color.FgRed).Sprint("✘") +} + +func vulnFail(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "scan vuln: "+format+"\n", args...) + os.Exit(2) +} diff --git a/apps/cli/constants/version.go b/apps/cli/constants/version.go index dc4b5e1..32da910 100644 --- a/apps/cli/constants/version.go +++ b/apps/cli/constants/version.go @@ -3,7 +3,7 @@ package constants // Version information const ( // Version is the current version of the application - Version = "4.0.0" + Version = "4.1.0" // VersionCheckURL is the URL to check for new versions // This should point to a released version JSON file on GitHub diff --git a/apps/cli/go.mod b/apps/cli/go.mod index 27a8b05..b977d12 100644 --- a/apps/cli/go.mod +++ b/apps/cli/go.mod @@ -16,37 +16,83 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/BobuSumisu/aho-corasick v1.0.3 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect + github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/charmbracelet/lipgloss v0.5.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/fatih/semgroup v1.2.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/gitleaks/go-gitdiff v0.9.1 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/swag/jsonname v0.26.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-querystring v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/h2non/filetype v1.1.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mholt/archives v0.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/oasdiff/yaml v0.1.0 // indirect github.com/oasdiff/yaml3 v0.0.13 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect github.com/ulikunitz/xz v0.5.15 // indirect + github.com/wasilibs/go-re2 v1.9.0 // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect + github.com/zricethezav/gitleaks/v8 v8.30.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/crypto v0.53.0 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect diff --git a/apps/cli/go.sum b/apps/cli/go.sum index 935babd..40539f1 100644 --- a/apps/cli/go.sum +++ b/apps/cli/go.sum @@ -1,23 +1,80 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= +github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/fatih/semgroup v1.2.0 h1:h/OLXwEM+3NNyAdZEpMiH1OzfplU09i2qXPVThGZvyg= +github.com/fatih/semgroup v1.2.0/go.mod h1:1KAD4iIYfXjE4U13B48VM4z9QUwV5Tt8O4rS879kgm8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -25,6 +82,10 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g= github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g= +github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0= +github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= @@ -33,8 +94,26 @@ github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0 github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -43,17 +122,51 @@ github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQF github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ= github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -61,17 +174,40 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A= +github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg= github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0= github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg= @@ -81,17 +217,33 @@ github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -104,78 +256,241 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/wasilibs/go-re2 v1.9.0 h1:kjAd8qbNvV4Ve2Uf+zrpTCrDHtqH4dlsRXktywo73JQ= +github.com/wasilibs/go-re2 v1.9.0/go.mod h1:0sRtscWgpUdNA137bmr1IUgrRX0Su4dcn9AEe61y+yI= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zricethezav/gitleaks/v8 v8.30.1 h1:PmEvCfVI7ti9dV3s5aMZUY7sS2GxRvG3yzih7E+cS3w= +github.com/zricethezav/gitleaks/v8 v8.30.1/go.mod h1:rTDwxRjufMKAkhTI/Mijd07nday1yOhf9qywjwz5Irw= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/apps/cli/internal/checker/dbdetect.go b/apps/cli/internal/checker/dbdetect.go new file mode 100644 index 0000000..3c59a5c --- /dev/null +++ b/apps/cli/internal/checker/dbdetect.go @@ -0,0 +1,207 @@ +package checker + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/raizora/radas/v4/internal/utils" +) + +// DetectDatabase scans the project for known database dependencies +// and returns the detected database driver name (e.g. "postgres", "mysql", "sqlite"). +// Supported stacks: Go (go.mod), PHP/Laravel (composer.json), Elixir (mix.exs). +func DetectDatabase(dir string) string { + // 1. Check for platform-level configs first (supabase, turso CLI) + if d := detectPlatformDB(dir); d != "" { + return d + } + + // 2. Check dependency files + if f, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !f.IsDir() { + return detectGoDatabase(dir) + } + if f, err := os.Stat(filepath.Join(dir, "composer.json")); err == nil && !f.IsDir() { + return detectPhpDatabase(dir) + } + if f, err := os.Stat(filepath.Join(dir, "mix.exs")); err == nil && !f.IsDir() { + return detectElixirDatabase(dir) + } + return "" +} + +// detectPlatformDB checks for platform-level config files (supabase, turso CLI). +func detectPlatformDB(dir string) string { + // Supabase: project with supabase/ dir and config.toml + if f, err := os.Stat(filepath.Join(dir, "supabase", "config.toml")); err == nil && !f.IsDir() { + return "supabase (postgres)" + } + // Turso: config file in project root + if f, err := os.Stat(filepath.Join(dir, "turso.json")); err == nil && !f.IsDir() { + return "turso (sqlite)" + } + if f, err := os.Stat(filepath.Join(dir, ".turso")); err == nil && f.IsDir() { + return "turso (sqlite)" + } + return "" +} + +var goDBDrivers = []struct { + path string + name string + weight int +}{ + // Supabase (platform — underlying DB is postgres) + {"github.com/supabase-community/supabase-go", "supabase (postgres)", 4}, + {"github.com/supabase-community/postgrest-go", "supabase (postgres)", 3}, + {"github.com/supabase/supabase-go", "supabase (postgres)", 3}, + + // Turso / libsql (edge SQLite) + {"github.com/tursodatabase/libsql-client-go", "turso (sqlite)", 4}, + {"github.com/libsql/libsql-client-go", "turso (sqlite)", 3}, + + // PostgreSQL + {"github.com/jackc/pgx/", "postgres", 3}, + {"github.com/jackc/pgx", "postgres", 2}, + {"github.com/lib/pq", "postgres", 2}, + {"github.com/go-pg/pg", "postgres", 1}, + + // MySQL + {"github.com/go-sql-driver/mysql", "mysql", 2}, + {"github.com/go-gorm/mysql", "mysql", 1}, + + // SQLite + {"github.com/mattn/go-sqlite3", "sqlite", 2}, + {"github.com/glebarez/go-sqlite", "sqlite", 1}, + + // MSSQL + {"github.com/denisenkom/go-mssqldb", "mssql", 2}, + {"github.com/microsoft/go-mssqldb", "mssql", 2}, + + // NoSQL / others + {"go.mongodb.org/mongo-driver", "mongodb", 2}, + {"github.com/redis/go-redis", "redis", 1}, + {"github.com/gocql/gocql", "cassandra", 1}, + {"github.com/ClickHouse/clickhouse-go", "clickhouse", 1}, + {"github.com/tarantool/go-tarantool", "tarantool", 1}, +} + +func detectGoDatabase(dir string) string { + f, err := os.Open(filepath.Join(dir, "go.mod")) + if err != nil { + return "" + } + defer f.Close() + + type candidate struct { + name string + weight int + } + var best candidate + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + for _, d := range goDBDrivers { + if strings.Contains(line, d.path) && d.weight > best.weight { + best = candidate{name: d.name, weight: d.weight} + } + } + } + return best.name +} + +var phpDBDrivers = []struct { + path string + name string +}{ + {"doctrine/dbal", "postgres/mysql"}, + {"laravel/database", "postgres/mysql"}, + {"mongodb/mongodb", "mongodb"}, + {"illuminate/database", "postgres/mysql"}, +} + +func detectPhpDatabase(dir string) string { + data, err := os.ReadFile(filepath.Join(dir, "composer.json")) + if err != nil { + return "" + } + var composer struct { + Require map[string]string `json:"require"` + } + if err := json.Unmarshal(data, &composer); err != nil { + return "" + } + for _, d := range phpDBDrivers { + if _, ok := composer.Require[d.path]; ok { + return d.name + } + } + return "" +} + +func detectElixirDatabase(dir string) string { + data, err := os.ReadFile(filepath.Join(dir, "mix.exs")) + if err != nil { + return "" + } + content := string(data) + drivers := map[string]string{ + "ecto_sql": "postgres/mysql", + "mongodb": "mongodb", + "mongo": "mongodb", + "redix": "redis", + } + for dep, db := range drivers { + if strings.Contains(content, dep) { + return db + } + } + return "" +} + +// SuggestDSN returns a DSN hint for the given database driver. +// envPrefix is the environment variable prefix (e.g. "DB", "DATABASE"). +func SuggestDSN(driver string) string { + switch driver { + case "postgres", "supabase (postgres)": + return "postgres://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres" + case "mysql": + return "user:pass@tcp(localhost:3306)/dbname?parseTime=true" + case "sqlite": + return "./data.db" + case "turso (sqlite)": + return "libsql://[DB-NAME]-[ORG].turso.io?authToken=[TOKEN]" + case "mongodb": + return "mongodb://localhost:27017/dbname" + case "redis": + return "redis://localhost:6379/0" + case "mssql": + return "sqlserver://user:pass@localhost:1433?database=dbname" + case "clickhouse": + return "clickhouse://localhost:9000/dbname" + case "cassandra": + return "cassandra://localhost:9042/dbname" + default: + return "" + } +} + +// PrintDatabaseResult prints the detected database info to stdout (doctor-style). +func PrintDatabaseResult(dir string) { + db := DetectDatabase(dir) + fmt.Print("Detecting database: ") + if db == "" { + utils.Warning("⚠ Could not detect database driver\n") + fmt.Println(" (no known driver found in go.mod / composer.json / mix.exs)") + return + } + utils.Success("✓ %s\n", db) + dsn := SuggestDSN(db) + if dsn != "" { + fmt.Printf(" Default DSN: %s\n", dsn) + } +} diff --git a/apps/cli/internal/checker/dbdetect_test.go b/apps/cli/internal/checker/dbdetect_test.go new file mode 100644 index 0000000..d4b84ff --- /dev/null +++ b/apps/cli/internal/checker/dbdetect_test.go @@ -0,0 +1,171 @@ +package checker + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDetectDatabase(t *testing.T) { + t.Run("GoPostgres", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test + +go 1.25 + +require ( + github.com/jackc/pgx/v5 v5.7.0 +) +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "postgres" { + t.Errorf("DetectDatabase = %q, want postgres", got) + } + }) + + t.Run("GoMySQL", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test + +go 1.25 + +require ( + github.com/go-sql-driver/mysql v1.8.0 +) +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "mysql" { + t.Errorf("DetectDatabase = %q, want mysql", got) + } + }) + + t.Run("GoSQLite", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test + +go 1.25 + +require github.com/mattn/go-sqlite3 v1.14.22 +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "sqlite" { + t.Errorf("DetectDatabase = %q, want sqlite", got) + } + }) + + t.Run("GoMongoDB", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test + +go 1.25 + +require go.mongodb.org/mongo-driver v1.14.0 +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "mongodb" { + t.Errorf("DetectDatabase = %q, want mongodb", got) + } + }) + + t.Run("NoGoMod", func(t *testing.T) { + dir := t.TempDir() + if got := DetectDatabase(dir); got != "" { + t.Errorf("DetectDatabase = %q, want empty", got) + } + }) + + t.Run("GoModNoDB", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test + +go 1.25 + +require github.com/spf13/cobra v1.10.0 +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "" { + t.Errorf("DetectDatabase = %q, want empty", got) + } + }) + + t.Run("PhpLaravel", func(t *testing.T) { + dir := t.TempDir() + composer := `{ + "require": { + "laravel/framework": "^11.0", + "laravel/database": "^11.0" + } + }` + os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composer), 0644) + if got := DetectDatabase(dir); got != "postgres/mysql" { + t.Errorf("DetectDatabase = %q, want postgres/mysql", got) + } + }) + + t.Run("ElixirEcto", func(t *testing.T) { + dir := t.TempDir() + mix := `defmodule MyApp.MixProject do + use Mix.Project + def deps do + [{:ecto_sql, "~> 3.11"}] + end +end` + os.WriteFile(filepath.Join(dir, "mix.exs"), []byte(mix), 0644) + if got := DetectDatabase(dir); got != "postgres/mysql" { + t.Errorf("DetectDatabase = %q, want postgres/mysql", got) + } + }) + + t.Run("GoSupabaseClient", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test +go 1.25 +require github.com/supabase-community/supabase-go v0.1.0 +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "supabase (postgres)" { + t.Errorf("DetectDatabase = %q, want supabase (postgres)", got) + } + }) + + t.Run("SupabaseConfigDir", func(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(filepath.Join(dir, "supabase"), 0755) + os.WriteFile(filepath.Join(dir, "supabase", "config.toml"), []byte("[api]"), 0644) + if got := DetectDatabase(dir); got != "supabase (postgres)" { + t.Errorf("DetectDatabase = %q, want supabase (postgres)", got) + } + }) + + t.Run("GoTursoLibsql", func(t *testing.T) { + dir := t.TempDir() + gomod := `module test +go 1.25 +require github.com/tursodatabase/libsql-client-go v0.1.0 +` + os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644) + if got := DetectDatabase(dir); got != "turso (sqlite)" { + t.Errorf("DetectDatabase = %q, want turso (sqlite)", got) + } + }) +} + +func TestSuggestDSN(t *testing.T) { + tests := []struct { + driver string + want string + }{ + {"postgres", "postgres://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"}, + {"supabase (postgres)", "postgres://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"}, + {"mysql", "user:pass@tcp(localhost:3306)/dbname?parseTime=true"}, + {"sqlite", "./data.db"}, + {"turso (sqlite)", "libsql://[DB-NAME]-[ORG].turso.io?authToken=[TOKEN]"}, + {"unknown", ""}, + } + for _, tc := range tests { + got := SuggestDSN(tc.driver) + if got != tc.want { + t.Errorf("SuggestDSN(%q) = %q, want %q", tc.driver, got, tc.want) + } + } +} diff --git a/apps/cli/internal/config/parser.go b/apps/cli/internal/config/parser.go index e9c72b6..9a65564 100644 --- a/apps/cli/internal/config/parser.go +++ b/apps/cli/internal/config/parser.go @@ -12,29 +12,93 @@ import ( "gopkg.in/yaml.v3" ) +// --- shared contract types --------------------------------------------------- + +// ContractSource is a single design-token or API spec input. +type ContractSource struct { + Path string `yaml:"path"` + Type string `yaml:"type"` +} + +// ContractConfig describes the design and API inputs the project consumes. +type ContractConfig struct { + Design []ContractSource `yaml:"design,omitempty"` + API []ContractSource `yaml:"api,omitempty"` +} + +// --- BE-specific types ------------------------------------------------------- + +// BuildConfig controls how the backend binary is built. +type BuildConfig struct { + Main string `yaml:"main,omitempty"` + Output string `yaml:"output,omitempty"` + Ldflags string `yaml:"ldflags,omitempty"` +} + +// DBConfig describes the database connection and migration paths. +type DBConfig struct { + Driver string `yaml:"driver,omitempty"` + Migrations string `yaml:"migrations,omitempty"` + Seeds string `yaml:"seeds,omitempty"` + DefaultDSN string `yaml:"default_dsn,omitempty"` +} + +// GenTemplate is a single code-generator template mapping. +type GenTemplate struct { + Template string `yaml:"template,omitempty"` + Output string `yaml:"output,omitempty"` +} + +// GenConfig groups code-generation template paths. +type GenConfig struct { + Handler *GenTemplate `yaml:"handler,omitempty"` + Service *GenTemplate `yaml:"service,omitempty"` + Model *GenTemplate `yaml:"model,omitempty"` +} + +// RunConfig controls how the dev server is started. +type RunConfig struct { + // Command is the run command (e.g. "go run ./cmd/server"). + Command string `yaml:"command,omitempty"` + // Watch enables hot-reload when true. + Watch bool `yaml:"watch,omitempty"` + // WatchTool is the hot-reload tool ("air", "gow", "reflex", "nodemon"). + WatchTool string `yaml:"watch_tool,omitempty"` +} + +// ServerConfig holds dev-server defaults. +type ServerConfig struct { + Port int `yaml:"port,omitempty"` +} + +// TestConfig controls test-runner behaviour. +type TestConfig struct { + CoverThreshold int `yaml:"cover_threshold,omitempty"` + Flags string `yaml:"flags,omitempty"` +} + // RadasConfig represents the structure of radas.yml. type RadasConfig struct { // Name is the human-readable project name. Name string `yaml:"name"` // Description is a one-line project summary. Description string `yaml:"description"` - // Type is the project archetype (e.g. "be", "fe", "infra"). + // Type is the project archetype (e.g. "backend-api", "frontend-web"). Type string `yaml:"type"` // Stacks lists the technology stacks used (e.g. ["go", "gin"]). Stacks []string `yaml:"stacks"` + // Contract describes the design and API inputs the project consumes. - Contract struct { - // Design lists design-token input files. - Design []struct { - Path string `yaml:"path"` - Type string `yaml:"type"` - } `yaml:"design"` - // API lists OpenAPI input specs. - API []struct { - Path string `yaml:"path"` - Type string `yaml:"type"` - } `yaml:"api"` - } `yaml:"contract"` + Contract ContractConfig `yaml:"contract"` + + // --- BE-specific --------------------------------------------------------- + + Build BuildConfig `yaml:"build,omitempty"` + DB DBConfig `yaml:"db,omitempty"` + Gen GenConfig `yaml:"gen,omitempty"` + Server ServerConfig `yaml:"server,omitempty"` + Test TestConfig `yaml:"test,omitempty"` + Run RunConfig `yaml:"run,omitempty"` } // ParseConfig reads and parses the radas.yml file at configPath. If diff --git a/apps/cli/internal/config/parser_test.go b/apps/cli/internal/config/parser_test.go index e47b83d..ba484a5 100644 --- a/apps/cli/internal/config/parser_test.go +++ b/apps/cli/internal/config/parser_test.go @@ -59,6 +59,116 @@ stacks: [go, gin] }) } +func TestParseConfigBESections(t *testing.T) { + tmpDir, _ := os.MkdirTemp("", "radas-be-*") + defer os.RemoveAll(tmpDir) + + t.Run("BuildConfig", func(t *testing.T) { + content := ` +name: api +type: backend-api +stacks: [go] +build: + main: ./cmd/server + output: ./bin/app +` + cfgPath := filepath.Join(tmpDir, "be-build.yml") + os.WriteFile(cfgPath, []byte(content), 0644) + + cfg, err := ParseConfig(cfgPath) + if err != nil { + t.Fatalf("ParseConfig failed: %v", err) + } + if cfg.Build.Main != "./cmd/server" { + t.Errorf("Build.Main = %q, want ./cmd/server", cfg.Build.Main) + } + if cfg.Build.Output != "./bin/app" { + t.Errorf("Build.Output = %q, want ./bin/app", cfg.Build.Output) + } + }) + + t.Run("DBConfig", func(t *testing.T) { + content := ` +name: api +type: backend-api +stacks: [go] +db: + driver: postgres + migrations: ./migrations + seeds: ./seeds +` + cfgPath := filepath.Join(tmpDir, "be-db.yml") + os.WriteFile(cfgPath, []byte(content), 0644) + + cfg, err := ParseConfig(cfgPath) + if err != nil { + t.Fatalf("ParseConfig failed: %v", err) + } + if cfg.DB.Driver != "postgres" { + t.Errorf("DB.Driver = %q, want postgres", cfg.DB.Driver) + } + if cfg.DB.Migrations != "./migrations" { + t.Errorf("DB.Migrations = %q, want ./migrations", cfg.DB.Migrations) + } + }) + + t.Run("RunConfig", func(t *testing.T) { + content := ` +name: api +type: backend-api +stacks: [go] +run: + command: go run ./cmd/server + watch: true + watch_tool: air +` + cfgPath := filepath.Join(tmpDir, "be-run.yml") + os.WriteFile(cfgPath, []byte(content), 0644) + + cfg, err := ParseConfig(cfgPath) + if err != nil { + t.Fatalf("ParseConfig failed: %v", err) + } + if cfg.Run.Command != "go run ./cmd/server" { + t.Errorf("Run.Command = %q, want go run ./cmd/server", cfg.Run.Command) + } + if !cfg.Run.Watch { + t.Error("Run.Watch should be true") + } + if cfg.Run.WatchTool != "air" { + t.Errorf("Run.WatchTool = %q, want air", cfg.Run.WatchTool) + } + }) + + t.Run("GenConfig", func(t *testing.T) { + content := ` +name: api +type: backend-api +stacks: [go] +gen: + handler: + template: templates/handler.gotpl + output: internal/handler + service: + template: templates/service.gotpl + output: internal/service +` + cfgPath := filepath.Join(tmpDir, "be-gen.yml") + os.WriteFile(cfgPath, []byte(content), 0644) + + cfg, err := ParseConfig(cfgPath) + if err != nil { + t.Fatalf("ParseConfig failed: %v", err) + } + if cfg.Gen.Handler == nil || cfg.Gen.Handler.Output != "internal/handler" { + t.Errorf("Gen.Handler.Output = %v, want internal/handler", cfg.Gen.Handler) + } + if cfg.Gen.Service == nil || cfg.Gen.Service.Template != "templates/service.gotpl" { + t.Errorf("Gen.Service.Template = %v, want templates/service.gotpl", cfg.Gen.Service) + } + }) +} + func TestFindConfig(t *testing.T) { tmpDir, _ := os.MkdirTemp("", "radas-find-*") tmpDir, _ = filepath.EvalSymlinks(tmpDir) // Normalize for macOS diff --git a/apps/cli/internal/ignore/degit.go b/apps/cli/internal/ignore/degit.go new file mode 100644 index 0000000..26140ae --- /dev/null +++ b/apps/cli/internal/ignore/degit.go @@ -0,0 +1,39 @@ +package ignore + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +var errDegitMissing = errors.New("degit not found in PATH") + +// fetchViaDegit is a package-level variable, overridden in tests. +// Returns the resolved degit binary path (or errDegitMissing if +// not found). Implementations must populate dest with the cloned +// template tree. +var fetchViaDegit = fetchViaDegitImpl + +// fetchViaDegitImpl is the real implementation, used in production. +func fetchViaDegitImpl(repo, dest string) (string, error) { + bin, err := exec.LookPath("degit") + if err != nil { + return "", fmt.Errorf("%w (install with: npm install -g degit)", errDegitMissing) + } + dest = filepath.Clean(dest) + if err := os.RemoveAll(dest); err != nil { + return "", fmt.Errorf("clear dest: %w", err) + } + if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { + return "", fmt.Errorf("mkdir parent: %w", err) + } + cmd := exec.Command(bin, repo, dest) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return "", err + } + return bin, nil +} diff --git a/apps/cli/internal/ignore/degit_test.go b/apps/cli/internal/ignore/degit_test.go new file mode 100644 index 0000000..8e185d9 --- /dev/null +++ b/apps/cli/internal/ignore/degit_test.go @@ -0,0 +1,17 @@ +package ignore + +import ( + "errors" + "testing" +) + +func TestDegitNotFound(t *testing.T) { + t.Setenv("PATH", "/nonexistent") + _, err := fetchViaDegit("github.com/foo/bar", "/tmp/dest") + if err == nil { + t.Fatal("expected error when degit not in PATH") + } + if !errors.Is(err, errDegitMissing) { + t.Fatalf("expected errDegitMissing, got %v", err) + } +} diff --git a/apps/cli/internal/ignore/merge.go b/apps/cli/internal/ignore/merge.go new file mode 100644 index 0000000..da96d00 --- /dev/null +++ b/apps/cli/internal/ignore/merge.go @@ -0,0 +1,88 @@ +// Package ignore generates .gitignore (and related) files for the +// fe/be/infra teams. Templates are fetched from a degit repo and +// merged with any existing file so user customizations are preserved. +package ignore + +import ( + "fmt" + "os" + "strings" +) + +const managedHeader = "# radas-managed: do not remove this header.\n" + + "# Re-running `radas ignore` is safe; it preserves your\n" + + "# manual additions below.\n" + +func MergePatterns(existing, template string, force bool) (string, error) { + if force { + return managedHeader + template, nil + } + + if strings.TrimSpace(existing) == "" { + if strings.TrimSpace(template) == "" { + return "", nil + } + return managedHeader + template, nil + } + if strings.TrimSpace(template) == "" { + return existing, nil + } + + existingSet := lineSet(existing) + newLines := []string{} + for _, line := range strings.Split(template, "\n") { + if line == "" { + continue + } + if _, ok := existingSet[line]; !ok { + newLines = append(newLines, line) + existingSet[line] = struct{}{} + } + } + + if len(newLines) == 0 { + return existing, nil + } + + var b strings.Builder + b.WriteString(existing) + if !strings.HasSuffix(existing, "\n") { + b.WriteString("\n") + } + b.WriteString("\n# Added by radas:\n") + for _, l := range newLines { + b.WriteString(l) + b.WriteString("\n") + } + return b.String(), nil +} + +func lineSet(s string) map[string]struct{} { + out := map[string]struct{}{} + for _, l := range strings.Split(s, "\n") { + if l == "" { + continue + } + out[l] = struct{}{} + } + return out +} + +func IsBinary(path string) (bool, error) { + f, err := os.Open(path) + if err != nil { + return false, fmt.Errorf("open: %w", err) + } + defer f.Close() + buf := make([]byte, 8192) + n, err := f.Read(buf) + if err != nil { + return false, fmt.Errorf("read: %w", err) + } + for i := 0; i < n; i++ { + if buf[i] == 0 { + return true, nil + } + } + return false, nil +} diff --git a/apps/cli/internal/ignore/merge_test.go b/apps/cli/internal/ignore/merge_test.go new file mode 100644 index 0000000..df3f05c --- /dev/null +++ b/apps/cli/internal/ignore/merge_test.go @@ -0,0 +1,135 @@ +package ignore + +import ( + "strings" + "testing" +) + +func TestMergePatterns_EmptyBoth(t *testing.T) { + got, err := MergePatterns("", "", false) + if err != nil { + t.Fatalf("err: %v", err) + } + if got != "" { + t.Errorf("expected empty, got %q", got) + } +} + +func TestMergePatterns_EmptyExisting(t *testing.T) { + template := "# header\nnode_modules/\n" + got, err := MergePatterns("", template, false) + if err != nil { + t.Fatalf("err: %v", err) + } + if !strings.Contains(got, "radas-managed") { + t.Error("expected radas-managed header on first write") + } + if !strings.Contains(got, "node_modules/") { + t.Error("expected template content") + } +} + +func TestMergePatterns_EmptyTemplate(t *testing.T) { + existing := "node_modules/\n*.log\n" + got, err := MergePatterns(existing, "", false) + if err != nil { + t.Fatalf("err: %v", err) + } + if got != existing { + t.Errorf("expected existing unchanged, got %q", got) + } +} + +func TestMergePatterns_NoOverlap(t *testing.T) { + existing := "node_modules/\n" + template := "dist/\n" + got, err := MergePatterns(existing, template, false) + if err != nil { + t.Fatalf("err: %v", err) + } + if !strings.Contains(got, "node_modules/") { + t.Error("existing missing") + } + if !strings.Contains(got, "dist/") { + t.Error("template missing") + } + if strings.Index(got, "node_modules/") > strings.Index(got, "dist/") { + t.Error("expected existing before template in output") + } +} + +func TestMergePatterns_FullOverlap(t *testing.T) { + existing := "node_modules/\ndist/\n" + template := "node_modules/\ndist/\n" + got, err := MergePatterns(existing, template, false) + if err != nil { + t.Fatalf("err: %v", err) + } + if got != existing { + t.Errorf("expected unchanged, got %q", got) + } +} + +func TestMergePatterns_PartialOverlap(t *testing.T) { + existing := "node_modules/\n*.log\n" + template := "node_modules/\ndist/\n*.log\n" + got, err := MergePatterns(existing, template, false) + if err != nil { + t.Fatalf("err: %v", err) + } + if !strings.Contains(got, "node_modules/") { + t.Error("missing node_modules/") + } + if !strings.Contains(got, "dist/") { + t.Error("missing dist/") + } + if strings.Index(got, "dist/") < strings.Index(got, "*.log") { + t.Error("dist/ should be appended after existing lines") + } + if strings.Count(got, "node_modules/") > 1 { + t.Errorf("node_modules/ appears %d times, expected 1", strings.Count(got, "node_modules/")) + } +} + +func TestMergePatterns_UserCustomPreserved(t *testing.T) { + existing := "node_modules/\n# My custom: don't ignore .env.example\n.env.example\n" + template := "node_modules/\ndist/\n" + got, err := MergePatterns(existing, template, false) + if err != nil { + t.Fatalf("err: %v", err) + } + if !strings.Contains(got, ".env.example") { + t.Error("user custom line '.env.example' was lost") + } + if !strings.Contains(got, "My custom") { + t.Error("user comment was lost") + } +} + +func TestMergePatterns_Force(t *testing.T) { + existing := "node_modules/\n# user custom\n" + template := "dist/\n" + got, err := MergePatterns(existing, template, true) + if err != nil { + t.Fatalf("err: %v", err) + } + if strings.Contains(got, "user custom") { + t.Error("force=true should drop user customizations") + } + if !strings.Contains(got, "dist/") { + t.Error("force should write template") + } + if strings.Contains(got, "node_modules/") { + t.Error("force should drop existing (template had no node_modules/)") + } +} + +func TestMergePatterns_Idempotent(t *testing.T) { + existing := "node_modules/\n" + template := "node_modules/\ndist/\n" + first, _ := MergePatterns(existing, template, false) + second, _ := MergePatterns(first, template, false) + if first != second { + t.Errorf("not idempotent:\nfirst: %q\nsecond: %q", first, second) + } +} diff --git a/apps/cli/internal/ignore/template.go b/apps/cli/internal/ignore/template.go new file mode 100644 index 0000000..a58350d --- /dev/null +++ b/apps/cli/internal/ignore/template.go @@ -0,0 +1,89 @@ +package ignore + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +const templateRepo = "github.com/raizora/radas-templates/ignore" + +var ( + teamFe = teamSpec{ + files: map[string][]string{ + ".gitignore": {"gitignore/.gitignore"}, + ".biomeignore": {"biomeignore/default.biomeignore"}, + ".prettierignore": {"prettierignore/default.prettierignore"}, + }, + stacks: []string{"nextjs", "vite", "remix"}, + defaultStack: "nextjs", + } + teamBe = teamSpec{ + files: map[string][]string{ + ".gitignore": {"gitignore/default.gitignore"}, + }, + stacks: []string{"default"}, + defaultStack: "default", + } + teamInfra = teamSpec{ + files: map[string][]string{ + ".gitignore": {"gitignore/.gitignore"}, + ".dockerignore": {"dockerignore/default.dockerignore"}, + }, + stacks: []string{"docker", "terraform", "k8s"}, + defaultStack: "docker", + } + + teams = map[string]teamSpec{ + "fe": teamFe, + "be": teamBe, + "infra": teamInfra, + } +) + +type teamSpec struct { + files map[string][]string + stacks []string + defaultStack string +} + +func Fetch(team, stack, destDir string) (map[string]string, error) { + spec, ok := teams[team] + if !ok { + return nil, fmt.Errorf("unknown team %q (valid: fe, be, infra)", team) + } + if stack == "" { + stack = spec.defaultStack + } + if !containsString(spec.stacks, stack) { + return nil, fmt.Errorf("unknown stack %q for team %q (valid: %s)", + stack, team, strings.Join(spec.stacks, ", ")) + } + + if _, err := fetchViaDegit(templateRepo, destDir); err != nil { + return nil, fmt.Errorf("fetch templates: %w", err) + } + + results := map[string]string{} + for outName, patterns := range spec.files { + rel := patterns[0] + rel = strings.ReplaceAll(rel, "", stack) + src := filepath.Join(destDir, "ignore", team, rel) + data, err := os.ReadFile(src) + if err != nil { + return nil, fmt.Errorf("read %s: %w", src, err) + } + results[outName] = string(data) + } + return results, nil +} + +func containsString(s []string, v string) bool { + for _, x := range s { + if x == v { + return true + } + } + return false +} diff --git a/apps/cli/internal/ignore/template_test.go b/apps/cli/internal/ignore/template_test.go new file mode 100644 index 0000000..6d4eac6 --- /dev/null +++ b/apps/cli/internal/ignore/template_test.go @@ -0,0 +1,133 @@ +package ignore + +import ( + "errors" + "os" + "path/filepath" + "testing" +) + +func TestFetch_TeamFE_DefaultStack(t *testing.T) { + dir := t.TempDir() + plantFakeTemplate(t, dir, "fe", map[string]string{ + "gitignore/nextjs.gitignore": "node_modules/\n.next/\n", + "biomeignore/default.biomeignore": "**/dist/\n", + "prettierignore/default.prettierignore": ".cache/\n", + }) + + prev := fetchViaDegit + fetchViaDegit = func(repo, dest string) (string, error) { + return "", copyTree(dir, repo, dest) + } + defer func() { fetchViaDegit = prev }() + + results, err := Fetch("fe", "nextjs", "/tmp/dest") + if err != nil { + t.Fatalf("err: %v", err) + } + if len(results) != 3 { + t.Errorf("expected 3 files, got %d: %+v", len(results), results) + } + if !containsKey(results, ".gitignore") { + t.Error("missing .gitignore") + } + if !containsKey(results, ".biomeignore") { + t.Error("missing .biomeignore") + } + if !containsKey(results, ".prettierignore") { + t.Error("missing .prettierignore") + } +} + +func TestFetch_TeamBE_Default(t *testing.T) { + dir := t.TempDir() + plantFakeTemplate(t, dir, "be", map[string]string{ + "gitignore/default.gitignore": "radas\n", + }) + + prev := fetchViaDegit + fetchViaDegit = func(repo, dest string) (string, error) { return "", copyTree(dir, repo, dest) } + defer func() { fetchViaDegit = prev }() + + results, err := Fetch("be", "default", "/tmp/dest") + if err != nil { + t.Fatalf("err: %v", err) + } + if len(results) != 1 || !containsKey(results, ".gitignore") { + t.Errorf("expected 1 file (.gitignore), got %+v", results) + } +} + +func TestFetch_UnknownTeam(t *testing.T) { + _, err := Fetch("unknown-team", "default", "/tmp/dest") + if err == nil { + t.Fatal("expected error for unknown team") + } +} + +func TestFetch_UnknownStack(t *testing.T) { + _, err := Fetch("fe", "no-such-stack", "/tmp/dest") + if err == nil { + t.Fatal("expected error for unknown stack") + } +} + +func TestFetch_DownloadFails(t *testing.T) { + prev := fetchViaDegit + fetchViaDegit = func(repo, dest string) (string, error) { return "", errors.New("network down") } + defer func() { fetchViaDegit = prev }() + + _, err := Fetch("fe", "nextjs", "/tmp/dest") + if err == nil { + t.Fatal("expected error when download fails") + } +} + +func plantFakeTemplate(t *testing.T, root, team string, files map[string]string) { + t.Helper() + for rel, content := range files { + full := filepath.Join(root, "ignore", team, rel) + if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte(content), 0644); err != nil { + t.Fatal(err) + } + } +} + +func copyTree(src, repoUnused, dest string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + if !startsWith(rel, "ignore"+string(filepath.Separator)) { + return nil + } + out := filepath.Join(dest, rel) + if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil { + return err + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(out, data, 0644) + }) +} + +func containsKey(m map[string]string, k string) bool { + _, ok := m[k] + return ok +} + +func startsWith(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix +} diff --git a/apps/cli/internal/scan/gitleaks.go b/apps/cli/internal/scan/gitleaks.go new file mode 100644 index 0000000..bfeba98 --- /dev/null +++ b/apps/cli/internal/scan/gitleaks.go @@ -0,0 +1,96 @@ +package scan + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/zricethezav/gitleaks/v8/detect" +) + +type GitleaksScanner struct{} + +func NewGitleaksScanner() *GitleaksScanner { + return &GitleaksScanner{} +} + +func (s *GitleaksScanner) Scan(dir string, opts ScanOptions) ([]Finding, error) { + d, err := detect.NewDetectorDefaultConfig() + if err != nil { + return nil, fmt.Errorf("init detector: %w", err) + } + d.MaxTargetMegaBytes = 100 + + var findings []Finding + err = filepath.Walk(dir, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.IsDir() { + name := info.Name() + if name == ".git" || name == "node_modules" || name == "vendor" || name == "dist" || name == "build" { + return filepath.SkipDir + } + return nil + } + + if isBinary(path) { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + return nil + } + + frags := d.DetectBytes(data) + for _, f := range frags { + findings = append(findings, Finding{ + File: path, + Line: f.StartLine, + Rule: f.RuleID, + Secret: redact(f.Secret), + Severity: severityFromLevel(f.Entropy), + }) + } + return nil + }) + if err != nil { + return findings, fmt.Errorf("walk: %w", err) + } + return findings, nil +} + +func isBinary(path string) bool { + f, err := os.Open(path) + if err != nil { + return false + } + defer f.Close() + buf := make([]byte, 8192) + n, _ := f.Read(buf) + for i := 0; i < n; i++ { + if buf[i] == 0 { + return true + } + } + return false +} + +func redact(s string) string { + if len(s) <= 4 { + return "***" + } + return s[:4] + "***" +} + +func severityFromLevel(entropy float32) string { + switch { + case entropy >= 4.0: + return "error" + case entropy >= 3.0: + return "warning" + default: + return "note" + } +} diff --git a/apps/cli/internal/scan/gitleaks_test.go b/apps/cli/internal/scan/gitleaks_test.go new file mode 100644 index 0000000..9469f3e --- /dev/null +++ b/apps/cli/internal/scan/gitleaks_test.go @@ -0,0 +1,49 @@ +package scan + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGitleaksScanner_DetectsFakeAWSSecret(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, ".env") + if err := os.WriteFile(envFile, []byte("AWS_ACCESS_KEY_ID=AKIA5X2P7Q4R6S3T2VWZ\n"), 0600); err != nil { + t.Fatal(err) + } + + s := NewGitleaksScanner() + findings, err := s.Scan(dir, ScanOptions{}) + if err != nil { + t.Fatalf("scan: %v", err) + } + if len(findings) == 0 { + t.Fatal("expected at least one finding, got 0") + } + found := false + for _, f := range findings { + if filepath.Base(f.File) == ".env" && f.Rule != "" { + found = true + break + } + } + if !found { + t.Errorf("expected finding for .env, got %+v", findings) + } +} + +func TestGitleaksScanner_NoSecretsEmpty(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("# no secrets here\n"), 0644); err != nil { + t.Fatal(err) + } + s := NewGitleaksScanner() + findings, err := s.Scan(dir, ScanOptions{}) + if err != nil { + t.Fatalf("scan: %v", err) + } + if len(findings) != 0 { + t.Errorf("expected 0 findings, got %d: %+v", len(findings), findings) + } +} diff --git a/apps/cli/internal/scan/sarif.go b/apps/cli/internal/scan/sarif.go new file mode 100644 index 0000000..3f765a5 --- /dev/null +++ b/apps/cli/internal/scan/sarif.go @@ -0,0 +1,97 @@ +package scan + +import "encoding/json" + +func sarifLevel(severity string) string { + switch severity { + case "error": + return "error" + case "warning": + return "warning" + default: + return "note" + } +} + +type sarifReport struct { + Version string `json:"version"` + Schema string `json:"$schema"` + Runs []sarifRun `json:"runs"` +} + +type sarifRun struct { + Tool sarifTool `json:"tool"` + Results []sarifResult `json:"results,omitempty"` +} + +type sarifTool struct { + Driver sarifDriver `json:"driver"` +} + +type sarifDriver struct { + Name string `json:"name"` + Version string `json:"version"` + Info string `json:"informationUri,omitempty"` +} + +type sarifResult struct { + RuleID string `json:"ruleId"` + Level string `json:"level"` + Message sarifMessage `json:"message"` + Locations []sarifLocation `json:"locations"` +} + +type sarifMessage struct { + Text string `json:"text"` +} + +type sarifLocation struct { + PhysicalLocation sarifPhys `json:"physicalLocation"` +} + +type sarifPhys struct { + ArtifactLocation sarifArtifact `json:"artifactLocation"` + Region sarifRegion `json:"region"` +} + +type sarifArtifact struct { + URI string `json:"uri"` +} + +type sarifRegion struct { + StartLine int `json:"startLine"` + SnippetText string `json:"snippet,omitempty"` +} + +func ToSARIF(findings []Finding, toolVersion string) []byte { + rep := sarifReport{ + Version: "2.1.0", + Schema: "https://json.schemastore.org/sarif-2.1.0.json", + Runs: []sarifRun{{ + Tool: sarifTool{ + Driver: sarifDriver{ + Name: "radas", + Version: toolVersion, + Info: "https://github.com/raizora/radas", + }, + }, + }}, + } + for _, f := range findings { + rep.Runs[0].Results = append(rep.Runs[0].Results, sarifResult{ + RuleID: f.Rule, + Level: sarifLevel(f.Severity), + Message: sarifMessage{ + Text: "secret detected: " + f.Rule, + }, + Locations: []sarifLocation{{ + PhysicalLocation: sarifPhys{ + ArtifactLocation: sarifArtifact{URI: f.File}, + Region: sarifRegion{StartLine: f.Line, SnippetText: f.Secret}, + }, + }}, + }) + } + out, _ := json.MarshalIndent(rep, "", " ") + return out +} diff --git a/apps/cli/internal/scan/sarif_test.go b/apps/cli/internal/scan/sarif_test.go new file mode 100644 index 0000000..c0fcce6 --- /dev/null +++ b/apps/cli/internal/scan/sarif_test.go @@ -0,0 +1,80 @@ +package scan + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestToSARIF_EmptyFindings(t *testing.T) { + out := ToSARIF(nil, "1.0.0") + if !strings.Contains(string(out), `"version": "2.1.0"`) { + t.Errorf("missing SARIF version: %s", out) + } + if !strings.Contains(string(out), `"name": "radas"`) { + t.Errorf("missing tool name: %s", out) + } + if strings.Contains(string(out), `"results"`) { + t.Errorf("expected no results key when findings empty, got: %s", out) + } +} + +func TestToSARIF_SingleFinding(t *testing.T) { + findings := []Finding{{ + File: "/repo/.env", + Line: 1, + Rule: "aws-access-token", + Secret: "AKIA***", + Severity: "error", + }} + out := ToSARIF(findings, "1.2.3") + s := string(out) + + if !strings.Contains(s, `"version": "2.1.0"`) { + t.Errorf("missing SARIF version") + } + if !strings.Contains(s, `"name": "radas"`) { + t.Error("missing tool name") + } + if !strings.Contains(s, `"version": "1.2.3"`) { + t.Error("missing tool version") + } + if !strings.Contains(s, `"ruleId": "aws-access-token"`) { + t.Error("missing ruleId") + } + if !strings.Contains(s, `"level": "error"`) { + t.Error("missing level") + } + if !strings.Contains(s, `"uri": "/repo/.env"`) { + t.Error("missing artifactLocation uri") + } + if !strings.Contains(s, `"startLine": 1`) { + t.Error("missing startLine") + } + if !strings.Contains(s, `"AKIA***"`) { + t.Error("missing redacted secret snippet") + } + + var any interface{} + if err := json.Unmarshal(out, &any); err != nil { + t.Errorf("output is not valid JSON: %v\n%s", err, out) + } +} + +func TestToSARIF_LevelMapping(t *testing.T) { + cases := []struct { + severity string + want string + }{ + {"error", "error"}, + {"warning", "warning"}, + {"note", "note"}, + {"unknown", "note"}, + } + for _, c := range cases { + got := sarifLevel(c.severity) + if got != c.want { + t.Errorf("sarifLevel(%q) = %q, want %q", c.severity, got, c.want) + } + } +} diff --git a/apps/cli/internal/scan/table.go b/apps/cli/internal/scan/table.go new file mode 100644 index 0000000..1b28be1 --- /dev/null +++ b/apps/cli/internal/scan/table.go @@ -0,0 +1,21 @@ +package scan + +import ( + "fmt" + "strings" + + "github.com/raizora/radas/v4/internal/utils" +) + +func ToTable(findings []Finding) string { + if len(findings) == 0 { + return "✓ no secrets found." + } + rows := make([][]string, 0, len(findings)) + for _, f := range findings { + rows = append(rows, []string{f.File, fmt.Sprintf("%d", f.Line), f.Rule, f.Secret}) + } + var sb strings.Builder + utils.PrintTableTo(&sb, []string{"File", "Line", "Rule", "Secret"}, rows) + return sb.String() +} diff --git a/apps/cli/internal/scan/table_test.go b/apps/cli/internal/scan/table_test.go new file mode 100644 index 0000000..97e2818 --- /dev/null +++ b/apps/cli/internal/scan/table_test.go @@ -0,0 +1,28 @@ +package scan + +import ( + "strings" + "testing" +) + +func TestToTable_Empty(t *testing.T) { + out := ToTable(nil) + if !strings.Contains(out, "no secrets") { + t.Errorf("expected empty-state message, got: %q", out) + } +} + +func TestToTable_OneFinding(t *testing.T) { + findings := []Finding{{ + File: "/r/.env", + Line: 3, + Rule: "aws-access-token", + Secret: "AKIA***", + }} + out := ToTable(findings) + for _, want := range []string{"/r/.env", "3", "aws-access-token", "AKIA***"} { + if !strings.Contains(out, want) { + t.Errorf("missing %q in:\n%s", want, out) + } + } +} diff --git a/apps/cli/internal/scan/types.go b/apps/cli/internal/scan/types.go new file mode 100644 index 0000000..a811b90 --- /dev/null +++ b/apps/cli/internal/scan/types.go @@ -0,0 +1,25 @@ +// Package scan provides security scanning for radas, currently +// focused on secret detection via the gitleaks library. +package scan + +// Finding is a single secret detected during a scan. +type Finding struct { + File string + Line int + Rule string + Secret string // redacted by gitleaks + Severity string // "error" | "warning" | "note" +} + +// ScanOptions controls scan scope. +type ScanOptions struct { + Staged bool // only scan staged files + All bool // scan full git history + Config string // path to .gitleaks.toml; "" for default +} + +// Scanner is the contract for any secrets scanner. Concrete impl +// in gitleaks.go wraps the upstream library. +type Scanner interface { + Scan(dir string, opts ScanOptions) ([]Finding, error) +} diff --git a/apps/cli/internal/scan/vuln.go b/apps/cli/internal/scan/vuln.go new file mode 100644 index 0000000..634aa0b --- /dev/null +++ b/apps/cli/internal/scan/vuln.go @@ -0,0 +1,149 @@ +package scan + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// VulnResult holds the output of a vulnerability scan. +type VulnResult struct { + Tool string // e.g. "govulncheck", "pnpm audit" + Summary string // short one-line summary + Output string // full tool output + Pass bool // true if no vulns found +} + +// RunGovulncheck runs govulncheck in dir and returns the result. +func RunGovulncheck(dir string) *VulnResult { + r := &VulnResult{Tool: "govulncheck"} + + // Check if there's a Go project + if _, err := os.Stat(filepath.Join(dir, "go.mod")); os.IsNotExist(err) { + r.Summary = "skipped (no go.mod)" + r.Pass = true + return r + } + + var out []byte + var err error + + // Prefer installed govulncheck, fallback to go run + if _, lookErr := exec.LookPath("govulncheck"); lookErr == nil { + cmd := exec.Command("govulncheck", "./...") + cmd.Dir = dir + out, err = cmd.CombinedOutput() + } else { + cmd := exec.Command("go", "run", "golang.org/x/vuln/cmd/govulncheck@latest", "./...") + cmd.Dir = dir + out, err = cmd.CombinedOutput() + } + + r.Output = strings.TrimSpace(string(out)) + + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + // govulncheck exits 1 when vulns found, 3 on errors + if exitErr.ExitCode() == 1 { + r.Summary = "vulnerabilities found" + return r + } + if exitErr.ExitCode() == 3 { + r.Summary = "error running scan" + r.Output = r.Output + "\n" + string(exitErr.Stderr) + return r + } + } + r.Summary = fmt.Sprintf("failed: %v", err) + return r + } + + r.Summary = "no vulnerabilities found" + r.Pass = true + return r +} + +// RunPnpmAudit runs pnpm audit in dir and returns the result. +func RunPnpmAudit(dir string) *VulnResult { + r := &VulnResult{Tool: "pnpm audit"} + + if _, err := os.Stat(filepath.Join(dir, "pnpm-lock.yaml")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(dir, "package.json")); os.IsNotExist(err) { + r.Summary = "skipped (no package.json)" + r.Pass = true + return r + } + } + + if _, lookErr := exec.LookPath("pnpm"); lookErr != nil { + r.Summary = "skipped (pnpm not found)" + r.Pass = true + return r + } + + cmd := exec.Command("pnpm", "audit", "--prod", "--audit-level", "high") + cmd.Dir = dir + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + r.Output = strings.TrimSpace(stdout.String() + "\n" + stderr.String()) + + if err != nil { + if _, ok := err.(*exec.ExitError); ok { + r.Summary = "high/critical vulnerabilities found" + return r + } + r.Summary = fmt.Sprintf("failed: %v", err) + return r + } + + r.Summary = "no high/critical vulnerabilities" + r.Pass = true + return r +} + +// RunNpmAudit runs npm audit in dir and returns the result. +func RunNpmAudit(dir string) *VulnResult { + r := &VulnResult{Tool: "npm audit"} + + if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(dir, "package.json")); os.IsNotExist(err) { + r.Summary = "skipped (no package.json)" + r.Pass = true + return r + } + } + + if _, lookErr := exec.LookPath("npm"); lookErr != nil { + r.Summary = "skipped (npm not found)" + r.Pass = true + return r + } + + cmd := exec.Command("npm", "audit", "--production", "--audit-level", "high") + cmd.Dir = dir + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + r.Output = strings.TrimSpace(stdout.String() + "\n" + stderr.String()) + + if err != nil { + if _, ok := err.(*exec.ExitError); ok { + r.Summary = "high/critical vulnerabilities found" + return r + } + r.Summary = fmt.Sprintf("failed: %v", err) + return r + } + + r.Summary = "no high/critical vulnerabilities" + r.Pass = true + return r +} diff --git a/apps/cli/internal/scan/vuln_test.go b/apps/cli/internal/scan/vuln_test.go new file mode 100644 index 0000000..48a061f --- /dev/null +++ b/apps/cli/internal/scan/vuln_test.go @@ -0,0 +1,48 @@ +package scan + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRunGovulncheck_SkippedNoGoMod(t *testing.T) { + dir := t.TempDir() + r := RunGovulncheck(dir) + if !r.Pass { + t.Errorf("expected Pass=true when no go.mod, got Pass=%v Summary=%q", r.Pass, r.Summary) + } + if r.Summary != "skipped (no go.mod)" { + t.Errorf("expected 'skipped (no go.mod)', got %q", r.Summary) + } +} + +func TestRunGovulncheck_HasGoMod(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\ngo 1.25\n"), 0644) + // Just verify it doesn't panic and returns something + r := RunGovulncheck(dir) + if r.Tool != "govulncheck" { + t.Errorf("expected Tool=govulncheck, got %q", r.Tool) + } + // Summary will vary depending on whether govulncheck is installed + if r.Summary == "" { + t.Error("expected non-empty Summary") + } +} + +func TestRunPnpmAudit_SkippedNoPackageJSON(t *testing.T) { + dir := t.TempDir() + r := RunPnpmAudit(dir) + if !r.Pass { + t.Errorf("expected Pass=true when no package.json, got Pass=%v", r.Pass) + } +} + +func TestRunNpmAudit_SkippedNoPackageJSON(t *testing.T) { + dir := t.TempDir() + r := RunNpmAudit(dir) + if !r.Pass { + t.Errorf("expected Pass=true when no package.json, got Pass=%v", r.Pass) + } +} diff --git a/apps/cli/internal/utils/table.go b/apps/cli/internal/utils/table.go index d23f2d6..5f582f5 100644 --- a/apps/cli/internal/utils/table.go +++ b/apps/cli/internal/utils/table.go @@ -2,16 +2,21 @@ package utils import ( "fmt" + "io" + "os" "strings" ) -// PrintTable prints a 2D string slice as a pretty table type Table struct { Header []string Rows [][]string } func PrintTable(header []string, rows [][]string) { + PrintTableTo(os.Stdout, header, rows) +} + +func PrintTableTo(w io.Writer, header []string, rows [][]string) { colWidths := make([]int, len(header)) for i, h := range header { colWidths[i] = len(h) @@ -27,19 +32,19 @@ func PrintTable(header []string, rows [][]string) { for _, w := range colWidths { border += strings.Repeat("-", w+2) + "+" } - fmt.Println(border) - fmt.Print("|") + fmt.Fprintln(w, border) + fmt.Fprint(w, "|") for i, h := range header { - fmt.Printf(" %-*s |", colWidths[i], h) + fmt.Fprintf(w, " %-*s |", colWidths[i], h) } - fmt.Println() - fmt.Println(border) + fmt.Fprintln(w) + fmt.Fprintln(w, border) for _, row := range rows { - fmt.Print("|") + fmt.Fprint(w, "|") for i, cell := range row { - fmt.Printf(" %-*s |", colWidths[i], cell) + fmt.Fprintf(w, " %-*s |", colWidths[i], cell) } - fmt.Println() + fmt.Fprintln(w) } - fmt.Println(border) + fmt.Fprintln(w, border) } diff --git a/apps/cli/main.go b/apps/cli/main.go index fd48e4f..3ed3402 100644 --- a/apps/cli/main.go +++ b/apps/cli/main.go @@ -91,6 +91,7 @@ It includes commands for Frontend (fe), Backend (be), DevOps, and Design teams.` rootCmd.AddCommand(rootcmd.DoctorCmd) + rootCmd.AddCommand(rootcmd.ScanCmd) // Execute if err := rootCmd.Execute(); err != nil { diff --git a/apps/cli/release/radas-darwin-amd64 b/apps/cli/release/radas-darwin-amd64 deleted file mode 100755 index 345203a..0000000 Binary files a/apps/cli/release/radas-darwin-amd64 and /dev/null differ diff --git a/apps/cli/release/radas-darwin-amd64.tar.gz b/apps/cli/release/radas-darwin-amd64.tar.gz deleted file mode 100644 index 4391b7e..0000000 Binary files a/apps/cli/release/radas-darwin-amd64.tar.gz and /dev/null differ diff --git a/apps/cli/release/radas-darwin-arm64 b/apps/cli/release/radas-darwin-arm64 deleted file mode 100755 index f0afc1d..0000000 Binary files a/apps/cli/release/radas-darwin-arm64 and /dev/null differ diff --git a/apps/cli/release/radas-darwin-arm64.tar.gz b/apps/cli/release/radas-darwin-arm64.tar.gz deleted file mode 100644 index 727ff97..0000000 Binary files a/apps/cli/release/radas-darwin-arm64.tar.gz and /dev/null differ diff --git a/apps/cli/release/radas-linux-amd64 b/apps/cli/release/radas-linux-amd64 deleted file mode 100755 index b821371..0000000 Binary files a/apps/cli/release/radas-linux-amd64 and /dev/null differ diff --git a/apps/cli/release/radas-linux-amd64.tar.gz b/apps/cli/release/radas-linux-amd64.tar.gz deleted file mode 100644 index 8012f47..0000000 Binary files a/apps/cli/release/radas-linux-amd64.tar.gz and /dev/null differ diff --git a/apps/cli/release/radas-windows-amd64.exe b/apps/cli/release/radas-windows-amd64.exe deleted file mode 100755 index e5a3a13..0000000 Binary files a/apps/cli/release/radas-windows-amd64.exe and /dev/null differ diff --git a/apps/cli/release/radas-windows-amd64.zip b/apps/cli/release/radas-windows-amd64.zip deleted file mode 100644 index 11423dc..0000000 Binary files a/apps/cli/release/radas-windows-amd64.zip and /dev/null differ diff --git a/apps/cli/scripts/release-github.sh b/apps/cli/scripts/release-github.sh index 4bcf8fd..8307eab 100755 --- a/apps/cli/scripts/release-github.sh +++ b/apps/cli/scripts/release-github.sh @@ -1,37 +1,60 @@ #!/bin/bash -set -e +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$PROJECT_DIR" + +CURRENT_VERSION=$(go run -mod=mod github.com/raizora/radas/v4/constants 2>/dev/null || grep "Version = " constants/version.go | head -1 | sed 's/.*"\(.*\)".*/\1/') +VERSION="${1:-$CURRENT_VERSION}" -# Set your version/tag -VERSION="$1" if [ -z "$VERSION" ]; then - echo "Usage: $0 " + echo "Usage: $0 [version]" + echo " (defaults to version from constants/version.go)" exit 1 fi -# Build for all platforms +echo "==> Releasing radas $VERSION" + +# Update version constant if different +if [ "$VERSION" != "$CURRENT_VERSION" ]; then + echo "==> Bumping version: $CURRENT_VERSION → $VERSION" + sed -i '' "s/Version = \"$CURRENT_VERSION\"/Version = \"$VERSION\"/" constants/version.go +fi + +echo "==> Building for all platforms..." BIN_DIR="release" +rm -rf "$BIN_DIR" mkdir -p "$BIN_DIR" -GOOS=linux GOARCH=amd64 go build -o "$BIN_DIR/radas-linux-amd64" . -GOOS=darwin GOARCH=amd64 go build -o "$BIN_DIR/radas-darwin-amd64" . -GOOS=darwin GOARCH=arm64 go build -o "$BIN_DIR/radas-darwin-arm64" . -GOOS=windows GOARCH=amd64 go build -o "$BIN_DIR/radas-windows-amd64.exe" . +echo " linux/amd64..." +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/raizora/radas/v4/constants.Version=$VERSION" -o "$BIN_DIR/radas-linux-amd64" . -# (Optional) Compress binaries +echo " darwin/amd64..." +GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X github.com/raizora/radas/v4/constants.Version=$VERSION" -o "$BIN_DIR/radas-darwin-amd64" . + +echo " darwin/arm64..." +GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X github.com/raizora/radas/v4/constants.Version=$VERSION" -o "$BIN_DIR/radas-darwin-arm64" . + +echo " windows/amd64..." +GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X github.com/raizora/radas/v4/constants.Version=$VERSION" -o "$BIN_DIR/radas-windows-amd64.exe" . + +echo "==> Compressing..." cd "$BIN_DIR" tar czf "radas-linux-amd64.tar.gz" radas-linux-amd64 tar czf "radas-darwin-amd64.tar.gz" radas-darwin-amd64 tar czf "radas-darwin-arm64.tar.gz" radas-darwin-arm64 -zip "radas-windows-amd64.zip" radas-windows-amd64.exe -cd .. - -# Create a GitHub release (draft, or publish directly) -gh release create "$VERSION" \ - --title "Release $VERSION" \ - --notes "See CHANGELOG.md for details." \ - release/radas-linux-amd64.tar.gz \ - release/radas-darwin-amd64.tar.gz \ - release/radas-darwin-arm64.tar.gz \ - release/radas-windows-amd64.zip - -echo "GitHub release $VERSION created with binaries!" \ No newline at end of file +zip -q "radas-windows-amd64.zip" radas-windows-amd64.exe +rm -f radas-linux-amd64 radas-darwin-amd64 radas-darwin-arm64 radas-windows-amd64.exe +cd "$PROJECT_DIR" + +echo "==> Verifying binaries..." +for f in "$BIN_DIR"/*.tar.gz "$BIN_DIR"/*.zip; do + echo " $(ls -lh "$f" | awk '{print $5, $NF}')" +done + +echo "" +echo "==> Release $VERSION ready in $BIN_DIR/" +echo "" +echo "To publish: gh release create \"$VERSION\" --title \"Release $VERSION\" --notes \"\" $BIN_DIR/*"