Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/cli. OpenStatus CLI www.openstatus.dev
Something went wrong. Try again.
11 kB · 418 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419package statusreport
import ( "context" "errors" "fmt" "os" "strings"
status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "connectrpc.com/connect" "github.com/charmbracelet/huh" "github.com/openstatusHQ/cli/internal/api" output "github.com/openstatusHQ/cli/internal/cli")
type createInputs struct { PageID string PageName string Title string Status string Message string ComponentIDs []string componentNames map[string]string // component ID -> display name, for summary Notify bool Confirmed bool}
type addUpdateInputs struct { ReportID string ReportName string Status string Message string Notify bool Confirmed bool}
func notEmpty(fieldName string) func(string) error { return func(s string) error { if strings.TrimSpace(s) == "" { return fmt.Errorf("%s cannot be empty", fieldName) } return nil }}
func handleFormError(err error) error { if errors.Is(err, huh.ErrUserAborted) { fmt.Fprintln(os.Stderr, "Aborted.") os.Exit(130) } return err}
func statusSelectOptions() []huh.Option[string] { return []huh.Option[string]{ huh.NewOption("Investigating", "investigating"), huh.NewOption("Identified", "identified"), huh.NewOption("Monitoring", "monitoring"), huh.NewOption("Resolved", "resolved"), }}
func buildSummary(lines [][2]string) string { var sb strings.Builder for _, line := range lines { sb.WriteString(fmt.Sprintf(" %s: %s\n", line[0], line[1])) } return sb.String()}
// Data-fetching functions
func fetchStatusPages(ctx context.Context, apiKey string) ([]*status_pagev1.StatusPageSummary, error) { client := status_pagev1connect.NewStatusPageServiceClient( api.DefaultHTTPClient, api.ConnectBaseURL, connect.WithInterceptors(api.NewAuthInterceptor(apiKey)), connect.WithProtoJSON(), ) resp, err := client.ListStatusPages(ctx, &status_pagev1.ListStatusPagesRequest{}) if err != nil { return nil, output.FormatError(err, "status-page", "") } return resp.GetStatusPages(), nil}
func fetchPageComponents(ctx context.Context, apiKey string, pageID string) ([]*status_pagev1.PageComponent, []*status_pagev1.PageComponentGroup, error) { client := status_pagev1connect.NewStatusPageServiceClient( api.DefaultHTTPClient, api.ConnectBaseURL, connect.WithInterceptors(api.NewAuthInterceptor(apiKey)), connect.WithProtoJSON(), ) req := &status_pagev1.GetStatusPageContentRequest{} req.SetId(pageID) resp, err := client.GetStatusPageContent(ctx, req) if err != nil { return nil, nil, output.FormatError(err, "status-page", pageID) } return resp.GetComponents(), resp.GetGroups(), nil}
func fetchStatusReports(ctx context.Context, apiKey string) ([]*status_reportv1.StatusReportSummary, error) { client := NewStatusReportClient(apiKey)
// Try unresolved reports first req := &status_reportv1.ListStatusReportsRequest{} l := int32(20) req.SetLimit(l) req.SetStatuses([]status_reportv1.StatusReportStatus{ status_reportv1.StatusReportStatus_STATUS_REPORT_STATUS_INVESTIGATING, status_reportv1.StatusReportStatus_STATUS_REPORT_STATUS_IDENTIFIED, status_reportv1.StatusReportStatus_STATUS_REPORT_STATUS_MONITORING, })
resp, err := client.ListStatusReports(ctx, req) if err != nil { return nil, output.FormatError(err, "status-report", "") }
reports := resp.GetStatusReports() if len(reports) > 0 { return reports, nil }
// Fall back to all recent reports reqAll := &status_reportv1.ListStatusReportsRequest{} reqAll.SetLimit(l) resp, err = client.ListStatusReports(ctx, reqAll) if err != nil { return nil, output.FormatError(err, "status-report", "") } return resp.GetStatusReports(), nil}
// Wizard: sr create
func runCreateWizard(ctx context.Context, apiKey string, prefilled *createInputs) (*createInputs, error) { inputs := *prefilled
// Fetch status pages s := output.StartSpinner("Fetching status pages...") pages, err := fetchStatusPages(ctx, apiKey) output.StopSpinner(s) if err != nil { return nil, err } if len(pages) == 0 { return nil, fmt.Errorf("no status pages found. Create one at https://www.openstatus.dev first, then run this command again") }
// Form 1: page select (if needed) if inputs.PageID == "" { pageOptions := make([]huh.Option[string], 0, len(pages)) for _, p := range pages { label := p.GetTitle() + " (" + statusPageURL(p) + ")" pageOptions = append(pageOptions, huh.NewOption(label, p.GetId())) }
form1 := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Status page"). Options(pageOptions...). Value(&inputs.PageID), ), ).WithTheme(huh.ThemeBase())
if err := form1.Run(); err != nil { return nil, handleFormError(err) } }
// Resolve page name for summary for _, p := range pages { if p.GetId() == inputs.PageID { inputs.PageName = p.GetTitle() break } }
// Fetch components for selected page s = output.StartSpinner("Fetching page components...") components, groups, err := fetchPageComponents(ctx, apiKey, inputs.PageID) output.StopSpinner(s) if err != nil { return nil, err }
// Build Form 2 fields dynamically var fields []huh.Field
// Build component name map for summary display if len(components) > 0 { groupMap := make(map[string]string, len(groups)) for _, g := range groups { groupMap[g.GetId()] = g.GetName() }
inputs.componentNames = make(map[string]string, len(components)) compOptions := make([]huh.Option[string], 0, len(components)) for _, c := range components { label := c.GetName() if gid := c.GetGroupId(); gid != "" { if gname, ok := groupMap[gid]; ok { label += " (" + gname + ")" } } inputs.componentNames[c.GetId()] = label compOptions = append(compOptions, huh.NewOption(label, c.GetId())) }
// Component multi-select (only if not prefilled) if len(prefilled.ComponentIDs) == 0 { fields = append(fields, huh.NewMultiSelect[string](). Title("Components"). Options(compOptions...). Value(&inputs.ComponentIDs)) } }
if inputs.Title == "" { fields = append(fields, huh.NewInput(). Title("Title"). Validate(notEmpty("title")). Value(&inputs.Title)) }
if inputs.Status == "" { fields = append(fields, huh.NewSelect[string](). Title("Status"). Options(statusSelectOptions()...). Value(&inputs.Status)) }
if inputs.Message == "" { fields = append(fields, huh.NewText(). Title("Message"). Validate(notEmpty("message")). Value(&inputs.Message)) }
fields = append(fields, huh.NewConfirm(). Title("Notify subscribers?"). Value(&inputs.Notify))
// Summary + confirm group summaryNote := huh.NewNote(). Title("Summary"). DescriptionFunc(func() string { lines := [][2]string{ {"Page", inputs.PageName}, } if len(inputs.ComponentIDs) > 0 { names := make([]string, 0, len(inputs.ComponentIDs)) for _, id := range inputs.ComponentIDs { if name, ok := inputs.componentNames[id]; ok { names = append(names, name) } else { names = append(names, id) } } lines = append(lines, [2]string{"Components", strings.Join(names, ", ")}) } lines = append(lines, [2]string{"Title", inputs.Title}, [2]string{"Status", inputs.Status}, [2]string{"Message", inputs.Message}, ) notifyStr := "no" if inputs.Notify { notifyStr = "yes" } lines = append(lines, [2]string{"Notify", notifyStr}) return buildSummary(lines) }, &inputs)
form2 := huh.NewForm( huh.NewGroup(fields...), huh.NewGroup( summaryNote, huh.NewConfirm(). Title("Create this status report?"). Value(&inputs.Confirmed), ), ).WithTheme(huh.ThemeBase())
if err := form2.Run(); err != nil { return nil, handleFormError(err) }
if !inputs.Confirmed { fmt.Fprintln(os.Stderr, "Aborted.") os.Exit(130) }
return &inputs, nil}
// Wizard: sr add-update
func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdateInputs) (*addUpdateInputs, error) { inputs := *prefilled
// Fetch reports for selection or name resolution s := output.StartSpinner("Fetching status reports...") reports, err := fetchStatusReports(ctx, apiKey) output.StopSpinner(s) if err != nil { return nil, err }
if inputs.ReportID == "" { // Form 1: report select if len(reports) == 0 { return nil, fmt.Errorf("no status reports found") }
reportOptions := make([]huh.Option[string], 0, len(reports)) for _, r := range reports { label := r.GetTitle() + " (" + statusToString(r.GetStatus()) + ")" reportOptions = append(reportOptions, huh.NewOption(label, r.GetId())) }
form1 := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("Select a status report"). Options(reportOptions...). Value(&inputs.ReportID), ), ).WithTheme(huh.ThemeBase())
if err := form1.Run(); err != nil { return nil, handleFormError(err) } }
// Resolve report name for summary for _, r := range reports { if r.GetId() == inputs.ReportID { inputs.ReportName = r.GetTitle() break } } if inputs.ReportName == "" { inputs.ReportName = inputs.ReportID }
// Build Form 2 fields dynamically var fields []huh.Field
if inputs.Status == "" { fields = append(fields, huh.NewSelect[string](). Title("Status"). Options(statusSelectOptions()...). Value(&inputs.Status)) }
if inputs.Message == "" { fields = append(fields, huh.NewInput(). Title("Message"). Validate(notEmpty("message")). Value(&inputs.Message)) }
fields = append(fields, huh.NewConfirm(). Title("Notify subscribers?"). Value(&inputs.Notify))
summaryNote := huh.NewNote(). Title("Summary"). DescriptionFunc(func() string { lines := [][2]string{ {"Report", inputs.ReportName}, {"Status", inputs.Status}, {"Message", inputs.Message}, } notifyStr := "no" if inputs.Notify { notifyStr = "yes" } lines = append(lines, [2]string{"Notify", notifyStr}) return buildSummary(lines) }, &inputs)
form2 := huh.NewForm( huh.NewGroup(fields...), huh.NewGroup( summaryNote, huh.NewConfirm(). Title("Add this update?"). Value(&inputs.Confirmed), ), ).WithTheme(huh.ThemeBase())
if err := form2.Run(); err != nil { return nil, handleFormError(err) }
if !inputs.Confirmed { fmt.Fprintln(os.Stderr, "Aborted.") os.Exit(130) }
return &inputs, nil}
func statusPageURL(p *status_pagev1.StatusPageSummary) string { if d := p.GetCustomDomain(); d != "" { d = strings.TrimPrefix(strings.TrimPrefix(d, "https://"), "http://") return "https://" + d } return "https://" + p.GetSlug() + ".openstatus.dev"}