194 lines
4.8 KiB
Go
194 lines
4.8 KiB
Go
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 <list|show|status> [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
|
|
}
|