package main import ( "fmt" "github.com/orchard9/rdev/internal/sdlc" "github.com/spf13/cobra" ) var featureTitle string var featureCmd = &cobra.Command{ Use: "feature", Short: "Manage features", } var featureCreateCmd = &cobra.Command{ Use: "create ", Short: "Create a new feature", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { root := mustResolveRoot() slug := args[0] title := featureTitle if title == "" { title = slug } f, err := sdlc.CreateFeature(root, slug, title) if err != nil { return err } // Add to active work in state state, err := sdlc.LoadState(root) if err != nil { return err } state.AddActiveFeature(slug, sdlc.PhaseDraft) state.RecordAction("CREATE_FEATURE", slug, "cli", "success") if err := state.Save(root); err != nil { return err } if jsonOutput { return printJSON(f) } fmt.Printf("Created feature: %s\n", slug) fmt.Printf(" Title: %s\n", f.Title) fmt.Printf(" Phase: %s\n", f.Phase) fmt.Printf(" Path: .sdlc/features/%s/\n", slug) fmt.Println() fmt.Printf("Next: sdlc next --for %s\n", slug) return nil }, } var featureListCmd = &cobra.Command{ Use: "list", Short: "List all features", RunE: func(_ *cobra.Command, _ []string) error { root := mustResolveRoot() features, err := sdlc.ListFeatures(root) if err != nil { return err } if jsonOutput { return printJSON(features) } if len(features) == 0 { fmt.Println("No features found.") fmt.Println("Create one: sdlc feature create --title \"Feature Name\"") return nil } fmt.Println("Features:") for _, f := range features { summary := sdlc.SummarizeTasks(f.Tasks) taskInfo := "" if summary.Total > 0 { taskInfo = fmt.Sprintf(" (%d/%d tasks)", summary.Completed, summary.Total) } blocked := "" if f.IsBlocked() { blocked = " [BLOCKED]" } fmt.Printf(" %-20s [%-15s]%s%s\n", f.Slug, f.Phase, taskInfo, blocked) } return nil }, } var featureShowCmd = &cobra.Command{ Use: "show ", Short: "Show feature details", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { root := mustResolveRoot() f, err := sdlc.LoadFeature(root, args[0]) if err != nil { return err } if jsonOutput { return printJSON(f) } fmt.Printf("Feature: %s\n", f.Slug) fmt.Printf(" Title: %s\n", f.Title) fmt.Printf(" Phase: %s\n", f.Phase) fmt.Printf(" Created: %s\n", f.Created.Format("2006-01-02 15:04:05")) if f.Branch != "" { fmt.Printf(" Branch: %s\n", f.Branch) } fmt.Println() fmt.Println("Artifacts:") for _, at := range sdlc.ValidArtifactTypes { art := f.GetArtifact(at) if art == nil { continue } fmt.Printf(" %-12s %s\n", at, art.Status) } fmt.Println() if len(f.Tasks) > 0 { summary := sdlc.SummarizeTasks(f.Tasks) fmt.Printf("Tasks: %d/%d complete", summary.Completed, summary.Total) if summary.InProgress > 0 { fmt.Printf(", %d in-progress", summary.InProgress) } if summary.Blocked > 0 { fmt.Printf(", %d blocked", summary.Blocked) } fmt.Println() } if f.IsBlocked() { fmt.Println() fmt.Println("Blockers:") for _, b := range f.Blockers { fmt.Printf(" - %s\n", b) } } return nil }, } var featureStatusCmd = &cobra.Command{ Use: "status ", Short: "Show feature phase and progress", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { root := mustResolveRoot() f, err := sdlc.LoadFeature(root, args[0]) if err != nil { return err } if jsonOutput { result := map[string]any{ "slug": f.Slug, "phase": f.Phase, "blocked": f.IsBlocked(), "tasks": sdlc.SummarizeTasks(f.Tasks), "blockers": f.Blockers, } return printJSON(result) } fmt.Printf("Feature: %s\n", f.Slug) fmt.Printf("Phase: %s\n", f.Phase) if f.IsBlocked() { fmt.Println("Status: BLOCKED") } if len(f.Tasks) > 0 { s := sdlc.SummarizeTasks(f.Tasks) fmt.Printf("Tasks: %d/%d complete\n", s.Completed, s.Total) } return nil }, } var featureTransitionCmd = &cobra.Command{ Use: "transition ", Short: "Manually transition feature to a phase", Args: cobra.ExactArgs(2), RunE: func(_ *cobra.Command, args []string) error { root := mustResolveRoot() slug, phase := args[0], sdlc.FeaturePhase(args[1]) f, err := sdlc.LoadFeature(root, slug) if err != nil { return err } cfg, err := sdlc.LoadConfig(root) if err != nil { return err } if err := f.CanTransitionTo(phase, cfg); err != nil { return err } from := f.Phase if err := f.Transition(phase); err != nil { return err } if err := f.Save(root); err != nil { return err } // Update state state, err := sdlc.LoadState(root) if err != nil { return err } state.UpdateActiveFeature(slug, phase, f.Branch) state.RecordAction("TRANSITION", slug, "cli", "success") if err := state.Save(root); err != nil { return err } if jsonOutput { return printJSON(map[string]string{ "slug": slug, "from": string(from), "to": string(phase), }) } fmt.Printf("Transitioned %s: %s -> %s\n", slug, from, phase) return nil }, } var featureBlockCmd = &cobra.Command{ Use: "block ", Short: "Mark feature as blocked", Args: cobra.ExactArgs(2), RunE: func(_ *cobra.Command, args []string) error { root := mustResolveRoot() slug, reason := args[0], args[1] f, err := sdlc.LoadFeature(root, slug) if err != nil { return err } f.AddBlocker(reason) if err := f.Save(root); err != nil { return err } if jsonOutput { return printJSON(map[string]string{"slug": slug, "blocker": reason}) } fmt.Printf("Blocked %s: %s\n", slug, reason) return nil }, } var featureUnblockCmd = &cobra.Command{ Use: "unblock ", Short: "Remove all blockers from feature", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { root := mustResolveRoot() slug := args[0] f, err := sdlc.LoadFeature(root, slug) if err != nil { return err } f.ClearBlockers() if err := f.Save(root); err != nil { return err } if jsonOutput { return printJSON(map[string]string{"slug": slug, "status": "unblocked"}) } fmt.Printf("Unblocked %s\n", slug) return nil }, } func init() { featureCreateCmd.Flags().StringVar(&featureTitle, "title", "", "feature title") featureCmd.AddCommand( featureCreateCmd, featureListCmd, featureShowCmd, featureStatusCmd, featureTransitionCmd, featureBlockCmd, featureUnblockCmd, ) rootCmd.AddCommand(featureCmd) }