package main import ( "encoding/json" "fmt" "os" "path/filepath" "github.com/orchard9/rdev/internal/sdlc" "github.com/spf13/cobra" ) var ( rootDir string jsonOutput bool ) var rootCmd = &cobra.Command{ Use: "sdlc", Short: "Deterministic SDLC orchestration tool", Long: "Manage the software development lifecycle with deterministic state, artifacts, and classification.", } func init() { rootCmd.PersistentFlags().StringVar(&rootDir, "root", "", "project root (default: auto-detect)") rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "output as JSON") } // resolveRoot finds the project root by looking for .sdlc/ or .git/ walking up. func resolveRoot() (string, error) { if rootDir != "" { abs, err := filepath.Abs(rootDir) if err != nil { return "", fmt.Errorf("resolve root: %w", err) } return abs, nil } dir, err := os.Getwd() if err != nil { return "", fmt.Errorf("get working directory: %w", err) } for { if sdlc.IsInitialized(dir) { return dir, nil } if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } // Fall back to cwd cwd, _ := os.Getwd() return cwd, nil } // mustResolveRoot resolves root or panics (for use in RunE where error is returned). // Note: Panicking here is intentional as these commands run under cobra which // catches panics and converts them to errors. For production use, prefer resolveRoot(). func mustResolveRoot() string { root, err := resolveRoot() if err != nil { panic(fmt.Sprintf("resolve root: %v", err)) } return root } // printJSON marshals v as indented JSON and prints to stdout. // Returns an error if marshaling fails instead of exiting. func printJSON(v any) error { data, err := json.MarshalIndent(v, "", " ") if err != nil { return fmt.Errorf("marshal JSON: %w", err) } fmt.Println(string(data)) return nil }