package main import ( "fmt" "os" "sort" "strings" ) // cmdFlow dispatches flow subcommands. func cmdFlow(args []string, flags Flags) { if len(args) == 0 { fmt.Fprintln(os.Stderr, "usage: dev_console flow [args]") os.Exit(1) } sub := args[0] rest := args[1:] switch sub { case "list": flowList(rest, flags) case "show": flowShow(rest, flags) case "status": flowStatus(rest, flags) // v2 stubs case "create", "dod", "trace", "user-test", "run", "chain", "done": fmt.Fprintf(os.Stderr, "TODO v2: flow %s not yet implemented\n", sub) os.Exit(2) default: fmt.Fprintf(os.Stderr, "unknown flow subcommand: %s\n", sub) os.Exit(1) } } // flowList lists flows with optional filters. func flowList(args []string, flags Flags) { root := mustRegistryRoot() flows, err := LoadAllFlows(root) if err != nil { fatalf("load flows: %v", err) } // Apply filters var filtered []Flow for _, fl := range flows { if flags.App != "" && !matchApp(fl.Apps, flags.App) { continue } if flags.Pattern != "" && !matchStr(fl.Pattern, flags.Pattern) { continue } if flags.Risk != "" && !matchStr(fl.Risk, flags.Risk) { continue } filtered = append(filtered, fl) } sort.Slice(filtered, func(i, j int) bool { return filtered[i].ID < filtered[j].ID }) if flags.JSON { printJSON(filtered) return } headers := []string{"ID", "NAME", "STATUS", "PRIO", "RISK", "APPS", "ACCEPTANCE", "DOD", "USER-FACING"} var rows [][]string for _, fl := range filtered { rows = append(rows, []string{ fl.ID, truncate(fl.Name, 30), statusShort(fl.Status), fl.Priority, fl.Risk, truncate(joinStrings(fl.Apps), 30), pctBar(fl.AcceptancePct), pctBar(fl.DoDPct), pctBar(fl.UserFacingPct), }) } printTable(os.Stdout, headers, rows) fmt.Printf("\nTotal: %d flows\n", len(filtered)) } // flowShow prints the full content of a flow. func flowShow(args []string, flags Flags) { if len(args) == 0 { fatalf("usage: dev_console flow show NNNN") } id := normalizeID(args[0]) root := mustRegistryRoot() fl, err := findFlowByID(root, id) if err != nil { fatalf("flow %s: %v", id, err) } if flags.JSON { printJSON(fl) return } fmt.Printf("# Flow %s — %s\n\n", fl.ID, fl.Name) fmt.Printf("Status: %s\n", statusShort(fl.Status)) fmt.Printf("Priority: %s\n", fl.Priority) fmt.Printf("Risk: %s\n", fl.Risk) fmt.Printf("Apps: %s\n", joinStrings(fl.Apps)) fmt.Printf("Trigger: %s\n", fl.Trigger) fmt.Printf("Schedule: %s\n", fl.Schedule) fmt.Printf("ExpectedRuntime: %ds\n", fl.ExpectedRuntimeS) fmt.Printf("Tags: %s\n", joinStrings(fl.Tags)) fmt.Printf("Created: %s\n", fl.Created) fmt.Printf("Updated: %s\n", fl.Updated) fmt.Printf("Path: %s\n", fl.Path) fmt.Printf("\nAcceptance: %s\n", pctBar(fl.AcceptancePct)) fmt.Printf("DoD: %s\n", pctBar(fl.DoDPct)) fmt.Printf("User-facing: %s\n", pctBar(fl.UserFacingPct)) fmt.Printf("\n---\n%s\n", fl.Body) } // flowStatus prints acceptance + DoD + user-facing % for a flow. func flowStatus(args []string, flags Flags) { if len(args) == 0 { fatalf("usage: dev_console flow status NNNN") } id := normalizeID(args[0]) root := mustRegistryRoot() fl, err := findFlowByID(root, id) if err != nil { fatalf("flow %s: %v", id, err) } type statusOut struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` AcceptancePct int `json:"acceptance_pct"` DoDPct int `json:"dod_pct"` UserFacingPct int `json:"user_facing_pct"` DoDComplete bool `json:"dod_complete"` } out := statusOut{ ID: fl.ID, Name: fl.Name, Status: fl.Status, AcceptancePct: fl.AcceptancePct, DoDPct: fl.DoDPct, UserFacingPct: fl.UserFacingPct, DoDComplete: fl.DoDPct == 100, } if flags.JSON { printJSON(out) return } fmt.Printf("Flow %s — %s\n", fl.ID, fl.Name) fmt.Printf("Status: %s\n", statusShort(fl.Status)) fmt.Printf("Acceptance: %s\n", pctBar(fl.AcceptancePct)) fmt.Printf("DoD: %s\n", pctBar(fl.DoDPct)) fmt.Printf("User-facing: %s\n", pctBar(fl.UserFacingPct)) if fl.DoDPct == 100 { fmt.Println("DoD: COMPLETE (100%)") } else { fmt.Printf("DoD: INCOMPLETE (%d%%)\n", fl.DoDPct) } } // findFlowByID searches for a flow by ID. func findFlowByID(root, id string) (Flow, error) { flows, err := LoadAllFlows(root) if err != nil { return Flow{}, err } for _, fl := range flows { if fl.ID == id { return fl, nil } } return Flow{}, fmt.Errorf("not found") } // matchApp returns true if any of the flow's apps matches the filter. func matchApp(apps []string, filter string) bool { filter = strings.ToLower(filter) for _, a := range apps { if strings.ToLower(a) == filter { return true } } return false }