From e82d58891557d69eee0082970741d51de7ed18d5 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Wed, 17 Jun 2026 12:32:38 +0800 Subject: [PATCH 1/4] docs(cli): cross-reference insight from incident list --help; clarify insight group purpose Add "See also: fduty insight team|responder|channel" to incident list Long description so agents discover the analytics API without a user prompt. Update insight group Short to call out it is preferred for metrics queries. --- internal/cli/incident.go | 2 +- internal/cli/insight.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 61515bb..91426e6 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -80,7 +80,7 @@ func newIncidentListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List incidents", - Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100.", "Incidents", "List"), + Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100.\n\nSee also: fduty insight team|responder|channel for aggregated metrics (MTTA, MTTR, noise reduction) instead of paginating raw incidents.", "Incidents", "List"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { startTime, err := timeutil.Parse(since) diff --git a/internal/cli/insight.go b/internal/cli/insight.go index 182c75b..577e2aa 100644 --- a/internal/cli/insight.go +++ b/internal/cli/insight.go @@ -13,7 +13,7 @@ import ( func newInsightCmd() *cobra.Command { cmd := &cobra.Command{ Use: "insight", - Short: "Query insight metrics", + Short: "Query aggregated incident metrics by team/responder/channel (preferred over incident list for analytics)", } // insight team/channel/responder are now served by the generated commands // (richer flag set: severities, *_ids, fields, aggregate-unit, …; relative From 58e823047e6d370645ff2658e7bc4fcf2f9a0bba Mon Sep 17 00:00:00 2001 From: ysyneu Date: Wed, 17 Jun 2026 12:32:45 +0800 Subject: [PATCH 2/4] feat(cli): accept positional on team get (completes PR#50 rollout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR#50 added positional IDs to incident/alert curated commands but left team get requiring --id. Agents using the positional form got syntax errors (seen 3× in sess_5Cg5UV6WpTY8Yt4VSbHrLw steps 60-61). MaximumNArgs(1) + PreRunE bypass when arg present is consistent with the PR#50 pattern. --- internal/cli/team.go | 18 ++++++++++++++---- internal/cli/team_get_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 internal/cli/team_get_test.go diff --git a/internal/cli/team.go b/internal/cli/team.go index 1d82ac2..fbf031e 100644 --- a/internal/cli/team.go +++ b/internal/cli/team.go @@ -92,23 +92,35 @@ func newTeamGetCmd() *cobra.Command { ) cmd := &cobra.Command{ - Use: "get", + Use: "get []", Short: "Get team detail", Long: curatedLong(`Get detailed information about a specific team. -Specify the team by exactly one of: --id, --name, or --ref-id. +Specify the team by positional ID, or by exactly one of: --id, --name, or --ref-id. The output includes team metadata, member list, and audit information. Examples: + flashduty team get 123 flashduty team get --id 123 flashduty team get --name "SRE Team" flashduty team get --ref-id "hr-dept-42" flashduty team get --id 123 --json`, "Teams", "ReadInfo"), + Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + return nil + } return requireExactlyOneFlag(cmd, "id", "name", "ref-id") }, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { + if len(ctx.Args) == 1 { + id, err := strconv.ParseInt(ctx.Args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid team id %q: must be a number", ctx.Args[0]) + } + teamID = id + } team, _, err := ctx.Client.Teams.ReadInfo(cmdContext(ctx.Cmd), &flashduty.TeamInfoRequest{ TeamID: uint64(teamID), TeamName: teamName, @@ -122,8 +134,6 @@ Examples: return ctx.Printer.Print(team, nil) } - // TeamItem carries only member person IDs; resolve names/emails - // in one batch to replicate the legacy member display. members := resolveTeamMemberInfos(ctx, team.PersonIDs) printTeamDetail(ctx.Writer, team, members) return nil diff --git a/internal/cli/team_get_test.go b/internal/cli/team_get_test.go new file mode 100644 index 0000000..572a259 --- /dev/null +++ b/internal/cli/team_get_test.go @@ -0,0 +1,29 @@ +package cli + +import "testing" + +func TestTeamGetAcceptsPositionalID(t *testing.T) { + cmd := newTeamGetCmd() + // MaximumNArgs(1): two positional args should be rejected + if cmd.Args != nil { + if err := cmd.Args(cmd, []string{"123456", "789"}); err == nil { + t.Fatal("team get should reject two positional args") + } + } +} + +func TestTeamGetNoArgNoFlagFails(t *testing.T) { + cmd := newTeamGetCmd() + err := cmd.PreRunE(cmd, []string{}) + if err == nil { + t.Fatal("expected error when no positional arg and no flag provided") + } +} + +func TestTeamGetPositionalArgBypassesPreRunE(t *testing.T) { + cmd := newTeamGetCmd() + err := cmd.PreRunE(cmd, []string{"123456"}) + if err != nil { + t.Fatalf("PreRunE should succeed with positional arg, got: %v", err) + } +} From 8bb11486fa6445d7e559051815e71720f7418a9e Mon Sep 17 00:00:00 2001 From: ysyneu Date: Wed, 17 Jun 2026 12:35:25 +0800 Subject: [PATCH 3/4] refactor(cli): use optionalArg("id") for team get positional validator cobra.MaximumNArgs(1) was the only direct cobra-validator use in the cli package. The optionalArg helper is the project idiom for an optional positional that has a flag alternative (its doc comment describes exactly this case), and it yields the consistent "expects at most one id" error instead of cobra's default terse message. --- internal/cli/team.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/team.go b/internal/cli/team.go index fbf031e..8c4c943 100644 --- a/internal/cli/team.go +++ b/internal/cli/team.go @@ -105,7 +105,7 @@ Examples: flashduty team get --name "SRE Team" flashduty team get --ref-id "hr-dept-42" flashduty team get --id 123 --json`, "Teams", "ReadInfo"), - Args: cobra.MaximumNArgs(1), + Args: optionalArg("id"), PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { return nil From 18098aeb0ad80be21bdc5fc8b82856eddfb7032f Mon Sep 17 00:00:00 2001 From: ysyneu Date: Wed, 17 Jun 2026 12:37:01 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(cli):=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20conflict=20detection,=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - team get: error when positional and any named flag are both provided (previously silently discarded the flag); add test for the conflict case - Restore member-resolution comment explaining the second round-trip - insight Short: trim to 63 chars (was 104, longest in codebase) - incident list Long: use metavar instead of bare pipe to avoid shell-alternation confusion --- internal/cli/incident.go | 2 +- internal/cli/insight.go | 2 +- internal/cli/team.go | 7 +++++++ internal/cli/team_get_test.go | 9 +++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 91426e6..2da3f92 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -80,7 +80,7 @@ func newIncidentListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List incidents", - Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100.\n\nSee also: fduty insight team|responder|channel for aggregated metrics (MTTA, MTTR, noise reduction) instead of paginating raw incidents.", "Incidents", "List"), + Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100.\n\nSee also: fduty insight for aggregated metrics (MTTA, MTTR, noise reduction) instead of paginating raw incidents.", "Incidents", "List"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { startTime, err := timeutil.Parse(since) diff --git a/internal/cli/insight.go b/internal/cli/insight.go index 577e2aa..d61729d 100644 --- a/internal/cli/insight.go +++ b/internal/cli/insight.go @@ -13,7 +13,7 @@ import ( func newInsightCmd() *cobra.Command { cmd := &cobra.Command{ Use: "insight", - Short: "Query aggregated incident metrics by team/responder/channel (preferred over incident list for analytics)", + Short: "Query aggregated incident metrics by team, responder, or channel", } // insight team/channel/responder are now served by the generated commands // (richer flag set: severities, *_ids, fields, aggregate-unit, …; relative diff --git a/internal/cli/team.go b/internal/cli/team.go index 8c4c943..6aa35e2 100644 --- a/internal/cli/team.go +++ b/internal/cli/team.go @@ -108,6 +108,11 @@ Examples: Args: optionalArg("id"), PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { + for _, f := range []string{"id", "name", "ref-id"} { + if cmd.Flags().Changed(f) { + return fmt.Errorf("positional cannot be combined with --%s", f) + } + } return nil } return requireExactlyOneFlag(cmd, "id", "name", "ref-id") @@ -134,6 +139,8 @@ Examples: return ctx.Printer.Print(team, nil) } + // TeamItem carries only member person IDs; resolve names/emails + // in one batch to replicate the legacy member display. members := resolveTeamMemberInfos(ctx, team.PersonIDs) printTeamDetail(ctx.Writer, team, members) return nil diff --git a/internal/cli/team_get_test.go b/internal/cli/team_get_test.go index 572a259..265f12c 100644 --- a/internal/cli/team_get_test.go +++ b/internal/cli/team_get_test.go @@ -27,3 +27,12 @@ func TestTeamGetPositionalArgBypassesPreRunE(t *testing.T) { t.Fatalf("PreRunE should succeed with positional arg, got: %v", err) } } + +func TestTeamGetPositionalAndFlagConflictFails(t *testing.T) { + cmd := newTeamGetCmd() + _ = cmd.Flags().Set("id", "456") + err := cmd.PreRunE(cmd, []string{"123"}) + if err == nil { + t.Fatal("expected error when positional arg and --id flag are both provided") + } +}