From 7df80f07b47fdf907cc138acf188a2223a381171 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Tue, 16 Jun 2026 12:17:41 +0800 Subject: [PATCH] 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) } }