Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ module github.com/flashcatcloud/flashduty-cli
go 1.25.1

require (
github.com/flashcatcloud/flashduty-sdk v0.9.1
github.com/flashcatcloud/go-flashduty v0.5.2
github.com/mattn/go-runewidth v0.0.23
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c
golang.org/x/term v0.42.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.43.0 // indirect
)
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/flashcatcloud/flashduty-sdk v0.9.1 h1:vDTkSjAJJD6Ex5r7S+VCxPi4yxSFNw1bU/SfoRCvk+k=
github.com/flashcatcloud/flashduty-sdk v0.9.1/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/flashcatcloud/go-flashduty v0.5.2 h1:mYg/M0jqkil30WTLdICVtTJVGxEIGmae/3zBpRkwLRQ=
github.com/flashcatcloud/go-flashduty v0.5.2/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
Expand All @@ -16,8 +16,6 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c h1:D8lDFovBMZywze1eh9iwMLcYor5f11mHBocLhO7cBe8=
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c/go.mod h1:j/BOnpF2ihnz4lELs99h9mwGJBx/zdleOUCnLLRPCsc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
Expand Down
138 changes: 95 additions & 43 deletions internal/cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package cli
import (
"fmt"
"io"
"strconv"
"strings"

flashduty "github.com/flashcatcloud/flashduty-sdk"
"github.com/flashcatcloud/go-flashduty"
"github.com/spf13/cobra"

"github.com/flashcatcloud/flashduty-cli/internal/output"
Expand All @@ -26,7 +27,7 @@ func newAlertCmd() *cobra.Command {
}

func newAlertListCmd() *cobra.Command {
var severity, channel, title, since, until string
var severity, channel, since, until string
var active, recovered, muted bool
var limit, page int

Expand All @@ -48,49 +49,50 @@ func newAlertListCmd() *cobra.Command {
return fmt.Errorf("invalid --until: %w", err)
}

input := &flashduty.ListAlertsInput{
req := &flashduty.AlertListRequest{
StartTime: startTime,
EndTime: endTime,
AlertSeverity: severity,
Title: title,
Limit: limit,
Page: page,
}
req.Limit = limit
req.Page = page

// Preserve legacy semantics: --active sends is_active=true,
// --recovered sends is_active=false, neither omits the filter.
if active {
input.IsActive = boolPtr(true)
req.IsActive = flashduty.Bool(true)
} else if recovered {
input.IsActive = boolPtr(false)
req.IsActive = flashduty.Bool(false)
}

if muted {
input.EverMuted = boolPtr(true)
req.EverMuted = flashduty.Bool(true)
}

if channel != "" {
channelIDs, err := parseIntSlice(channel)
if err != nil {
return fmt.Errorf("invalid --channel: %w", err)
}
input.ChannelIDs = channelIDs
req.ChannelIDs = channelIDs
}

result, err := ctx.Client.ListAlerts(cmdContext(ctx.Cmd), input)
result, _, err := ctx.Client.Alerts.ReadList(cmdContext(ctx.Cmd), req)
if err != nil {
return err
}

cols := []output.Column{
{Header: "ID", Field: func(v any) string { return v.(flashduty.Alert).AlertID }},
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.Alert).Title }},
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.Alert).AlertSeverity }},
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.Alert).AlertStatus }},
{Header: "EVENTS", Field: func(v any) string { return fmt.Sprintf("%d", v.(flashduty.Alert).EventCnt) }},
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.Alert).ChannelName }},
{Header: "STARTED", Field: func(v any) string { return output.FormatTime(v.(flashduty.Alert).StartTime) }},
{Header: "ID", Field: func(v any) string { return v.(flashduty.AlertItem).AlertID }},
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertItem).Title }},
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertItem).AlertSeverity }},
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertItem).AlertStatus }},
{Header: "EVENTS", Field: func(v any) string { return fmt.Sprintf("%d", v.(flashduty.AlertItem).EventCnt) }},
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.AlertItem).ChannelName }},
{Header: "STARTED", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertItem).StartTime) }},
}

return ctx.PrintList(result.Alerts, cols, len(result.Alerts), page, result.Total)
return ctx.PrintList(result.Items, cols, len(result.Items), page, int(result.Total))
})
},
}
Expand All @@ -100,7 +102,6 @@ func newAlertListCmd() *cobra.Command {
cmd.Flags().BoolVar(&recovered, "recovered", false, "Show recovered only")
cmd.Flags().StringVar(&channel, "channel", "", "Comma-separated channel IDs")
cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only")
cmd.Flags().StringVar(&title, "title", "", "Search by title keyword")
cmd.Flags().StringVar(&since, "since", "24h", "Start time")
cmd.Flags().StringVar(&until, "until", "now", "End time")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
Expand All @@ -116,32 +117,36 @@ func newAlertGetCmd() *cobra.Command {
Args: requireArgs("alert_id"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCommand(cmd, args, func(ctx *RunContext) error {
result, err := ctx.Client.GetAlertDetail(cmdContext(ctx.Cmd), &flashduty.GetAlertDetailInput{
result, _, err := ctx.Client.Alerts.ReadInfo(cmdContext(ctx.Cmd), &flashduty.AlertInfoRequest{
AlertID: ctx.Args[0],
})
if err != nil {
return err
}

if ctx.Structured() {
return ctx.Printer.Print(result.Alert, nil)
return ctx.Printer.Print(result, nil)
}

printAlertDetail(ctx.Writer, result.Alert)
printAlertDetail(ctx.Writer, result)
return nil
})
},
}
}

func printAlertDetail(w io.Writer, a flashduty.Alert) {
func printAlertDetail(w io.Writer, a *flashduty.AlertItem) {
if a == nil {
return
}

labels := make([]string, 0, len(a.Labels))
for k, v := range a.Labels {
labels = append(labels, k+"="+v)
}

incidentInfo := "-"
if a.Incident != nil {
if a.Incident.IncidentID != "" {
incidentInfo = fmt.Sprintf("%s (%s)", a.Incident.IncidentID, a.Incident.Progress)
}

Expand Down Expand Up @@ -174,27 +179,27 @@ func newAlertEventsCmd() *cobra.Command {
Args: requireArgs("alert_id"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCommand(cmd, args, func(ctx *RunContext) error {
result, err := ctx.Client.ListAlertEvents(cmdContext(ctx.Cmd), &flashduty.ListAlertEventsInput{
result, _, err := ctx.Client.Alerts.ReadEventList(cmdContext(ctx.Cmd), &flashduty.AlertEventListRequest{
AlertID: ctx.Args[0],
})
if err != nil {
return err
}

if len(result.AlertEvents) == 0 {
if len(result.Items) == 0 {
ctx.WriteResult("No alert events found.")
return nil
}

cols := []output.Column{
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).EventID }},
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEvent).EventSeverity }},
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEvent).EventStatus }},
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEvent).EventTime) }},
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEvent).Title }},
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventID }},
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventSeverity }},
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventStatus }},
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEventItem).EventTime) }},
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEventItem).Title }},
}

return ctx.PrintTotal(result.AlertEvents, cols, len(result.AlertEvents))
return ctx.PrintTotal(result.Items, cols, len(result.Items))
})
},
}
Expand All @@ -209,11 +214,11 @@ func newAlertTimelineCmd() *cobra.Command {
Args: requireArgs("alert_id"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCommand(cmd, args, func(ctx *RunContext) error {
result, err := ctx.Client.GetAlertFeed(cmdContext(ctx.Cmd), &flashduty.GetAlertFeedInput{
AlertID: ctx.Args[0],
Limit: limit,
Page: page,
})
req := &flashduty.AlertFeedRequest{AlertID: ctx.Args[0]}
req.Limit = limit
req.Page = page

result, _, err := ctx.Client.Alerts.ReadFeed(cmdContext(ctx.Cmd), req)
if err != nil {
return err
}
Expand All @@ -223,12 +228,28 @@ func newAlertTimelineCmd() *cobra.Command {
return nil
}

// go-flashduty returns raw feed items, so replicate the legacy
// SDK's operator-name enrichment by resolving each entry's actor
// (creator) person ID via /person/infos. Best-effort: the
// OPERATOR column falls back to the numeric ID when a name can't
// be resolved.
nameByID := resolveAlertFeedOperators(ctx, result.Items)

cols := []output.Column{
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.TimelineEvent).Timestamp) }},
{Header: "TYPE", Field: func(v any) string { return v.(flashduty.TimelineEvent).Type }},
{Header: "OPERATOR", Field: func(v any) string { return v.(flashduty.TimelineEvent).OperatorName }},
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.FeedItem).CreatedAt) }},
{Header: "TYPE", Field: func(v any) string { return string(v.(flashduty.FeedItem).Type) }},
{Header: "OPERATOR", Field: func(v any) string {
it := v.(flashduty.FeedItem)
if it.CreatorID == 0 {
return "system"
}
if n, ok := nameByID[it.CreatorID]; ok && n != "" {
return n
}
return strconv.FormatInt(it.CreatorID, 10)
}},
{Header: "DETAIL", MaxWidth: 80, Field: func(v any) string {
d := v.(flashduty.TimelineEvent).Detail
d := v.(flashduty.FeedItem).Detail
if d == nil {
return "-"
}
Expand All @@ -247,6 +268,37 @@ func newAlertTimelineCmd() *cobra.Command {
return cmd
}

// resolveAlertFeedOperators resolves the actor (creator) person IDs of
// alert-feed items to display names via /person/infos, replicating the
// operator-name enrichment the legacy SDK did server-side. Best-effort: a
// lookup failure yields a nil map and callers fall back to the numeric ID.
func resolveAlertFeedOperators(rc *RunContext, items []flashduty.FeedItem) map[int64]string {
seen := make(map[int64]struct{}, len(items))
ids := make([]uint64, 0, len(items))
for _, it := range items {
if it.CreatorID == 0 {
continue
}
if _, ok := seen[it.CreatorID]; ok {
continue
}
seen[it.CreatorID] = struct{}{}
ids = append(ids, uint64(it.CreatorID))
}
if len(ids) == 0 {
return nil
}
resp, _, err := rc.Client.Members.PersonInfos(cmdContext(rc.Cmd), &flashduty.PersonInfosRequest{PersonIDs: ids})
if err != nil || resp == nil {
return nil
}
out := make(map[int64]string, len(resp.Items))
for _, p := range resp.Items {
out[int64(p.PersonID)] = p.PersonName
}
return out
}

func newAlertMergeCmd() *cobra.Command {
var incidentID, comment string

Expand All @@ -256,7 +308,7 @@ func newAlertMergeCmd() *cobra.Command {
Args: requireArgs("alert_id"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCommand(cmd, args, func(ctx *RunContext) error {
if err := ctx.Client.MergeAlertsToIncident(cmdContext(ctx.Cmd), &flashduty.MergeAlertsInput{
if _, err := ctx.Client.Alerts.WriteMerge(cmdContext(ctx.Cmd), &flashduty.AlertMergeRequest{
AlertIDs: ctx.Args,
IncidentID: incidentID,
Comment: comment,
Expand Down
28 changes: 15 additions & 13 deletions internal/cli/alert_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package cli

import (
"fmt"
"strings"

flashduty "github.com/flashcatcloud/flashduty-sdk"
"github.com/flashcatcloud/go-flashduty"
"github.com/spf13/cobra"

"github.com/flashcatcloud/flashduty-cli/internal/output"
Expand Down Expand Up @@ -37,15 +38,16 @@ func newAlertEventListCmd() *cobra.Command {
return fmt.Errorf("invalid --until: %w", err)
}

input := &flashduty.ListAlertEventsGlobalInput{
input := &flashduty.AlertEventGlobalListRequest{
StartTime: startTime,
EndTime: endTime,
Limit: limit,
Page: page,
}
input.Limit = limit
input.Page = page

if severity != "" {
input.Severities = parseStringSlice(severity)
// go-flashduty takes severities as a comma-separated string.
input.Severities = strings.Join(parseStringSlice(severity), ",")
}

if channel != "" {
Expand All @@ -60,21 +62,21 @@ func newAlertEventListCmd() *cobra.Command {
input.IntegrationTypes = parseStringSlice(integrationType)
}

result, err := ctx.Client.ListAlertEventsGlobal(cmdContext(ctx.Cmd), input)
result, _, err := ctx.Client.Alerts.EventReadList(cmdContext(ctx.Cmd), input)
if err != nil {
return err
}

cols := []output.Column{
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).EventID }},
{Header: "ALERT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).AlertID }},
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEvent).EventSeverity }},
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEvent).EventStatus }},
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEvent).EventTime) }},
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEvent).Title }},
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventID }},
{Header: "ALERT_ID", Field: func(v any) string { return v.(flashduty.AlertEventItem).AlertID }},
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventSeverity }},
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventStatus }},
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEventItem).EventTime) }},
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEventItem).Title }},
}

return ctx.PrintList(result.AlertEvents, cols, len(result.AlertEvents), page, result.Total)
return ctx.PrintList(result.Items, cols, len(result.Items), page, int(result.Total))
})
},
}
Expand Down
Loading
Loading