- Add auth.RequireScope() to all handler routes for proper authorization - Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator) - Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog) - Add artifact_test.go for SDLC artifact coverage - Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w - Fix error wrapping to use %w instead of %v throughout codebase - Improve CLI merge command with conflict detection and resolution - Fix handler tests to include auth middleware for RequireScope - Add cookbook tree runner scripts for automated testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
315 lines
6.6 KiB
Go
315 lines
6.6 KiB
Go
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 <slug>",
|
|
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 <slug> --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 <slug>",
|
|
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 <slug>",
|
|
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 <slug> <phase>",
|
|
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 <slug> <reason>",
|
|
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 <slug>",
|
|
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)
|
|
}
|