From 9c218b2f4cc4444288c46ac669392f325accab6e Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 15 Jun 2026 13:29:28 +0800 Subject: [PATCH 1/3] feat(channel): add --team-ids server-side filter to `channel list` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The curated `channel list` exposed only `--name` (a client-side substring filter), shadowing the generated twin that carries the API's `team_ids` filter. So there was no way to ask the server for a single team's channels — callers had to fetch all channels and filter locally, which scales poorly (server default caps the page at 100 rows) and, in fc-safari's /init scanner, made team-scoped incident totals impossible to compute correctly. Wire ListChannelsRequest.TeamIDs to a new --team-ids flag (empty = all teams, behavior unchanged). Intended release: minor bump v1.4.0. --- internal/cli/channel.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/cli/channel.go b/internal/cli/channel.go index bf02020..55e465a 100644 --- a/internal/cli/channel.go +++ b/internal/cli/channel.go @@ -39,19 +39,21 @@ type channelRow struct { func newChannelListCmd() *cobra.Command { var name string + var teamIDs []int64 cmd := &cobra.Command{ Use: "list", Short: "List channels", - Long: curatedLong("List channels in the account, optionally filtered by name.", "Channels", "ChannelList"), + Long: curatedLong("List channels in the account, optionally filtered by name or owning team.", "Channels", "ChannelList"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { // Legacy parity: the hand-written SDK called /channel/list with an // empty body and applied the --name filter client-side as a // case-insensitive substring match. go-flashduty's ChannelName field // is an exact-match server filter, so we keep the client-side filter - // to preserve behavior. - result, _, err := ctx.Client.Channels.ChannelList(cmdContext(ctx.Cmd), &flashduty.ListChannelsRequest{}) + // to preserve behavior. --team-ids, by contrast, is a server-side + // filter on the channel's owning team (empty = all teams, unchanged). + result, _, err := ctx.Client.Channels.ChannelList(cmdContext(ctx.Cmd), &flashduty.ListChannelsRequest{TeamIDs: teamIDs}) if err != nil { return err } @@ -87,6 +89,7 @@ func newChannelListCmd() *cobra.Command { } cmd.Flags().StringVar(&name, "name", "", "Search by name") + cmd.Flags().Int64SliceVar(&teamIDs, "team-ids", nil, "Filter by owning team ID(s), server-side (repeatable or comma-separated)") return cmd } From 0e23f9c62b8c66e9f85a06eb31e99c587b45c900 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 15 Jun 2026 19:45:28 +0800 Subject: [PATCH 2/3] feat(cli): drop transparent curated ID-verb shadows (parity via #50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the curated shadow commands whose generated twins now have full parity after #50 (positional args). genAddLeaf surfaces the generated leaf at the same name once the curated AddCommand is removed. Dropped (same generated name, same SDK endpoint, positional id folds to the *_ids/_id wire field, generated flags ⊇ curated): incident ack POST /incident/ack incident unack POST /incident/unack incident wake POST /incident/wake incident reopen POST /incident/reopen (+--reason, superset) incident disable-merge POST /incident/disable-merge alert merge POST /alert/merge Flag-name delta (for the paired docs migration): alert merge: curated --incident → generated --incident-id (also gains superset --alert-ids/--owner-id/--title) Accepted deltas of converging on the generated single source: - unack/wake/disable-merge lose the curated client-side ≤100-id cap; the backend still enforces it (help: "At most 100 per call"). - alert merge loses MarkFlagRequired("incident"); incident_id stays required server-side. - success line changes from the curated phrase to "OK: POST ". validateIncidentIDBatch is kept (still used by comment/remove, which stay curated for their --comment cap / destructive-confirm). Tests: migrated the unit + e2e assertions for the dropped commands to the generated twins' surface; dropped the unack/wake subtests of the 100-id-cap test (that cap is curated-only and intentionally gone). --- e2e/edge_case_test.go | 3 +- e2e/incident_extended_test.go | 3 +- e2e/incident_test.go | 4 +- internal/cli/alert.go | 33 +------- internal/cli/command_test.go | 15 ++-- internal/cli/gen_positional_test.go | 7 +- internal/cli/incident.go | 127 +--------------------------- 7 files changed, 25 insertions(+), 167 deletions(-) diff --git a/e2e/edge_case_test.go b/e2e/edge_case_test.go index ae56d14..ad9d4cf 100644 --- a/e2e/edge_case_test.go +++ b/e2e/edge_case_test.go @@ -134,10 +134,11 @@ func TestJSONOnAckCommand(t *testing.T) { id := extractIncidentID(t, r.Stdout) t.Cleanup(func() { runCLI(t, "incident", "close", id) }) + // ack is served by the generated twin; --json wraps the OK line as {"message":"..."}. r = runCLI(t, "incident", "ack", id, "--json") requireSuccess(t, r) requireValidJSON(t, r.Stdout) - requireContains(t, r.Stdout, "Acknowledged") + requireContains(t, r.Stdout, "OK: POST /incident/ack") } // Test 307: --json on close command diff --git a/e2e/incident_extended_test.go b/e2e/incident_extended_test.go index 20aa507..96e6660 100644 --- a/e2e/incident_extended_test.go +++ b/e2e/incident_extended_test.go @@ -192,9 +192,10 @@ func TestIncidentAckSingleID(t *testing.T) { id := extractIncidentID(t, r.Stdout) t.Cleanup(func() { runCLI(t, "incident", "close", id) }) + // Served by the generated twin (positional id → incident_ids). r = runCLI(t, "incident", "ack", id) requireSuccess(t, r) - requireContains(t, r.Stdout, "Acknowledged 1 incident(s).") + requireContains(t, r.Stdout, "OK: POST /incident/ack") } // Test 204: close single ID diff --git a/e2e/incident_test.go b/e2e/incident_test.go index 9a9ff1a..2019b47 100644 --- a/e2e/incident_test.go +++ b/e2e/incident_test.go @@ -101,10 +101,10 @@ func TestIncidentLifecycle(t *testing.T) { requireContains(t, r.Stdout, "Triggered") requireContains(t, r.Stdout, name) - // Step 3: Ack + // Step 3: Ack (served by the generated twin; positional id → incident_ids). r = runCLI(t, "incident", "ack", id) requireSuccess(t, r) - requireContains(t, r.Stdout, "Acknowledged 1 incident(s).") + requireContains(t, r.Stdout, "OK: POST /incident/ack") // Step 4: Get - should be Processing r = runCLI(t, "incident", "get", id) diff --git a/internal/cli/alert.go b/internal/cli/alert.go index a08254e..6f21258 100644 --- a/internal/cli/alert.go +++ b/internal/cli/alert.go @@ -22,7 +22,8 @@ func newAlertCmd() *cobra.Command { cmd.AddCommand(newAlertGetCmd()) cmd.AddCommand(newAlertEventsCmd()) cmd.AddCommand(newAlertTimelineCmd()) - cmd.AddCommand(newAlertMergeCmd()) + // merge is registered via the generated layer (positional alert-ids fold to + // alert_ids). Flag-name change: --incident (curated) → --incident-id (generated). return cmd } @@ -303,33 +304,3 @@ func resolveAlertFeedOperators(rc *RunContext, items []flashduty.FeedItem) map[i } return out } - -func newAlertMergeCmd() *cobra.Command { - var incidentID, comment string - - cmd := &cobra.Command{ - Use: "merge [ ...]", - Short: "Merge alerts into an incident", - Args: requireArgs("alert_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Alerts.WriteMerge(cmdContext(ctx.Cmd), &flashduty.AlertMergeRequest{ - AlertIDs: ctx.Args, - IncidentID: incidentID, - Comment: comment, - }); err != nil { - return err - } - - ctx.WriteResult(fmt.Sprintf("Merged %d alert(s) into incident %s.", len(ctx.Args), incidentID)) - return nil - }) - }, - } - - cmd.Flags().StringVar(&incidentID, "incident", "", "Target incident ID") - cmd.Flags().StringVar(&comment, "comment", "", "Merge comment") - _ = cmd.MarkFlagRequired("incident") - - return cmd -} diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go index 6915794..0def636 100644 --- a/internal/cli/command_test.go +++ b/internal/cli/command_test.go @@ -430,6 +430,7 @@ func TestCommandIncidentUnack(t *testing.T) { saveAndResetGlobals(t) stub := newGFStub(t) + // unack is served by the generated twin (positional ids → incident_ids). out, err := execCommand("incident", "unack", "inc-1", "inc-2") if err != nil { t.Fatalf("[incident-unack] unexpected error: %v", err) @@ -440,7 +441,7 @@ func TestCommandIncidentUnack(t *testing.T) { if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want { t.Fatalf("[incident-unack] expected ids %q, got %q", want, got) } - if !strings.Contains(out, "Unacknowledged 2 incident(s).") { + if !strings.Contains(out, "OK: POST /incident/unack") { t.Fatalf("[incident-unack] unexpected output:\n%s", out) } } @@ -449,6 +450,7 @@ func TestCommandIncidentWake(t *testing.T) { saveAndResetGlobals(t) stub := newGFStub(t) + // wake is served by the generated twin (positional id → incident_ids). out, err := execCommand("incident", "wake", "inc-1") if err != nil { t.Fatalf("[incident-wake] unexpected error: %v", err) @@ -459,7 +461,7 @@ func TestCommandIncidentWake(t *testing.T) { if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1"; got != want { t.Fatalf("[incident-wake] expected ids %q, got %q", want, got) } - if !strings.Contains(out, "Restored notifications for 1 incident(s).") { + if !strings.Contains(out, "OK: POST /incident/wake") { t.Fatalf("[incident-wake] unexpected output:\n%s", out) } } @@ -500,13 +502,15 @@ func TestCommandIncidentCommentAllows1024UnicodeRunes(t *testing.T) { } } +// TestCommandIncidentLifecycleRejectsMoreThan100IDs covers the curated +// commands that still enforce the 100-id batch cap client-side. unack and wake +// were dropped in favor of their generated twins, which carry no client-side +// cap (the backend enforces the limit), so they are intentionally absent here. func TestCommandIncidentLifecycleRejectsMoreThan100IDs(t *testing.T) { commands := []struct { name string args []string }{ - {name: "unack", args: []string{"incident", "unack"}}, - {name: "wake", args: []string{"incident", "wake"}}, {name: "comment", args: []string{"incident", "comment", "--comment", "too many"}}, {name: "remove", args: []string{"incident", "remove"}}, } @@ -612,6 +616,7 @@ func TestCommandIncidentDisableMerge(t *testing.T) { saveAndResetGlobals(t) stub := newGFStub(t) + // disable-merge is served by the generated twin (positional ids → incident_ids). out, err := execCommand("incident", "disable-merge", "inc-1", "inc-2") if err != nil { t.Fatalf("[incident-disable-merge] unexpected error: %v", err) @@ -622,7 +627,7 @@ func TestCommandIncidentDisableMerge(t *testing.T) { if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want { t.Fatalf("[incident-disable-merge] expected ids %q, got %q", want, got) } - if !strings.Contains(out, "Disabled auto-merge for 2 incident(s).") { + if !strings.Contains(out, "OK: POST /incident/disable-merge") { t.Fatalf("[incident-disable-merge] unexpected output:\n%s", out) } } diff --git a/internal/cli/gen_positional_test.go b/internal/cli/gen_positional_test.go index d177629..434da0c 100644 --- a/internal/cli/gen_positional_test.go +++ b/internal/cli/gen_positional_test.go @@ -11,9 +11,10 @@ import ( // the variadic marker; an *_id scalar op renders the single id; the override and // int cases render their pinned field. func TestGenPositionalUseLine(t *testing.T) { - // (a) and (b) read the generated constructors directly (the curated commands - // own the live `incident ack`/`incident info` path-names, so the generated - // twins are dropped at registration but still constructible for assertion). + // (a) and (b) call the generated constructors directly so the Use-line + // assertions stay independent of registration order. `incident ack` now + // surfaces the generated twin (curated shadow dropped); `incident info` was + // always generated-only (the curated leaf is named `detail`). ack := genIncidentsAckCmd() if got := ack.Use; got != "ack [...]" { t.Errorf("ack twin Use = %q, want %q", got, "ack [...]") diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 90e4b3a..61515bb 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -28,20 +28,18 @@ func newIncidentCmd() *cobra.Command { cmd.AddCommand(newIncidentGetCmd()) cmd.AddCommand(newIncidentCreateCmd()) cmd.AddCommand(newIncidentUpdateCmd()) - cmd.AddCommand(newIncidentAckCmd()) - cmd.AddCommand(newIncidentUnackCmd()) + // ack, unack, wake, reopen, and disable-merge are registered via the + // generated layer (positional ids fold to incident_ids; flags are a superset + // of the dropped curated shadows). cmd.AddCommand(newIncidentCloseCmd()) - cmd.AddCommand(newIncidentWakeCmd()) cmd.AddCommand(newIncidentTimelineCmd()) cmd.AddCommand(newIncidentAlertsCmd()) cmd.AddCommand(newIncidentSimilarCmd()) cmd.AddCommand(newIncidentMergeCmd()) cmd.AddCommand(newIncidentSnoozeCmd()) - cmd.AddCommand(newIncidentReopenCmd()) cmd.AddCommand(newIncidentReassignCmd()) cmd.AddCommand(newIncidentAddResponderCmd()) cmd.AddCommand(newIncidentCommentCmd()) - cmd.AddCommand(newIncidentDisableMergeCmd()) cmd.AddCommand(newIncidentRemoveCmd()) cmd.AddCommand(newIncidentWarRoomCmd()) cmd.AddCommand(newIncidentFeedCmd()) @@ -441,53 +439,6 @@ func newIncidentUpdateCmd() *cobra.Command { return cmd } -func newIncidentAckCmd() *cobra.Command { - return &cobra.Command{ - Use: "ack [ ...]", - Short: "Acknowledge incidents", - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Ack(cmdContext(ctx.Cmd), &flashduty.AckIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Acknowledged %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - -func newIncidentUnackCmd() *cobra.Command { - return &cobra.Command{ - Use: "unack [ ...]", - Short: "Cancel incident acknowledgement", - Long: `Cancel acknowledgement for one or more incidents. - -Use this when an incident was acknowledged by mistake and should return to the -unacknowledged state. The command accepts up to 100 incident IDs.`, - Example: ` flashduty incident unack inc_123 - flashduty incident unack inc_123 inc_456`, - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateIncidentIDBatch(args); err != nil { - return err - } - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Unack(cmdContext(ctx.Cmd), &flashduty.UnackIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Unacknowledged %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentCloseCmd() *cobra.Command { return &cobra.Command{ Use: "close [ ...]", @@ -507,34 +458,6 @@ func newIncidentCloseCmd() *cobra.Command { } } -func newIncidentWakeCmd() *cobra.Command { - return &cobra.Command{ - Use: "wake [ ...]", - Short: "Restore notifications for snoozed incidents", - Long: `Wake one or more snoozed incidents. - -This cancels snooze and restores normal incident notifications. The command -accepts up to 100 incident IDs.`, - Example: ` flashduty incident wake inc_123 - flashduty incident wake inc_123 inc_456`, - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateIncidentIDBatch(args); err != nil { - return err - } - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Wake(cmdContext(ctx.Cmd), &flashduty.WakeIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Restored notifications for %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentTimelineCmd() *cobra.Command { return &cobra.Command{ Use: "timeline ", @@ -784,25 +707,6 @@ func newIncidentSnoozeCmd() *cobra.Command { return cmd } -func newIncidentReopenCmd() *cobra.Command { - return &cobra.Command{ - Use: "reopen [ ...]", - Short: "Reopen closed incidents", - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.Reopen(cmdContext(ctx.Cmd), &flashduty.ReopenIncidentRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Reopened %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentReassignCmd() *cobra.Command { var person string @@ -952,31 +856,6 @@ webhook reply behavior.`, return cmd } -func newIncidentDisableMergeCmd() *cobra.Command { - return &cobra.Command{ - Use: "disable-merge [ ...]", - Short: "Disable automatic merging for incidents", - Long: `Disable automatic alert merging for one or more incidents. - -Use this when an incident should stay isolated and must not absorb additional -matching alerts automatically. The command accepts up to 100 incident IDs.`, - Example: ` flashduty incident disable-merge inc_123 - flashduty incident disable-merge inc_123 inc_456`, - Args: requireArgs("incident_id"), - RunE: func(cmd *cobra.Command, args []string) error { - return runCommand(cmd, args, func(ctx *RunContext) error { - if _, err := ctx.Client.Incidents.DisableMerge(cmdContext(ctx.Cmd), &flashduty.DisableIncidentMergeRequest{ - IncidentIDs: ctx.Args, - }); err != nil { - return err - } - ctx.WriteResult(fmt.Sprintf("Disabled auto-merge for %d incident(s).", len(ctx.Args))) - return nil - }) - }, - } -} - func newIncidentRemoveCmd() *cobra.Command { var force bool From 7df80f07b47fdf907cc138acf188a2223a381171 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Tue, 16 Jun 2026 12:17:41 +0800 Subject: [PATCH 3/3] feat(cli): incident info --num + channel team/creator names (fc-event#1674) Propagate fc-event#1674 through the generated layer by bumping go-flashduty to the synced+regenerated spec, plus a cligen enhancement so the now-optional incident_id stays back-compatible: - incident info: new --num flag (server-side 6-char short-id lookup); incident_id relaxed to optional. Add an OPTIONAL positional mode to cligen (optionalArg + positional.Optional + optionalPositional map) so `incident info ` keeps working while `incident info --num CBE249` resolves and bare `incident info` (with --num) is valid; `incident info a b` still rejected. - channel list/info: response help documents team_name + creator_name. go-flashduty pinned to the chore/enrich-1674-spec commit (pseudo-version); re-pin to the released version after go-flashduty#11 merges. Build + unit tests (incl. gen_positional) pass. --- go.mod | 2 +- go.sum | 4 +- internal/cli/args.go | 18 +++++++++ internal/cli/gen_positional_test.go | 15 ++++++-- internal/cli/zz_generated_channels.go | 4 ++ internal/cli/zz_generated_incidents.go | 14 +++++-- internal/cli/zz_generated_response_help.go | 4 +- internal/cmd/cligen/main.go | 45 +++++++++++++++++----- 8 files changed, 83 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index c5f780c..40eee45 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli go 1.25.1 require ( - github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07 + github.com/flashcatcloud/go-flashduty v0.5.4-0.20260616041609-da82c4097dd1 github.com/mattn/go-runewidth v0.0.24 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index 5c56bab..f99281d 100644 --- a/go.sum +++ b/go.sum @@ -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/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07 h1:bi1rOjR2OY+TovBGabtVOTcEQWlgzU9RfEwlJxU+3n8= -github.com/flashcatcloud/go-flashduty v0.5.4-0.20260602051355-7583ebae5b07/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8= +github.com/flashcatcloud/go-flashduty v0.5.4-0.20260616041609-da82c4097dd1 h1:K/TceO2NHUPAB8Ew7p/7y6gGDjokNpHyd30uxi8FApc= +github.com/flashcatcloud/go-flashduty v0.5.4-0.20260616041609-da82c4097dd1/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.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= diff --git a/internal/cli/args.go b/internal/cli/args.go index 43cd073..b9fd5b3 100644 --- a/internal/cli/args.go +++ b/internal/cli/args.go @@ -41,6 +41,24 @@ func requireExactArg(name string) cobra.PositionalArgs { } } +// optionalArg returns a positional argument validator that accepts zero or one +// argument named name. It backs generated commands whose positional folds into +// an OPTIONAL body field because the operation also accepts an alternative +// lookup key via a flag (e.g. `incident info` takes either the +// positional or --num). Extra arguments are rejected rather than silently +// dropped: +// +// - zero or one arg: ok +// - >1 args: "expects at most one . Usage: ..." +func optionalArg(name string) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return fmt.Errorf("expects at most one %s. Usage: %s", name, cmd.UseLine()) + } + return nil + } +} + // requireExactlyOneFlag validates that exactly one of the named flags is set. func requireExactlyOneFlag(cmd *cobra.Command, flagNames ...string) error { set := 0 diff --git a/internal/cli/gen_positional_test.go b/internal/cli/gen_positional_test.go index 434da0c..fb6ca3d 100644 --- a/internal/cli/gen_positional_test.go +++ b/internal/cli/gen_positional_test.go @@ -29,19 +29,26 @@ func TestGenPositionalUseLine(t *testing.T) { t.Errorf("ack twin Args rejected one arg: %v", err) } + // `incident info` pins incident_id as an OPTIONAL positional: the backend + // relaxed incident_id (a lookup may instead pass the 6-char num via --num), so + // the positional is 0-or-1. `info ` stays for back-compat; `info` alone + // (with --num) is valid; `info id1 id2` is still rejected. info := genIncidentsInfoCmd() - if got := info.Use; got != "info " { - t.Errorf("info twin Use = %q, want %q", got, "info ") + if got := info.Use; got != "info []" { + t.Errorf("info twin Use = %q, want %q", got, "info []") } if info.Args == nil { t.Errorf("info twin has no Args validator") } - if err := info.Args(info, nil); err == nil { - t.Errorf("info twin Args accepted zero args (want exactly one)") + if err := info.Args(info, nil); err != nil { + t.Errorf("info twin Args rejected zero args (want 0-or-1; --num path): %v", err) } if err := info.Args(info, []string{"id1"}); err != nil { t.Errorf("info twin Args rejected one arg: %v", err) } + if err := info.Args(info, []string{"id1", "id2"}); err == nil { + t.Errorf("info twin Args accepted two args (want at most one)") + } // Override cases: merge pins target_incident_id (NOT source_incident_ids); // war-room detail pins chat_id. diff --git a/internal/cli/zz_generated_channels.go b/internal/cli/zz_generated_channels.go index 44c1e36..370b43a 100644 --- a/internal/cli/zz_generated_channels.go +++ b/internal/cli/zz_generated_channels.go @@ -878,6 +878,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le - channel_name (string) — Channel name. - created_at (integer) — Creation timestamp (unix seconds). - creator_id (integer) — Member ID who created the channel. + - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable). - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels. - description (string) — Free-form description. - disable_auto_close (boolean) — When true, automatic incident closing is disabled. @@ -909,6 +910,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le - Triggered (integer) (required) — Count of triggered incidents in the last 30 days. - status (string) — Channel status. [enabled, disabled, deleted] - team_id (integer) — Owning team ID. + - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable). - updated_at (integer) — Last update timestamp (unix seconds). `, Args: requireExactArg("channel_id"), @@ -1446,6 +1448,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; - channel_name (string) — Channel name. - created_at (integer) — Creation timestamp (unix seconds). - creator_id (integer) — Member ID who created the channel. + - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable). - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels. - description (string) — Free-form description. - disable_auto_close (boolean) — When true, automatic incident closing is disabled. @@ -1477,6 +1480,7 @@ Response fields ('data' envelope is unwrapped — rows are nested under items[]; - Triggered (integer) (required) — Count of triggered incidents in the last 30 days. - status (string) — Channel status. [enabled, disabled, deleted] - team_id (integer) — Owning team ID. + - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable). - updated_at (integer) — Last update timestamp (unix seconds). - total (integer) (required) — Total matching channels. `, diff --git a/internal/cli/zz_generated_incidents.go b/internal/cli/zz_generated_incidents.go index 29b5c54..ccbc755 100644 --- a/internal/cli/zz_generated_incidents.go +++ b/internal/cli/zz_generated_incidents.go @@ -784,8 +784,9 @@ Request fields: func genIncidentsInfoCmd() *cobra.Command { var dataJSON string var fIncidentID string + var fNum string cmd := &cobra.Command{ - Use: "info ", + Use: "info []", Short: "Get incident detail", Long: `Get incident detail. @@ -794,7 +795,8 @@ Retrieve detailed information for a single incident including timeline, alerts, API: POST /incident/info (incidentInfo) Request fields: - --incident-id string (required) — Incident ID (MongoDB ObjectID). + --incident-id string — Incident ID (MongoDB ObjectID). + --num string — Short incident ID (the 6-character uppercased id shown in the UI). Not unique — resolves to the most recent match. Supply either incident_id or num. Response fields ('data' envelope is unwrapped — these fields are at the top level): - account_id (integer) (required) — Account ID that owns the incident. @@ -952,7 +954,7 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le - title (string) (required) — Incident title. - updated_at (integer) (required) — Last update timestamp (seconds). `, - Args: requireExactArg("incident_id"), + Args: optionalArg("incident_id"), Example: ` flashduty incident info --data '{"incident_id":"69da451ef77b1b51f40e83ee"}'`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { @@ -963,6 +965,9 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le if cmd.Flags().Changed("incident-id") { body["incident_id"] = fIncidentID } + if cmd.Flags().Changed("num") { + body["num"] = fNum + } return nil }) if err != nil { @@ -980,7 +985,8 @@ Response fields ('data' envelope is unwrapped — these fields are at the top le }) }, } - cmd.Flags().StringVar(&fIncidentID, "incident-id", "", "Incident ID (MongoDB ObjectID). (required)") + cmd.Flags().StringVar(&fIncidentID, "incident-id", "", "Incident ID (MongoDB ObjectID).") + cmd.Flags().StringVar(&fNum, "num", "", "Short incident ID (the 6-character uppercased id shown in the UI). Not unique — resolves to the most recent match. Supply either incident_id or num.") cmd.Flags().StringVar(&dataJSON, "data", "", "Full request body as JSON; positional arguments and typed flags override its fields. Accepts inline JSON, or - to read stdin.") return cmd } diff --git a/internal/cli/zz_generated_response_help.go b/internal/cli/zz_generated_response_help.go index 130bea0..bec4a75 100644 --- a/internal/cli/zz_generated_response_help.go +++ b/internal/cli/zz_generated_response_help.go @@ -67,11 +67,11 @@ var responseHelpBySDKMethod = map[string]string{ "Channels.ChannelEscalateRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", "Channels.ChannelEscalateRuleInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) (required) — Owning account ID.\n - aggr_window (integer) (required) — Aggregation window in seconds.\n - channel_id (integer) (required) — Channel the rule belongs to.\n - channel_name (string) — Channel name, populated for cross-channel listing responses.\n - created_at (integer) (required) — Creation timestamp (unix seconds).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Emitted only for soft-deleted rules.\n - description (string) (required) — Rule description.\n - filters (object) (required)\n - layers (array) (required) — Escalation levels in order.\n - escalate_window (integer) — Wait before moving to the next level, in minutes. (0-720)\n - force_escalate (boolean) — When true, always escalate regardless of acknowledgement.\n - max_times (integer) — Max repeat notifications within the level. (0-6)\n - notify_step (number) — Repeat interval in minutes. (0.5-120)\n - target (object) (required) — Notification target. At least one of `person_ids`, `team_ids`, `schedule_to_role_ids`, or `emails` must be set, together with either `by` or `webhooks`.\n - by (object) — Per-severity personal notification channels. Required unless `webhooks` is provided.\n - critical (array) — Channels for Critical events (e.g. `voice`, `sms`, `email`, `feishu`).\n - follow_preference (boolean) — When true, use each responder's personal preference instead of the lists below.\n - info (array) — Channels for Info events.\n - warning (array) — Channels for Warning events.\n - emails (array) — Email addresses to notify (push-only scenarios).\n - person_ids (array) — Member IDs to notify directly.\n - schedule_to_role_ids (object) — Map of schedule ID to the role IDs on that schedule to notify.\n - team_ids (array) — Team IDs to notify.\n - webhooks (array) — Group chat / webhook targets. Required unless `by` is provided.\n - settings (object) (required) — Type-specific settings (chat IDs, URLs, etc.).\n - type (string) (required) — Webhook type (e.g. `feishu`, `dingtalk_app`, `wecom_app`, `slack`, `teams`, `custom`).\n - priority (integer) (required) — Evaluation priority. Lower runs first.\n - rule_id (string) (required) — Escalation rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name.\n - status (string) (required) — Rule status. [enabled, disabled]\n - template_id (string) (required) — Notification template ID (MongoDB ObjectID).\n - time_filters (array) (required) — Recurring time windows during which the rule applies.\n - cal_id (string) — Optional calendar ID; restricts the window to days matching the calendar.\n - end (string) — End of the window in `HH:MM`.\n - is_off (boolean) — When true, match days marked as days-off in the calendar.\n - repeat (array) — Days of the week this window repeats on. Empty means every day.\n - start (string) — Start of the window in `HH:MM`.\n - updated_at (integer) (required) — Last update timestamp (unix seconds).\n - updated_by (integer) (required) — Member ID that last updated the rule.\n", "Channels.ChannelEscalateRuleList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) (required) — Owning account ID.\n - aggr_window (integer) (required) — Aggregation window in seconds.\n - channel_id (integer) (required) — Channel the rule belongs to.\n - channel_name (string) — Channel name, populated for cross-channel listing responses.\n - created_at (integer) (required) — Creation timestamp (unix seconds).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Emitted only for soft-deleted rules.\n - description (string) (required) — Rule description.\n - filters (object) (required)\n - layers (array) (required) — Escalation levels in order.\n - escalate_window (integer) — Wait before moving to the next level, in minutes. (0-720)\n - force_escalate (boolean) — When true, always escalate regardless of acknowledgement.\n - max_times (integer) — Max repeat notifications within the level. (0-6)\n - notify_step (number) — Repeat interval in minutes. (0.5-120)\n - target (object) (required) — Notification target. At least one of `person_ids`, `team_ids`, `schedule_to_role_ids`, or `emails` must be set, together with either `by` or `webhooks`.\n - by (object) — Per-severity personal notification channels. Required unless `webhooks` is provided.\n - emails (array) — Email addresses to notify (push-only scenarios).\n - person_ids (array) — Member IDs to notify directly.\n - schedule_to_role_ids (object) — Map of schedule ID to the role IDs on that schedule to notify.\n - team_ids (array) — Team IDs to notify.\n - webhooks (array) — Group chat / webhook targets. Required unless `by` is provided.\n - priority (integer) (required) — Evaluation priority. Lower runs first.\n - rule_id (string) (required) — Escalation rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name.\n - status (string) (required) — Rule status. [enabled, disabled]\n - template_id (string) (required) — Notification template ID (MongoDB ObjectID).\n - time_filters (array) (required) — Recurring time windows during which the rule applies.\n - cal_id (string) — Optional calendar ID; restricts the window to days matching the calendar.\n - end (string) — End of the window in `HH:MM`.\n - is_off (boolean) — When true, match days marked as days-off in the calendar.\n - repeat (array) — Days of the week this window repeats on. Empty means every day.\n - start (string) — Start of the window in `HH:MM`.\n - updated_at (integer) (required) — Last update timestamp (unix seconds).\n - updated_by (integer) (required) — Member ID that last updated the rule.\n", - "Channels.ChannelInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - updated_at (integer) — Last update timestamp (unix seconds).\n", + "Channels.ChannelInfo": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable).\n - updated_at (integer) — Last update timestamp (unix seconds).\n", "Channels.ChannelInfos": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - channel_id (integer) (required) — Channel ID.\n - channel_name (string) (required) — Channel name.\n - status (string) — Channel status. [enabled, disabled]\n", "Channels.ChannelInhibitRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", "Channels.ChannelInhibitRuleList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) (required)\n - channel_id (integer) (required)\n - created_at (integer) (required)\n - deleted_at (integer)\n - description (string) (required)\n - equals (array) (required) — Label keys used to pair source and target alerts.\n - is_directly_discard (boolean) (required)\n - priority (integer) (required)\n - rule_id (string) (required)\n - rule_name (string) (required)\n - source_filters (object) (required)\n - status (string) (required) [enabled, disabled]\n - target_filters (object) (required)\n - updated_at (integer) (required)\n - updated_by (integer) (required)\n", - "Channels.ChannelList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - updated_at (integer) — Last update timestamp (unix seconds).\n", + "Channels.ChannelList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) — Owning account ID.\n - active_incident_highest_severity (string) — Highest severity among active incidents in the channel.\n - auto_resolve_mode (string) — Auto-resolve timer reset mode. [trigger, update]\n - auto_resolve_timeout (integer) — Auto-resolve timeout in seconds. 0 disables auto-resolve.\n - channel_id (integer) — Channel ID.\n - channel_name (string) — Channel name.\n - created_at (integer) — Creation timestamp (unix seconds).\n - creator_id (integer) — Member ID who created the channel.\n - creator_name (string) — Name of the member who created the channel (resolved from the member directory; empty when unavailable).\n - deleted_at (integer) — Deletion timestamp (unix seconds). Non-zero only for soft-deleted channels.\n - description (string) — Free-form description.\n - disable_auto_close (boolean) — When true, automatic incident closing is disabled.\n - disable_outlier_detection (boolean) — When true, outlier incident detection is disabled.\n - external_report_token (string) — Token granted to external reporters when external reporting is enabled.\n - flapping (object) — Flapping detection configuration.\n - in_mins (integer) — Observation window in minutes. (1-1440)\n - is_disabled (boolean) — Disable flapping detection.\n - max_changes (integer) — Max state changes allowed within `in_mins`. (2-100)\n - mute_mins (integer) — Mute duration in minutes after flapping is detected. (0-1440)\n - group (object) — Alert grouping configuration.\n - all_equals_required (boolean) — When true, all listed keys must be present for grouping.\n - cases (array) — Per-filter grouping overrides.\n - equals (array) — Groups of label keys whose equality defines a bucket.\n - i_keys (array) — Label keys used for intelligent grouping embeddings.\n - i_score_threshold (number) — Intelligent grouping similarity threshold. (0.5-1)\n - method (string) (required) — Grouping method: `i` intelligent, `p` pattern, `n` none. [i, p, n]\n - storm_threshold (integer) — Alert storm threshold. (0-10000)\n - storm_thresholds (array) — Multi-level storm thresholds.\n - time_window (integer) — Grouping time window in minutes. Default max is 1440 minutes (24 h); extended accounts may allow up to 43200 minutes (30 days). (min 0)\n - window_type (string) — Window type. Defaults to `tumbling`. [tumbling, sliding]\n - is_external_report_enabled (boolean) — Whether external reporters can file incidents into this channel.\n - is_private (boolean) — When true, the channel is visible only to its managing teams.\n - is_starred (boolean) — Whether the current user has starred this channel.\n - last_incident_at (integer) — Timestamp of the most recent incident (unix seconds).\n - managing_team_ids (array) — Additional teams that can manage the channel.\n - progress_to_incident_cnts (object)\n - Processing (integer) (required) — Count of processing incidents in the last 30 days.\n - Triggered (integer) (required) — Count of triggered incidents in the last 30 days.\n - status (string) — Channel status. [enabled, disabled, deleted]\n - team_id (integer) — Owning team ID.\n - team_name (string) — Owning team name (resolved from the team directory; empty when unavailable).\n - updated_at (integer) — Last update timestamp (unix seconds).\n", "Channels.ChannelSilenceRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", "Channels.ChannelSilenceRuleList": "Response fields (this command's `--json` is a TOP-LEVEL array of these row objects — pipe `jq '.[]'`, NOT `.items[]`):\n - account_id (integer) (required)\n - channel_id (integer) (required)\n - created_at (integer) (required)\n - deleted_at (integer)\n - description (string) (required)\n - filters (object) (required)\n - from_incident_id (string) — Source incident ID when the silence was created from an incident.\n - is_auto_delete (boolean) — When true, the silence rule is automatically deleted after its time window expires. Defaults to false.\n - is_directly_discard (boolean) (required) — When true, silenced alerts are dropped instead of suppressed into incidents.\n - is_effective (boolean) (required) — Whether the rule is currently in effect.\n - priority (integer) (required) — Evaluation priority. Lower runs first.\n - rule_id (string) (required)\n - rule_name (string) (required)\n - status (string) (required) [enabled, disabled]\n - time_filter (object) (required) — One-off time window defined by unix seconds.\n - end_time (integer) (required) — Window end (unix seconds). Must be > 0.\n - start_time (integer) (required) — Window start (unix seconds). Must be > 0 and less than `end_time`.\n - time_filters (array) (required) — Recurring time windows.\n - cal_id (string) — Optional calendar ID; restricts the window to days matching the calendar.\n - end (string) — End of the window in `HH:MM`.\n - is_off (boolean) — When true, match days marked as days-off in the calendar.\n - repeat (array) — Days of the week this window repeats on. Empty means every day.\n - start (string) — Start of the window in `HH:MM`.\n - updated_at (integer) (required)\n - updated_by (integer) (required)\n", "Channels.ChannelUnsubscribeRuleCreate": "Response fields (`data` envelope is unwrapped — these fields are at the top level):\n - rule_id (string) (required) — Newly created rule ID (MongoDB ObjectID).\n - rule_name (string) (required) — Rule name echoed back from the request.\n", diff --git a/internal/cmd/cligen/main.go b/internal/cmd/cligen/main.go index 6bd148c..b2b993a 100644 --- a/internal/cmd/cligen/main.go +++ b/internal/cmd/cligen/main.go @@ -695,6 +695,12 @@ func scalarKind(t reflect.Type) (string, bool) { // and picks it, making the command feel like `rule-move ` while `--ids` // carries the actual subjects. Suppress the positional entirely so both fields // are explicit flags. +// - incidentInfo: incident_id is no longer required (the backend relaxed it so a +// lookup can supply the 6-char `num` short id via --num instead). The id-or-num +// pair means the *_id heuristic (required-only) would drop the positional, but +// is the natural subject and must stay for back-compat. Pin it and +// mark it OPTIONAL (see optionalPositional) so `incident info ` keeps working +// while `incident info --num CBE249` also resolves. // // An empty string in this map means "suppress positional" — no positional is // emitted for that operation, even when the heuristic would pick one. @@ -702,14 +708,24 @@ var positionalOverride = map[string]string{ "incidentMerge": "target_incident_id", "incidentWarRoomDetail": "chat_id", "incident-write-add-war-room-member": "chat_id", - "monit-rule-write-move": "", // suppress: `ids` bypasses *_ids heuristic; dest_folder_id is not the natural subject + "incidentInfo": "incident_id", // optional (see optionalPositional): OR --num + "monit-rule-write-move": "", // suppress: `ids` bypasses *_ids heuristic; dest_folder_id is not the natural subject +} + +// optionalPositional marks override ops whose pinned positional is 0-or-1 rather +// than exactly-one. The field is optional because the operation accepts an +// alternative lookup key via a flag, so a bare-positional-less invocation is +// valid. Today only incidentInfo (incident_id OR --num) qualifies. +var optionalPositional = map[string]bool{ + "incidentInfo": true, } // positional describes the positional argument a generated command exposes. type positional struct { - Wire string // request-body wire key the positional folds into - Kind string // "string" | "slice" | "int" — selects genFoldPositional behavior - Array bool // true => variadic (>=1 arg); false => exactly one arg + Wire string // request-body wire key the positional folds into + Kind string // "string" | "slice" | "int" — selects genFoldPositional behavior + Array bool // true => variadic (>=1 arg); false => exactly one arg + Optional bool // true => 0-or-1 arg (field is optional; an alternative lookup key exists). Scalar only. } // selectPositional decides which (if any) request field becomes the command's @@ -754,7 +770,9 @@ func selectPositional(o specOp, scalars []scalarField, byWire map[string]specFie if wire == "" { return positional{}, false // explicit suppress: empty string means no positional } - return mk(wire) + p, found := mk(wire) + p.Optional = optionalPositional[o.OpID] // 0-or-1 when the op also accepts an alternative lookup key + return p, found } var reqScalars, reqArrays []string @@ -889,9 +907,12 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string { // [...] for the additional variadic ids, which reads as a list of one // id-kind rather than " id2 id3". name := kebab(pos.Wire) - if pos.Array { + switch { + case pos.Array: use = verb + " <" + strings.TrimSuffix(name, "s") + "> [...]" - } else { + case pos.Optional: + use = verb + " [<" + name + ">]" + default: use = verb + " <" + name + ">" } } @@ -903,10 +924,14 @@ func emitCmd(fn string, s service, o specOp, mi methodInfo) string { // Scalar positionals use requireExactArg so extra arguments (e.g. // `incident info id1 id2`) are rejected with a clear error instead of // silently dropping id2. Array positionals use requireArgs (>=1) because - // they are variadic by design. - if pos.Array { + // they are variadic by design. Optional scalar positionals use optionalArg + // (0-or-1) because the op also accepts an alternative lookup flag. + switch { + case pos.Array: fmt.Fprintf(&b, "\t\tArgs: requireArgs(%q),\n", pos.Wire) - } else { + case pos.Optional: + fmt.Fprintf(&b, "\t\tArgs: optionalArg(%q),\n", pos.Wire) + default: fmt.Fprintf(&b, "\t\tArgs: requireExactArg(%q),\n", pos.Wire) } }