Skip to content

Commit d3ea908

Browse files
authored
feat: migrate CLI onto generated go-flashduty SDK, deprecate flashduty-sdk (#24)
Fully migrate every command from the hand-written flashduty-sdk to the generated go-flashduty SDK (v0.5.2). e2e-validated against a live backend across all command groups + via the safari ai-sre agent.
1 parent 5595294 commit d3ea908

41 files changed

Lines changed: 2173 additions & 1784 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/flashduty-sdk v0.9.1
6+
github.com/flashcatcloud/go-flashduty v0.5.2
77
github.com/mattn/go-runewidth v0.0.23
88
github.com/spf13/cobra v1.10.2
99
github.com/spf13/pflag v1.0.10
10+
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c
1011
golang.org/x/term v0.42.0
1112
gopkg.in/yaml.v3 v3.0.1
1213
)
1314

1415
require (
1516
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
1617
github.com/inconshreveable/mousetrap v1.1.0 // indirect
17-
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c // indirect
18-
golang.org/x/sync v0.19.0 // indirect
1918
golang.org/x/sys v0.43.0 // indirect
2019
)

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
22
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4-
github.com/flashcatcloud/flashduty-sdk v0.9.1 h1:vDTkSjAJJD6Ex5r7S+VCxPi4yxSFNw1bU/SfoRCvk+k=
5-
github.com/flashcatcloud/flashduty-sdk v0.9.1/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
4+
github.com/flashcatcloud/go-flashduty v0.5.2 h1:mYg/M0jqkil30WTLdICVtTJVGxEIGmae/3zBpRkwLRQ=
5+
github.com/flashcatcloud/go-flashduty v0.5.2/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
66
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
77
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
88
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
@@ -16,8 +16,6 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
1616
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c h1:D8lDFovBMZywze1eh9iwMLcYor5f11mHBocLhO7cBe8=
1717
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c/go.mod h1:j/BOnpF2ihnz4lELs99h9mwGJBx/zdleOUCnLLRPCsc=
1818
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
19-
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
20-
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
2119
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
2220
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
2321
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=

internal/cli/alert.go

Lines changed: 95 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package cli
33
import (
44
"fmt"
55
"io"
6+
"strconv"
67
"strings"
78

8-
flashduty "github.com/flashcatcloud/flashduty-sdk"
9+
"github.com/flashcatcloud/go-flashduty"
910
"github.com/spf13/cobra"
1011

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

2829
func newAlertListCmd() *cobra.Command {
29-
var severity, channel, title, since, until string
30+
var severity, channel, since, until string
3031
var active, recovered, muted bool
3132
var limit, page int
3233

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

51-
input := &flashduty.ListAlertsInput{
52+
req := &flashduty.AlertListRequest{
5253
StartTime: startTime,
5354
EndTime: endTime,
5455
AlertSeverity: severity,
55-
Title: title,
56-
Limit: limit,
57-
Page: page,
5856
}
57+
req.Limit = limit
58+
req.Page = page
5959

60+
// Preserve legacy semantics: --active sends is_active=true,
61+
// --recovered sends is_active=false, neither omits the filter.
6062
if active {
61-
input.IsActive = boolPtr(true)
63+
req.IsActive = flashduty.Bool(true)
6264
} else if recovered {
63-
input.IsActive = boolPtr(false)
65+
req.IsActive = flashduty.Bool(false)
6466
}
6567

6668
if muted {
67-
input.EverMuted = boolPtr(true)
69+
req.EverMuted = flashduty.Bool(true)
6870
}
6971

7072
if channel != "" {
7173
channelIDs, err := parseIntSlice(channel)
7274
if err != nil {
7375
return fmt.Errorf("invalid --channel: %w", err)
7476
}
75-
input.ChannelIDs = channelIDs
77+
req.ChannelIDs = channelIDs
7678
}
7779

78-
result, err := ctx.Client.ListAlerts(cmdContext(ctx.Cmd), input)
80+
result, _, err := ctx.Client.Alerts.ReadList(cmdContext(ctx.Cmd), req)
7981
if err != nil {
8082
return err
8183
}
8284

8385
cols := []output.Column{
84-
{Header: "ID", Field: func(v any) string { return v.(flashduty.Alert).AlertID }},
85-
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.Alert).Title }},
86-
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.Alert).AlertSeverity }},
87-
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.Alert).AlertStatus }},
88-
{Header: "EVENTS", Field: func(v any) string { return fmt.Sprintf("%d", v.(flashduty.Alert).EventCnt) }},
89-
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.Alert).ChannelName }},
90-
{Header: "STARTED", Field: func(v any) string { return output.FormatTime(v.(flashduty.Alert).StartTime) }},
86+
{Header: "ID", Field: func(v any) string { return v.(flashduty.AlertItem).AlertID }},
87+
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertItem).Title }},
88+
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertItem).AlertSeverity }},
89+
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertItem).AlertStatus }},
90+
{Header: "EVENTS", Field: func(v any) string { return fmt.Sprintf("%d", v.(flashduty.AlertItem).EventCnt) }},
91+
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.AlertItem).ChannelName }},
92+
{Header: "STARTED", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertItem).StartTime) }},
9193
}
9294

93-
return ctx.PrintList(result.Alerts, cols, len(result.Alerts), page, result.Total)
95+
return ctx.PrintList(result.Items, cols, len(result.Items), page, int(result.Total))
9496
})
9597
},
9698
}
@@ -100,7 +102,6 @@ func newAlertListCmd() *cobra.Command {
100102
cmd.Flags().BoolVar(&recovered, "recovered", false, "Show recovered only")
101103
cmd.Flags().StringVar(&channel, "channel", "", "Comma-separated channel IDs")
102104
cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only")
103-
cmd.Flags().StringVar(&title, "title", "", "Search by title keyword")
104105
cmd.Flags().StringVar(&since, "since", "24h", "Start time")
105106
cmd.Flags().StringVar(&until, "until", "now", "End time")
106107
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
@@ -116,32 +117,36 @@ func newAlertGetCmd() *cobra.Command {
116117
Args: requireArgs("alert_id"),
117118
RunE: func(cmd *cobra.Command, args []string) error {
118119
return runCommand(cmd, args, func(ctx *RunContext) error {
119-
result, err := ctx.Client.GetAlertDetail(cmdContext(ctx.Cmd), &flashduty.GetAlertDetailInput{
120+
result, _, err := ctx.Client.Alerts.ReadInfo(cmdContext(ctx.Cmd), &flashduty.AlertInfoRequest{
120121
AlertID: ctx.Args[0],
121122
})
122123
if err != nil {
123124
return err
124125
}
125126

126127
if ctx.Structured() {
127-
return ctx.Printer.Print(result.Alert, nil)
128+
return ctx.Printer.Print(result, nil)
128129
}
129130

130-
printAlertDetail(ctx.Writer, result.Alert)
131+
printAlertDetail(ctx.Writer, result)
131132
return nil
132133
})
133134
},
134135
}
135136
}
136137

137-
func printAlertDetail(w io.Writer, a flashduty.Alert) {
138+
func printAlertDetail(w io.Writer, a *flashduty.AlertItem) {
139+
if a == nil {
140+
return
141+
}
142+
138143
labels := make([]string, 0, len(a.Labels))
139144
for k, v := range a.Labels {
140145
labels = append(labels, k+"="+v)
141146
}
142147

143148
incidentInfo := "-"
144-
if a.Incident != nil {
149+
if a.Incident.IncidentID != "" {
145150
incidentInfo = fmt.Sprintf("%s (%s)", a.Incident.IncidentID, a.Incident.Progress)
146151
}
147152

@@ -174,27 +179,27 @@ func newAlertEventsCmd() *cobra.Command {
174179
Args: requireArgs("alert_id"),
175180
RunE: func(cmd *cobra.Command, args []string) error {
176181
return runCommand(cmd, args, func(ctx *RunContext) error {
177-
result, err := ctx.Client.ListAlertEvents(cmdContext(ctx.Cmd), &flashduty.ListAlertEventsInput{
182+
result, _, err := ctx.Client.Alerts.ReadEventList(cmdContext(ctx.Cmd), &flashduty.AlertEventListRequest{
178183
AlertID: ctx.Args[0],
179184
})
180185
if err != nil {
181186
return err
182187
}
183188

184-
if len(result.AlertEvents) == 0 {
189+
if len(result.Items) == 0 {
185190
ctx.WriteResult("No alert events found.")
186191
return nil
187192
}
188193

189194
cols := []output.Column{
190-
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).EventID }},
191-
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEvent).EventSeverity }},
192-
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEvent).EventStatus }},
193-
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEvent).EventTime) }},
194-
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEvent).Title }},
195+
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventID }},
196+
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventSeverity }},
197+
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventStatus }},
198+
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEventItem).EventTime) }},
199+
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEventItem).Title }},
195200
}
196201

197-
return ctx.PrintTotal(result.AlertEvents, cols, len(result.AlertEvents))
202+
return ctx.PrintTotal(result.Items, cols, len(result.Items))
198203
})
199204
},
200205
}
@@ -209,11 +214,11 @@ func newAlertTimelineCmd() *cobra.Command {
209214
Args: requireArgs("alert_id"),
210215
RunE: func(cmd *cobra.Command, args []string) error {
211216
return runCommand(cmd, args, func(ctx *RunContext) error {
212-
result, err := ctx.Client.GetAlertFeed(cmdContext(ctx.Cmd), &flashduty.GetAlertFeedInput{
213-
AlertID: ctx.Args[0],
214-
Limit: limit,
215-
Page: page,
216-
})
217+
req := &flashduty.AlertFeedRequest{AlertID: ctx.Args[0]}
218+
req.Limit = limit
219+
req.Page = page
220+
221+
result, _, err := ctx.Client.Alerts.ReadFeed(cmdContext(ctx.Cmd), req)
217222
if err != nil {
218223
return err
219224
}
@@ -223,12 +228,28 @@ func newAlertTimelineCmd() *cobra.Command {
223228
return nil
224229
}
225230

231+
// go-flashduty returns raw feed items, so replicate the legacy
232+
// SDK's operator-name enrichment by resolving each entry's actor
233+
// (creator) person ID via /person/infos. Best-effort: the
234+
// OPERATOR column falls back to the numeric ID when a name can't
235+
// be resolved.
236+
nameByID := resolveAlertFeedOperators(ctx, result.Items)
237+
226238
cols := []output.Column{
227-
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.TimelineEvent).Timestamp) }},
228-
{Header: "TYPE", Field: func(v any) string { return v.(flashduty.TimelineEvent).Type }},
229-
{Header: "OPERATOR", Field: func(v any) string { return v.(flashduty.TimelineEvent).OperatorName }},
239+
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.FeedItem).CreatedAt) }},
240+
{Header: "TYPE", Field: func(v any) string { return string(v.(flashduty.FeedItem).Type) }},
241+
{Header: "OPERATOR", Field: func(v any) string {
242+
it := v.(flashduty.FeedItem)
243+
if it.CreatorID == 0 {
244+
return "system"
245+
}
246+
if n, ok := nameByID[it.CreatorID]; ok && n != "" {
247+
return n
248+
}
249+
return strconv.FormatInt(it.CreatorID, 10)
250+
}},
230251
{Header: "DETAIL", MaxWidth: 80, Field: func(v any) string {
231-
d := v.(flashduty.TimelineEvent).Detail
252+
d := v.(flashduty.FeedItem).Detail
232253
if d == nil {
233254
return "-"
234255
}
@@ -247,6 +268,37 @@ func newAlertTimelineCmd() *cobra.Command {
247268
return cmd
248269
}
249270

271+
// resolveAlertFeedOperators resolves the actor (creator) person IDs of
272+
// alert-feed items to display names via /person/infos, replicating the
273+
// operator-name enrichment the legacy SDK did server-side. Best-effort: a
274+
// lookup failure yields a nil map and callers fall back to the numeric ID.
275+
func resolveAlertFeedOperators(rc *RunContext, items []flashduty.FeedItem) map[int64]string {
276+
seen := make(map[int64]struct{}, len(items))
277+
ids := make([]uint64, 0, len(items))
278+
for _, it := range items {
279+
if it.CreatorID == 0 {
280+
continue
281+
}
282+
if _, ok := seen[it.CreatorID]; ok {
283+
continue
284+
}
285+
seen[it.CreatorID] = struct{}{}
286+
ids = append(ids, uint64(it.CreatorID))
287+
}
288+
if len(ids) == 0 {
289+
return nil
290+
}
291+
resp, _, err := rc.Client.Members.PersonInfos(cmdContext(rc.Cmd), &flashduty.PersonInfosRequest{PersonIDs: ids})
292+
if err != nil || resp == nil {
293+
return nil
294+
}
295+
out := make(map[int64]string, len(resp.Items))
296+
for _, p := range resp.Items {
297+
out[int64(p.PersonID)] = p.PersonName
298+
}
299+
return out
300+
}
301+
250302
func newAlertMergeCmd() *cobra.Command {
251303
var incidentID, comment string
252304

@@ -256,7 +308,7 @@ func newAlertMergeCmd() *cobra.Command {
256308
Args: requireArgs("alert_id"),
257309
RunE: func(cmd *cobra.Command, args []string) error {
258310
return runCommand(cmd, args, func(ctx *RunContext) error {
259-
if err := ctx.Client.MergeAlertsToIncident(cmdContext(ctx.Cmd), &flashduty.MergeAlertsInput{
311+
if _, err := ctx.Client.Alerts.WriteMerge(cmdContext(ctx.Cmd), &flashduty.AlertMergeRequest{
260312
AlertIDs: ctx.Args,
261313
IncidentID: incidentID,
262314
Comment: comment,

internal/cli/alert_event.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package cli
22

33
import (
44
"fmt"
5+
"strings"
56

6-
flashduty "github.com/flashcatcloud/flashduty-sdk"
7+
"github.com/flashcatcloud/go-flashduty"
78
"github.com/spf13/cobra"
89

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

40-
input := &flashduty.ListAlertEventsGlobalInput{
41+
input := &flashduty.AlertEventGlobalListRequest{
4142
StartTime: startTime,
4243
EndTime: endTime,
43-
Limit: limit,
44-
Page: page,
4544
}
45+
input.Limit = limit
46+
input.Page = page
4647

4748
if severity != "" {
48-
input.Severities = parseStringSlice(severity)
49+
// go-flashduty takes severities as a comma-separated string.
50+
input.Severities = strings.Join(parseStringSlice(severity), ",")
4951
}
5052

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

63-
result, err := ctx.Client.ListAlertEventsGlobal(cmdContext(ctx.Cmd), input)
65+
result, _, err := ctx.Client.Alerts.EventReadList(cmdContext(ctx.Cmd), input)
6466
if err != nil {
6567
return err
6668
}
6769

6870
cols := []output.Column{
69-
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).EventID }},
70-
{Header: "ALERT_ID", Field: func(v any) string { return v.(flashduty.AlertEvent).AlertID }},
71-
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEvent).EventSeverity }},
72-
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEvent).EventStatus }},
73-
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEvent).EventTime) }},
74-
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEvent).Title }},
71+
{Header: "EVENT_ID", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventID }},
72+
{Header: "ALERT_ID", Field: func(v any) string { return v.(flashduty.AlertEventItem).AlertID }},
73+
{Header: "SEVERITY", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventSeverity }},
74+
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.AlertEventItem).EventStatus }},
75+
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.AlertEventItem).EventTime) }},
76+
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.AlertEventItem).Title }},
7577
}
7678

77-
return ctx.PrintList(result.AlertEvents, cols, len(result.AlertEvents), page, result.Total)
79+
return ctx.PrintList(result.Items, cols, len(result.Items), page, int(result.Total))
7880
})
7981
},
8082
}

0 commit comments

Comments
 (0)