From 4d67416c13f6b375c95a6f94d1bc31cec0f27207 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 11:56:49 +0800 Subject: [PATCH 1/5] feat: add team CRUD commands (get, create, update, delete) Add full team management CLI commands with both table and JSON output: - team get: view team detail by --id, --name, or --ref-id Parallel /team/info + /team/infos for full metadata with member names - team create: create team with --name, --description, --person-ids, --emails - team update: update team by --id with any combination of fields Clear warning that --person-ids replaces the entire member list - team delete: delete team with interactive confirmation prompt Supports --force to skip confirmation - team list: enhanced with --limit, --orderby, --asc, --person-id flags New shared helpers: - WriteResultJSON: structured JSON for write operations - confirmAction: interactive delete confirmation using cmd.InOrStdin() - requireExactlyOneFlag: mutual exclusivity validator for identifier flags Depends on flashduty-sdk feat/team-crud branch. --- go.mod | 2 + go.sum | 4 - internal/cli/args.go | 39 ++++ internal/cli/command.go | 16 ++ internal/cli/command_test.go | 13 ++ internal/cli/root.go | 5 + internal/cli/team.go | 348 +++++++++++++++++++++++++++++++++-- 7 files changed, 405 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 4018889..a843e0a 100644 --- a/go.mod +++ b/go.mod @@ -16,3 +16,5 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.43.0 // indirect ) + +replace github.com/flashcatcloud/flashduty-sdk => ../flashduty-sdk diff --git a/go.sum b/go.sum index 14ee028..56acea7 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,4 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513114317-7c106c52bd36 h1:yrY7iNBkZrcj6qRZsXwCzMTXjKKzDRGlca/QRfNqiZI= -github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513114317-7c106c52bd36/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY= -github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513121733-bf978e2213f5 h1:AwVNa+CrGqSl60Pq2RQKRV+layS5coJQx01Np+34HD4= -github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513121733-bf978e2213f5/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/cli/args.go b/internal/cli/args.go index bb20d3b..4835312 100644 --- a/internal/cli/args.go +++ b/internal/cli/args.go @@ -1,10 +1,13 @@ package cli import ( + "bufio" "fmt" + "os" "strings" "github.com/spf13/cobra" + "golang.org/x/term" ) // requireArgs returns a positional argument validator that produces descriptive @@ -20,3 +23,39 @@ func requireArgs(argNames ...string) cobra.PositionalArgs { return nil } } + +// requireExactlyOneFlag validates that exactly one of the named flags is set. +func requireExactlyOneFlag(cmd *cobra.Command, flagNames ...string) error { + set := 0 + for _, name := range flagNames { + if cmd.Flags().Changed(name) { + set++ + } + } + if set != 1 { + return fmt.Errorf("exactly one of --%s must be specified", strings.Join(flagNames, ", --")) + } + return nil +} + +// confirmAction prompts the user for confirmation in interactive terminals. +// Returns true if the user confirms, or if running in non-interactive / JSON / --force mode. +func confirmAction(cmd *cobra.Command, message string) bool { + if flagJSON { + return true + } + force, _ := cmd.Flags().GetBool("force") + if force { + return true + } + if !term.IsTerminal(int(os.Stdin.Fd())) { + return true + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s [y/N]: ", message) + scanner := bufio.NewScanner(cmd.InOrStdin()) + if scanner.Scan() { + answer := strings.TrimSpace(strings.ToLower(scanner.Text())) + return answer == "y" || answer == "yes" + } + return false +} diff --git a/internal/cli/command.go b/internal/cli/command.go index db6b865..9f4289e 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -1,6 +1,7 @@ package cli import ( + "encoding/json" "fmt" "io" @@ -64,3 +65,18 @@ func (ctx *RunContext) PrintTotal(items any, cols []output.Column, total int) er func (ctx *RunContext) WriteResult(message string) { writeResult(ctx.Writer, message) } + +// WriteResultJSON outputs structured data as JSON in --json mode, +// or a human-readable message in table mode. +func (ctx *RunContext) WriteResultJSON(data any, humanMessage string) error { + if ctx.JSON { + out, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + _, _ = fmt.Fprintln(ctx.Writer, string(out)) + return nil + } + _, _ = fmt.Fprintln(ctx.Writer, humanMessage) + return nil +} diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go index bbcfac1..d8a609e 100644 --- a/internal/cli/command_test.go +++ b/internal/cli/command_test.go @@ -215,6 +215,19 @@ func (m *mockClient) CancelStatusPageMigration(context.Context, string) error { return fmt.Errorf("mockClient: CancelStatusPageMigration not implemented") } +// Phase 5: Team Management +func (m *mockClient) GetTeamInfo(context.Context, *flashduty.TeamGetInput) (*flashduty.TeamItem, error) { + return nil, fmt.Errorf("mockClient: GetTeamInfo not implemented") +} + +func (m *mockClient) UpsertTeam(context.Context, *flashduty.TeamUpsertInput) (*flashduty.TeamUpsertOutput, error) { + return nil, fmt.Errorf("mockClient: UpsertTeam not implemented") +} + +func (m *mockClient) DeleteTeam(context.Context, *flashduty.TeamDeleteInput) error { + return fmt.Errorf("mockClient: DeleteTeam not implemented") +} + // saveAndResetGlobals saves the current state of all global vars that commands // mutate, resets them to safe defaults, and returns a restore function for // t.Cleanup. diff --git a/internal/cli/root.go b/internal/cli/root.go index 69c5d75..3b76af9 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -80,6 +80,11 @@ type flashdutyClient interface { StartStatusPageEmailSubscriberMigration(ctx context.Context, input *flashduty.StartStatusPageEmailSubscriberMigrationInput) (*flashduty.StartStatusPageMigrationOutput, error) GetStatusPageMigrationStatus(ctx context.Context, jobID string) (*flashduty.StatusPageMigrationJob, error) CancelStatusPageMigration(ctx context.Context, jobID string) error + + // === PHASE 5: Team Management === + GetTeamInfo(ctx context.Context, input *flashduty.TeamGetInput) (*flashduty.TeamItem, error) + UpsertTeam(ctx context.Context, input *flashduty.TeamUpsertInput) (*flashduty.TeamUpsertOutput, error) + DeleteTeam(ctx context.Context, input *flashduty.TeamDeleteInput) error } // newClientFn creates a flashdutyClient. Override in tests to inject a mock. diff --git a/internal/cli/team.go b/internal/cli/team.go index 9927697..c4e5ae8 100644 --- a/internal/cli/team.go +++ b/internal/cli/team.go @@ -1,6 +1,8 @@ package cli import ( + "fmt" + "io" "strconv" "strings" @@ -14,48 +16,358 @@ func newTeamCmd() *cobra.Command { cmd := &cobra.Command{ Use: "team", Short: "Manage teams", + Long: "Create, list, view, update, and delete teams in your FlashDuty account.", } cmd.AddCommand(newTeamListCmd()) + cmd.AddCommand(newTeamGetCmd()) + cmd.AddCommand(newTeamCreateCmd()) + cmd.AddCommand(newTeamUpdateCmd()) + cmd.AddCommand(newTeamDeleteCmd()) return cmd } func newTeamListCmd() *cobra.Command { - var name string - var page int + var ( + name string + page int + limit int + orderBy string + asc bool + personID int64 + ) cmd := &cobra.Command{ Use: "list", Short: "List teams", + Long: `List teams in your account. + +Use --name to search by team name substring. +Use --person-id to filter teams containing a specific member. +Results are paginated; use --page and --limit to navigate. + +Examples: + flashduty team list + flashduty team list --name "SRE" + flashduty team list --person-id 12345 --limit 50 + flashduty team list --orderby team_name --asc`, RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { result, err := ctx.Client.ListTeams(cmdContext(ctx.Cmd), &flashduty.ListTeamsInput{ - Name: name, - Page: page, + Name: name, + Page: page, + Limit: limit, + OrderBy: orderBy, + Asc: asc, + PersonID: personID, + }) + if err != nil { + return err + } + + cols := teamListColumns() + return ctx.PrintTotal(result.Teams, cols, result.Total) + }) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Search by team name substring") + cmd.Flags().IntVar(&page, "page", 1, "Page number (default 1)") + cmd.Flags().IntVar(&limit, "limit", 20, "Page size, max 100 (default 20)") + cmd.Flags().StringVar(&orderBy, "orderby", "", "Sort field: created_at, updated_at, team_name") + cmd.Flags().BoolVar(&asc, "asc", false, "Sort in ascending order") + cmd.Flags().Int64Var(&personID, "person-id", 0, "Filter teams by member ID") + + return cmd +} + +func newTeamGetCmd() *cobra.Command { + var ( + teamID int64 + teamName string + refID string + ) + + cmd := &cobra.Command{ + Use: "get", + Short: "Get team detail", + Long: `Get detailed information about a specific team. + +Specify the team by exactly one of: --id, --name, or --ref-id. +The output includes team metadata, member list, and audit information. + +Examples: + 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`, + PreRunE: func(cmd *cobra.Command, args []string) error { + return requireExactlyOneFlag(cmd, "id", "name", "ref-id") + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runCommand(cmd, args, func(ctx *RunContext) error { + team, err := ctx.Client.GetTeamInfo(cmdContext(ctx.Cmd), &flashduty.TeamGetInput{ + TeamID: teamID, + TeamName: teamName, + RefID: refID, }) if err != nil { return err } - cols := []output.Column{ - {Header: "ID", Field: func(v any) string { return strconv.FormatInt(v.(flashduty.TeamInfo).TeamID, 10) }}, - {Header: "NAME", Field: func(v any) string { return v.(flashduty.TeamInfo).TeamName }}, - {Header: "MEMBERS", MaxWidth: 50, Field: func(v any) string { - members := v.(flashduty.TeamInfo).Members - names := make([]string, 0, len(members)) - for _, m := range members { - names = append(names, m.PersonName) - } - return strings.Join(names, ", ") - }}, + if ctx.JSON { + return ctx.Printer.Print(team, nil) } - return ctx.PrintTotal(result.Teams, cols, result.Total) + printTeamDetail(ctx.Writer, team) + return nil }) }, } - cmd.Flags().StringVar(&name, "name", "", "Search by name") - cmd.Flags().IntVar(&page, "page", 1, "Page number") + cmd.Flags().Int64Var(&teamID, "id", 0, "Team ID") + cmd.Flags().StringVar(&teamName, "name", "", "Team name") + cmd.Flags().StringVar(&refID, "ref-id", "", "External reference ID") return cmd } + +func newTeamCreateCmd() *cobra.Command { + var ( + name string + description string + personIDs string + emails string + refID string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new team", + Long: `Create a new team. + +The --name flag is required and must be unique within the account (1-39 characters). +Use --person-ids to add existing members by their person IDs (comma-separated). +Use --emails to invite members by email address (comma-separated). + +Examples: + flashduty team create --name "SRE Team" + flashduty team create --name "SRE Team" --description "Site Reliability" --person-ids 1,2,3 + flashduty team create --name "SRE Team" --emails alice@example.com,bob@example.com + flashduty team create --name "SRE Team" --ref-id "hr-dept-42" --json`, + RunE: func(cmd *cobra.Command, args []string) error { + return runCommand(cmd, args, func(ctx *RunContext) error { + ids, err := parseIntSlice(personIDs) + if err != nil { + return fmt.Errorf("invalid --person-ids: %w", err) + } + + result, err := ctx.Client.UpsertTeam(cmdContext(ctx.Cmd), &flashduty.TeamUpsertInput{ + TeamName: name, + Description: description, + PersonIDs: ids, + Emails: parseStringSlice(emails), + RefID: refID, + }) + if err != nil { + return err + } + + return ctx.WriteResultJSON(result, + fmt.Sprintf("Team created: %s (ID: %d)", result.TeamName, result.TeamID)) + }) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Team name (required, 1-39 characters)") + cmd.Flags().StringVar(&description, "description", "", "Team description (max 500 characters)") + cmd.Flags().StringVar(&personIDs, "person-ids", "", "Comma-separated member person IDs") + cmd.Flags().StringVar(&emails, "emails", "", "Comma-separated email addresses to invite") + cmd.Flags().StringVar(&refID, "ref-id", "", "External reference ID for HR system integration") + _ = cmd.MarkFlagRequired("name") + + return cmd +} + +func newTeamUpdateCmd() *cobra.Command { + var ( + teamID int64 + name string + description string + personIDs string + emails string + refID string + ) + + cmd := &cobra.Command{ + Use: "update", + Short: "Update an existing team", + Long: `Update an existing team. + +The --id flag is required to identify which team to update. +WARNING: --person-ids REPLACES the entire member list. To keep existing members, +include all current member IDs along with the new ones. Use "team get --id " +to see the current member list before updating. + +Examples: + flashduty team update --id 123 --name "New Name" + flashduty team update --id 123 --description "Updated description" + flashduty team update --id 123 --person-ids 1,2,3,4,5 + flashduty team update --id 123 --name "Renamed" --json`, + RunE: func(cmd *cobra.Command, args []string) error { + if teamID == 0 { + return fmt.Errorf("--id is required") + } + + return runCommand(cmd, args, func(ctx *RunContext) error { + ids, err := parseIntSlice(personIDs) + if err != nil { + return fmt.Errorf("invalid --person-ids: %w", err) + } + + input := &flashduty.TeamUpsertInput{ + TeamID: teamID, + } + if cmd.Flags().Changed("name") { + input.TeamName = name + } + if cmd.Flags().Changed("description") { + input.Description = description + } + if cmd.Flags().Changed("person-ids") { + input.PersonIDs = ids + } + if cmd.Flags().Changed("emails") { + input.Emails = parseStringSlice(emails) + } + if cmd.Flags().Changed("ref-id") { + input.RefID = refID + } + + result, err := ctx.Client.UpsertTeam(cmdContext(ctx.Cmd), input) + if err != nil { + return err + } + + return ctx.WriteResultJSON(result, + fmt.Sprintf("Team updated: %s (ID: %d)", result.TeamName, result.TeamID)) + }) + }, + } + + cmd.Flags().Int64Var(&teamID, "id", 0, "Team ID (required)") + cmd.Flags().StringVar(&name, "name", "", "New team name (1-39 characters)") + cmd.Flags().StringVar(&description, "description", "", "New description (max 500 characters)") + cmd.Flags().StringVar(&personIDs, "person-ids", "", "Comma-separated member person IDs (replaces entire member list)") + cmd.Flags().StringVar(&emails, "emails", "", "Comma-separated email addresses to invite") + cmd.Flags().StringVar(&refID, "ref-id", "", "External reference ID") + _ = cmd.MarkFlagRequired("id") + + return cmd +} + +func newTeamDeleteCmd() *cobra.Command { + var ( + teamID int64 + teamName string + refID string + force bool + ) + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a team", + Long: `Permanently delete a team. + +Specify the team by exactly one of: --id, --name, or --ref-id. +This action is irreversible. You will be prompted for confirmation +unless --force is set or output is in JSON mode. + +Examples: + flashduty team delete --id 123 + flashduty team delete --name "Old Team" --force + flashduty team delete --ref-id "hr-dept-99" --json`, + PreRunE: func(cmd *cobra.Command, args []string) error { + return requireExactlyOneFlag(cmd, "id", "name", "ref-id") + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runCommand(cmd, args, func(ctx *RunContext) error { + identifier := identifierDescription(teamID, teamName, refID) + if !confirmAction(ctx.Cmd, fmt.Sprintf("Are you sure you want to delete team %s?", identifier)) { + _, _ = fmt.Fprintln(ctx.Writer, "Aborted.") + return nil + } + + err := ctx.Client.DeleteTeam(cmdContext(ctx.Cmd), &flashduty.TeamDeleteInput{ + TeamID: teamID, + TeamName: teamName, + RefID: refID, + }) + if err != nil { + return err + } + + ctx.WriteResult("Team deleted successfully.") + return nil + }) + }, + } + + cmd.Flags().Int64Var(&teamID, "id", 0, "Team ID") + cmd.Flags().StringVar(&teamName, "name", "", "Team name") + cmd.Flags().StringVar(&refID, "ref-id", "", "External reference ID") + cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt") + + return cmd +} + +func teamListColumns() []output.Column { + return []output.Column{ + {Header: "ID", Field: func(v any) string { return strconv.FormatInt(v.(flashduty.TeamInfo).TeamID, 10) }}, + {Header: "NAME", Field: func(v any) string { return v.(flashduty.TeamInfo).TeamName }}, + {Header: "MEMBERS", MaxWidth: 50, Field: func(v any) string { + members := v.(flashduty.TeamInfo).Members + names := make([]string, 0, len(members)) + for _, m := range members { + names = append(names, m.PersonName) + } + return strings.Join(names, ", ") + }}, + } +} + +func printTeamDetail(w io.Writer, team *flashduty.TeamItem) { + members := make([]string, 0, len(team.Members)) + for _, m := range team.Members { + if m.Email != "" { + members = append(members, fmt.Sprintf("%s <%s>", m.PersonName, m.Email)) + } else { + members = append(members, m.PersonName) + } + } + if len(members) == 0 { + for _, id := range team.PersonIDs { + members = append(members, strconv.FormatInt(id, 10)) + } + } + + _, _ = fmt.Fprintf(w, "ID: %d\n", team.TeamID) + _, _ = fmt.Fprintf(w, "Name: %s\n", team.TeamName) + _, _ = fmt.Fprintf(w, "Description: %s\n", orDash(team.Description)) + _, _ = fmt.Fprintf(w, "Status: %s\n", orDash(team.Status)) + _, _ = fmt.Fprintf(w, "Ref ID: %s\n", orDash(team.RefID)) + _, _ = fmt.Fprintf(w, "Members: %s\n", orDash(strings.Join(members, ", "))) + _, _ = fmt.Fprintf(w, "Created: %s\n", output.FormatTime(team.CreatedAt)) + _, _ = fmt.Fprintf(w, "Updated: %s\n", output.FormatTime(team.UpdatedAt)) + _, _ = fmt.Fprintf(w, "Created By: %s\n", orDash(team.CreatorName)) + _, _ = fmt.Fprintf(w, "Updated By: %s\n", orDash(team.UpdatedByName)) +} + +func identifierDescription(id int64, name, refID string) string { + if id != 0 { + return fmt.Sprintf("ID=%d", id) + } + if name != "" { + return fmt.Sprintf("%q", name) + } + return fmt.Sprintf("ref-id=%q", refID) +} From 283d2a0d43ad88480c26e71680282ffbf45619b9 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 12:08:33 +0800 Subject: [PATCH 2/5] fix: auto-fetch team name on update when --name not provided The API requires team_name on every upsert call. When updating a team without --name, fetch the current team name first so it's preserved. --- internal/cli/team.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/cli/team.go b/internal/cli/team.go index c4e5ae8..d15bb21 100644 --- a/internal/cli/team.go +++ b/internal/cli/team.go @@ -224,11 +224,22 @@ Examples: return fmt.Errorf("invalid --person-ids: %w", err) } - input := &flashduty.TeamUpsertInput{ - TeamID: teamID, + // The API requires team_name on every upsert. If the user didn't + // provide --name, fetch the current name so we don't clear it. + teamName := name + if !cmd.Flags().Changed("name") { + existing, err := ctx.Client.GetTeamInfo(cmdContext(ctx.Cmd), &flashduty.TeamGetInput{ + TeamID: teamID, + }) + if err != nil { + return fmt.Errorf("failed to fetch current team: %w", err) + } + teamName = existing.TeamName } - if cmd.Flags().Changed("name") { - input.TeamName = name + + input := &flashduty.TeamUpsertInput{ + TeamID: teamID, + TeamName: teamName, } if cmd.Flags().Changed("description") { input.Description = description From b5bb39d4533f32919194ac791dc5fd75a510b759 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 12:18:22 +0800 Subject: [PATCH 3/5] fix: abort delete in non-interactive mode unless --force is set Previously, confirmAction returned true when stdin was not a TTY, allowing destructive deletes to proceed silently in CI/cron/pipes. Now it returns false, requiring explicit --force for non-interactive use. --- internal/cli/args.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/args.go b/internal/cli/args.go index 4835312..e83813d 100644 --- a/internal/cli/args.go +++ b/internal/cli/args.go @@ -49,7 +49,7 @@ func confirmAction(cmd *cobra.Command, message string) bool { return true } if !term.IsTerminal(int(os.Stdin.Fd())) { - return true + return false } _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s [y/N]: ", message) scanner := bufio.NewScanner(cmd.InOrStdin()) From c120fc700fbecb790c3f0cbee13e860d45ca19b5 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 12:23:32 +0800 Subject: [PATCH 4/5] fix: use remote SDK commit instead of local replace directive Remove the ../flashduty-sdk replace directive that breaks CI builds. Point go.mod at the published feat/team-crud commit (dbac133) instead. --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a843e0a..175d956 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/flashduty-sdk v0.8.1-0.20260513121733-bf978e2213f5 + github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514040822-dbac13398b6c github.com/spf13/cobra v1.10.2 golang.org/x/term v0.42.0 gopkg.in/yaml.v3 v3.0.1 @@ -16,5 +16,3 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.43.0 // indirect ) - -replace github.com/flashcatcloud/flashduty-sdk => ../flashduty-sdk diff --git a/go.sum b/go.sum index 56acea7..4206332 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514040822-dbac13398b6c h1:rGRydfWe+Sao/aEzFKz+CtWgTINTAa0+QLFQG5pA4JU= +github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514040822-dbac13398b6c/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= From 408862ef6631dc0b3b2bd2556f7a05d83ce7f7c0 Mon Sep 17 00:00:00 2001 From: debidong <1953531014@qq.com> Date: Thu, 14 May 2026 14:59:53 +0800 Subject: [PATCH 5/5] docs: update skills for team CRUD commands - flashduty-admin: document team get/create/update/delete commands, all flags, team lifecycle workflow, and member replacement warning - flashduty-shared: add team get to reference lookups, add safety rules for team delete and member list replacement --- skills/flashduty-admin/SKILL.md | 107 ++++++++++++++++++++++++++++++- skills/flashduty-shared/SKILL.md | 13 ++-- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/skills/flashduty-admin/SKILL.md b/skills/flashduty-admin/SKILL.md index 2677ebd..c1d0b8f 100644 --- a/skills/flashduty-admin/SKILL.md +++ b/skills/flashduty-admin/SKILL.md @@ -1,7 +1,7 @@ --- name: flashduty-admin version: 1.0.0 -description: "Flashduty administration: list teams and members, search audit logs for compliance and investigation. Commands: team list, member list, audit search. Use when looking up person IDs or team IDs for other commands, finding contact information, searching who performed specific actions, or reviewing audit trails for compliance." +description: "Flashduty administration: manage teams (list, get, create, update, delete), list members, and search audit logs for compliance and investigation. Commands: team list/get/create/update/delete, member list, audit search. Use when managing team structure, looking up person IDs or team IDs for other commands, finding contact information, searching who performed specific actions, or reviewing audit trails for compliance." metadata: requires: bins: ["flashduty"] @@ -28,11 +28,82 @@ flashduty team list [flags] | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--name` | string | | Search by team name | +| `--name` | string | | Search by team name substring | | `--page` | int | 1 | Page number | +| `--limit` | int | 20 | Page size, max 100 | +| `--orderby` | string | | Sort field: `created_at`, `updated_at`, `team_name` | +| `--asc` | bool | false | Sort in ascending order | +| `--person-id` | int | 0 | Filter teams by member ID | Output columns: ID, NAME, MEMBERS. +### team get + +Get detailed information about a specific team. Specify the team by exactly one of `--id`, `--name`, or `--ref-id`. + +```bash +flashduty team get --id +flashduty team get --name "SRE Team" +flashduty team get --ref-id "hr-dept-42" +``` + +| Flag | Type | Description | +|------|------|-------------| +| `--id` | int | Team ID | +| `--name` | string | Team name (exact match) | +| `--ref-id` | string | External reference ID | + +Output fields: ID, Name, Description, Status, Ref ID, Members, Created, Updated, Created By, Updated By. + +### team create + +Create a new team. The `--name` flag is required and must be unique (1-39 characters). + +```bash +flashduty team create --name "SRE Team" [flags] +``` + +| Flag | Type | Description | +|------|------|-------------| +| `--name` | string | **Required.** Team name (1-39 characters) | +| `--description` | string | Team description (max 500 characters) | +| `--person-ids` | string | Comma-separated member person IDs | +| `--emails` | string | Comma-separated email addresses to invite | +| `--ref-id` | string | External reference ID for HR system integration | + +### team update + +Update an existing team. The `--id` flag is required. **WARNING:** `--person-ids` replaces the entire member list. Use `team get` to see current members before updating. + +```bash +flashduty team update --id [flags] +``` + +| Flag | Type | Description | +|------|------|-------------| +| `--id` | int | **Required.** Team ID | +| `--name` | string | New team name (1-39 characters) | +| `--description` | string | New description (max 500 characters) | +| `--person-ids` | string | Comma-separated member person IDs (replaces entire member list) | +| `--emails` | string | Comma-separated email addresses to invite | +| `--ref-id` | string | External reference ID | + +### team delete + +Permanently delete a team. Specify the team by exactly one of `--id`, `--name`, or `--ref-id`. This action is **irreversible**. You will be prompted for confirmation unless `--force` is set. + +```bash +flashduty team delete --id +flashduty team delete --name "Old Team" --force +``` + +| Flag | Type | Description | +|------|------|-------------| +| `--id` | int | Team ID | +| `--name` | string | Team name | +| `--ref-id` | string | External reference ID | +| `--force` | bool | Skip confirmation prompt | + ### member list List organization members with contact details and status. @@ -107,14 +178,44 @@ flashduty team list # Search for a specific team by name flashduty team list --name "Platform" + +# Get full detail for a specific team +flashduty team get --id 123 + +# Look up a team by external reference ID +flashduty team get --ref-id "hr-dept-42" +``` + +### Team Lifecycle Management + +```bash +# Create a new team with initial members +flashduty team create --name "SRE Team" --description "Site Reliability" --person-ids 1,2,3 + +# Create a team and invite members by email +flashduty team create --name "Backend Team" --emails alice@example.com,bob@example.com + +# Rename a team +flashduty team update --id 123 --name "Platform SRE" + +# Replace the entire member list (check current members first!) +flashduty team get --id 123 +flashduty team update --id 123 --person-ids 1,2,3,4,5 + +# Delete a team (prompts for confirmation) +flashduty team delete --id 123 + +# Delete without confirmation (for scripting) +flashduty team delete --id 123 --force ``` ## Key Concepts - **Member IDs** (int64) are used across many commands: incident assign/reassign, audit filters, oncall schedules. - **Team IDs** (int64) are used for filtering: oncall schedules, postmortem list, channels. +- **Team update replaces members** -- `--person-ids` is a full replacement, not an append. Always check current members with `team get` before updating. +- **Team delete is irreversible** -- requires confirmation in interactive mode; requires `--force` in non-interactive (CI/scripted) mode. - **Audit logs** track all mutations in the system -- useful for compliance, incident investigation, and change tracking. -- All admin commands in the CLI are **read-only**. ## Cross-References diff --git a/skills/flashduty-shared/SKILL.md b/skills/flashduty-shared/SKILL.md index 3d7bad6..5ff4f8c 100644 --- a/skills/flashduty-shared/SKILL.md +++ b/skills/flashduty-shared/SKILL.md @@ -179,6 +179,9 @@ flashduty member list --email "john@example.com" # Find a team ID flashduty team list --name "SRE" +# Get full team detail by ID, name, or ref-id +flashduty team get --id 123 + # Find a channel (collaboration space) ID flashduty channel list --name "Production" @@ -202,10 +205,12 @@ flashduty status-page list 2. **NEVER** merge incidents or alerts without user confirmation -- merges are **irreversible**. 3. **NEVER** snooze incidents unless the user specifies a duration. 4. **NEVER** reassign or reopen incidents without user confirmation. -5. **Always** show what will be affected before executing destructive or mutating operations. -6. When in doubt about severity or scope, **list first, then act**. -7. Prefer **read-only** operations (`list`, `get`, `detail`) unless the user explicitly requests a mutation. -8. For bulk operations (multiple IDs), enumerate the targets and confirm before proceeding. +5. **NEVER** delete a team without explicit user confirmation -- deletion is **irreversible**. +6. **NEVER** update a team's member list without showing the current members first -- `--person-ids` replaces the entire list. +7. **Always** show what will be affected before executing destructive or mutating operations. +8. When in doubt about severity or scope, **list first, then act**. +9. Prefer **read-only** operations (`list`, `get`, `detail`) unless the user explicitly requests a mutation. +10. For bulk operations (multiple IDs), enumerate the targets and confirm before proceeding. ---