Skip to content

Commit a42780c

Browse files
committed
feat: sync with go-flashduty v0.14.4 (corrected OpenAPI surface)
- Bump go-flashduty to v0.14.4 and regenerate all commands. - Downloads are real now: operations returning a bounded binary body (CSV exports, enrichment mapping download, RUM issue export) generate commands that stream Response.Raw, so 'fduty insight channel-export > out.csv' works. cligen's streaming exclusion now covers only ndjson streams, matching the SDK generator. - Multipart uploads are curated: 'enrichment mapping-data-upload' and 'safari skill-upload' take --file (path or -) and ride the SDK's hand-written upload methods. The previously generated upload command could not send a file at all. - cligen renders nullable union types ([T, null]) by their non-null member at every classification site (new typeStr helper); nullable arrays previously showed as 'any' and their children never expanded. - Add claimStdin so --file - keeps the single-consumer stdin guard. - Retire the retiredOperationIDs bridge: the linked spec no longer carries those operations. - Fix curated call sites for the corrected SDK types (pointer time filters, nullable group name, MCP server item without tools).
1 parent a1c6bfe commit a42780c

67 files changed

Lines changed: 2901 additions & 2806 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/go-flashduty v0.14.2
6+
github.com/flashcatcloud/go-flashduty v0.14.4
77
github.com/mattn/go-runewidth v0.0.28
88
github.com/spf13/cobra v1.10.2
99
github.com/spf13/pflag v1.0.10

go.sum

Lines changed: 2 additions & 2 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/go-flashduty v0.14.2 h1:SIF8AMoaF67eEGAiiq8e4y5Dvv/0OOA/VzoxYpDB1CE=
5-
github.com/flashcatcloud/go-flashduty v0.14.2/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
4+
github.com/flashcatcloud/go-flashduty v0.14.4 h1:b+Hrkt631lP41o5M4fqxgLZPGd85CHvCnch1qv3Dj+E=
5+
github.com/flashcatcloud/go-flashduty v0.14.4/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
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.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=

internal/cli/alert_event.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ func newAlertEventListCmd() *cobra.Command {
4141
}
4242

4343
input := &flashduty.AlertEventGlobalListRequest{
44-
StartTime: startTime,
45-
EndTime: endTime,
44+
StartTime: flashduty.Int64(startTime),
45+
EndTime: flashduty.Int64(endTime),
4646
}
4747
input.Limit = limit
4848
input.Page = page

internal/cli/coverage_test.go

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,27 @@ import (
1515
type specOpMeta struct {
1616
id string
1717
path string
18-
streaming bool // 200 body is not application/json (e.g. application/x-ndjson)
18+
streaming bool // 200 body is application/x-ndjson with no JSON variant
1919
}
2020

2121
var curatedOperationIDs = map[string]bool{
2222
// Uses a path parameter plus bearer trigger token instead of the app_key
2323
// generated command runtime. Served by a hand-written path command.
2424
"automation-trigger-write-fire": true,
25-
}
26-
27-
// retiredOperationIDs bridges the release window where the server-side route
28-
// is gone but the CLI still compiles against the previous released SDK spec.
29-
var retiredOperationIDs = map[string]bool{
30-
"monit-preview-sync": true,
31-
"monit-read-query-rows": true,
32-
"monit-rule-write-status": true,
25+
// Multipart/form-data file uploads cannot be assembled from --data/flags;
26+
// both are served by curated commands built on the hand-written SDK
27+
// upload methods (see upload.go).
28+
"mapping-data-write-upload": true,
29+
"skill-write-upload": true,
3330
}
3431

3532
// loadSpecOps reads every public GET/POST operation from the openapi spec
3633
// shipped in the linked go-flashduty module — the same spec cligen generates
37-
// against — recording each op's id, path, and whether its 200 response is a
38-
// non-JSON streaming body. Streaming ops are served by curated commands (the
39-
// generated typed-response template cannot model an io.ReadCloser), so the
40-
// generator-coverage check excludes them.
34+
// against — recording each op's id, path, and whether its 200 response is an
35+
// ndjson stream. Streaming ops are served by curated commands (the generated
36+
// typed-response template cannot model an io.ReadCloser), so the
37+
// generator-coverage check excludes them. Bounded binary downloads (csv,
38+
// octet-stream) are NOT streams: the generated command streams Response.Raw.
4139
func loadSpecOps(t *testing.T) []specOpMeta {
4240
t.Helper()
4341
out, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/flashcatcloud/go-flashduty").Output()
@@ -73,29 +71,16 @@ func loadSpecOps(t *testing.T) []specOpMeta {
7371
}
7472
streaming := false
7573
if resp, ok := op.Responses["200"]; ok && len(resp.Content) > 0 {
76-
if _, hasJSON := resp.Content["application/json"]; !hasJSON {
77-
streaming = true
78-
}
74+
_, hasJSON := resp.Content["application/json"]
75+
_, hasNDJSON := resp.Content["application/x-ndjson"]
76+
streaming = !hasJSON && hasNDJSON
7977
}
8078
ops = append(ops, specOpMeta{id: op.OperationID, path: path, streaming: streaming})
8179
}
8280
}
8381
return ops
8482
}
8583

86-
// loadSpecPaths returns operationId -> path for every public GET/POST operation.
87-
func loadSpecPaths(t *testing.T) map[string]string {
88-
t.Helper()
89-
ids := map[string]string{}
90-
for _, op := range loadSpecOps(t) {
91-
if retiredOperationIDs[op.id] {
92-
continue
93-
}
94-
ids[op.id] = op.path
95-
}
96-
return ids
97-
}
98-
9984
// pathCommand derives the path-is-king command for an API path: the first
10085
// segment is the group, the remaining segments hyphen-join into the verb. This
10186
// mirrors cligen's cliGroup/cliVerb (spec path segments are already kebab-case,
@@ -139,7 +124,10 @@ func leafCommandPaths() map[string]bool {
139124
// the operation without guessing — generated commands provide the path-name and
140125
// curated commands win the exact name when they already own it.
141126
func TestEveryOperationHasPathCommand(t *testing.T) {
142-
specPaths := loadSpecPaths(t)
127+
specPaths := map[string]string{}
128+
for _, op := range loadSpecOps(t) {
129+
specPaths[op.id] = op.path
130+
}
143131
leaves := leafCommandPaths()
144132
var missing []string
145133
for _, apiPath := range specPaths {
@@ -157,21 +145,18 @@ func TestEveryOperationHasPathCommand(t *testing.T) {
157145

158146
// TestGeneratorTargetsFullSpec asserts the generator emitted a command for every
159147
// non-streaming spec operation (no gaps, no phantom manifest entries from a
160-
// stale run). Streaming ops (200 body is not application/json) are deliberately
148+
// stale run). Streaming ops (200 body is application/x-ndjson) are deliberately
161149
// excluded from generation — they cannot be modeled by the typed-response
162150
// template and are served by curated commands instead — so the manifest must NOT
163151
// contain them and they are not required to be generated. A small number of
164152
// non-streaming operations can also be curated when the generated app_key
165-
// runtime cannot model their auth or path shape.
153+
// runtime cannot model their auth, path, or multipart-body shape.
166154
func TestGeneratorTargetsFullSpec(t *testing.T) {
167155
ops := loadSpecOps(t)
168156
streaming := map[string]bool{}
169157
curated := map[string]bool{}
170158
wantGenerated := map[string]bool{}
171159
for _, op := range ops {
172-
if retiredOperationIDs[op.id] {
173-
continue
174-
}
175160
if op.streaming {
176161
streaming[op.id] = true
177162
continue
@@ -186,13 +171,12 @@ func TestGeneratorTargetsFullSpec(t *testing.T) {
186171
gen := map[string]bool{}
187172
for _, id := range generatedOpIDs {
188173
gen[id] = true
189-
if streaming[id] {
174+
switch {
175+
case streaming[id]:
190176
t.Errorf("manifest op %q is streaming and must not be generated (curated only)", id)
191-
}
192-
if curated[id] {
177+
case curated[id]:
193178
t.Errorf("manifest op %q is curated and must not be generated", id)
194-
}
195-
if !wantGenerated[id] && !streaming[id] && !curated[id] {
179+
case !wantGenerated[id]:
196180
t.Errorf("manifest op %q is not in the current spec (regenerate cligen)", id)
197181
}
198182
}

internal/cli/gen_support.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,30 @@ var stdinReader io.Reader = os.Stdin
3131
// the conflict.
3232
var stdinConsumedBy string
3333

34+
// claimStdin marks stdin as consumed by flag and returns the reader WITHOUT
35+
// reading it — for callers that stream (e.g. multipart upload), where
36+
// buffering the whole stream the way readStdin does would defeat the point.
37+
// The single-consumer rule is identical to readStdin's; both funnel through
38+
// this one claim site.
39+
func claimStdin(flag string) (io.Reader, error) {
40+
if stdinConsumedBy != "" {
41+
return nil, fmt.Errorf("only one flag can read from stdin: %s and %s were both set to \"-\"", stdinConsumedBy, flag)
42+
}
43+
stdinConsumedBy = flag
44+
return stdinReader, nil
45+
}
46+
3447
// readStdin is the single function through which every --*-file-style flag's
3548
// "-" value reads stdin. flag is the calling flag's canonical spelling (e.g.
3649
// "--data"), used only to label it in the already-consumed error below. A
3750
// second flag trying to read stdin after an earlier one already claimed it
3851
// errors instead of silently returning empty bytes.
3952
func readStdin(flag string) ([]byte, error) {
40-
if stdinConsumedBy != "" {
41-
return nil, fmt.Errorf("only one flag can read from stdin: %s and %s were both set to \"-\"", stdinConsumedBy, flag)
42-
}
43-
b, err := io.ReadAll(stdinReader)
53+
r, err := claimStdin(flag)
4454
if err != nil {
4555
return nil, err
4656
}
47-
stdinConsumedBy = flag
48-
return b, nil
57+
return io.ReadAll(r)
4958
}
5059

5160
// readPathOrStdin reads the raw bytes at path, or from stdin (via readStdin)

internal/cli/generic_table_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func TestRenderGenericTable_DetailVertical(t *testing.T) {
144144
}
145145
}
146146

147-
func TestRenderGenericTable_McpServerItemWithEmptyToolsRendersDetail(t *testing.T) {
147+
func TestRenderGenericTable_McpServerItemRendersDetail(t *testing.T) {
148148
var buf bytes.Buffer
149149
item := &flashduty.McpServerItem{
150150
ServerID: "mcp_test",
@@ -154,14 +154,13 @@ func TestRenderGenericTable_McpServerItemWithEmptyToolsRendersDetail(t *testing.
154154
Status: "enabled",
155155
Transport: "streamable-http",
156156
URL: "https://mcp.example.com/github",
157-
Tools: []flashduty.McpToolInfo{},
158157
}
159158
if err := renderGenericTable(tableCtx(&buf), item); err != nil {
160159
t.Fatalf("render: %v", err)
161160
}
162161
got := buf.String()
163162
if strings.Contains(got, "No results.") {
164-
t.Fatalf("single MCP server item with empty tools was rendered as an empty list:\n%s", got)
163+
t.Fatalf("single MCP server item was rendered as an empty list:\n%s", got)
165164
}
166165
for _, want := range []string{"FIELD", "VALUE", "SERVER_ID", "mcp_test", "SERVER_NAME", "github"} {
167166
if !strings.Contains(got, want) {

internal/cli/oncall.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,7 @@ func newOncallScheduleGetCmd() *cobra.Command {
245245
return output.FormatTime(v.(flashduty.ScheduleCalculatedSchedule).End)
246246
}},
247247
{Header: "GROUP", MaxWidth: 30, Field: func(v any) string {
248-
g := v.(flashduty.ScheduleCalculatedSchedule).Group
249-
if g.GroupName != "" {
250-
return g.GroupName
251-
}
252-
return g.Name
248+
return scheduleGroupName(v.(flashduty.ScheduleCalculatedSchedule).Group)
253249
}},
254250
}
255251

@@ -301,6 +297,18 @@ func scheduleLayerCount(s flashduty.ScheduleItem) string {
301297
}
302298
}
303299

300+
// scheduleGroupName resolves a schedule group's display name: the nullable
301+
// group_name when set, else the legacy name, else a placeholder.
302+
func scheduleGroupName(g flashduty.ScheduleGroup) string {
303+
if g.GroupName != nil && *g.GroupName != "" {
304+
return *g.GroupName
305+
}
306+
if g.Name != "" {
307+
return g.Name
308+
}
309+
return "-"
310+
}
311+
304312
// formatOncallMembers renders an on-call group's members as display names,
305313
// resolving person IDs through nameByID (best-effort, falling back to the
306314
// numeric ID), and finally to the group name when no members are present.
@@ -319,14 +327,7 @@ func formatOncallMembers(oncall *flashduty.ScheduleOncallGroup, nameByID map[int
319327
}
320328
}
321329
if len(names) == 0 {
322-
name := oncall.Group.GroupName
323-
if name == "" {
324-
name = oncall.Group.Name
325-
}
326-
if name != "" {
327-
return name
328-
}
329-
return "-"
330+
return scheduleGroupName(oncall.Group)
330331
}
331332
return strings.Join(names, ", ")
332333
}

internal/cli/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ func init() {
151151
// operation stays reachable at safari session-export.
152152
attachSafariSessionExport(rootCmd)
153153
attachSafariAutomationTriggerFire(rootCmd)
154+
155+
// Multipart uploads are excluded from generation (non-JSON request body);
156+
// attach their curated leaves to the generated path groups.
157+
attachEnrichmentMappingDataUpload(rootCmd)
158+
attachSafariSkillUpload(rootCmd)
154159
}
155160

156161
// Execute runs the root command.

0 commit comments

Comments
 (0)