Skip to content

Commit f06b81f

Browse files
authored
feat: add team CRUD commands (get, create, update, delete) (#7)
* 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. * 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. * 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. * 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. * 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
1 parent e848ccd commit f06b81f

9 files changed

Lines changed: 530 additions & 30 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513121733-bf978e2213f5
6+
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514040822-dbac13398b6c
77
github.com/spf13/cobra v1.10.2
88
golang.org/x/term v0.42.0
99
gopkg.in/yaml.v3 v3.0.1

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513114317-7c106c52bd36 h1:yrY7iNBkZrcj6qRZsXwCzMTXjKKzDRGlca/QRfNqiZI=
3-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513114317-7c106c52bd36/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
4-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513121733-bf978e2213f5 h1:AwVNa+CrGqSl60Pq2RQKRV+layS5coJQx01Np+34HD4=
5-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260513121733-bf978e2213f5/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
2+
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514040822-dbac13398b6c h1:rGRydfWe+Sao/aEzFKz+CtWgTINTAa0+QLFQG5pA4JU=
3+
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514040822-dbac13398b6c/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
64
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
75
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
86
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

internal/cli/args.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package cli
22

33
import (
4+
"bufio"
45
"fmt"
6+
"os"
57
"strings"
68

79
"github.com/spf13/cobra"
10+
"golang.org/x/term"
811
)
912

1013
// requireArgs returns a positional argument validator that produces descriptive
@@ -20,3 +23,39 @@ func requireArgs(argNames ...string) cobra.PositionalArgs {
2023
return nil
2124
}
2225
}
26+
27+
// requireExactlyOneFlag validates that exactly one of the named flags is set.
28+
func requireExactlyOneFlag(cmd *cobra.Command, flagNames ...string) error {
29+
set := 0
30+
for _, name := range flagNames {
31+
if cmd.Flags().Changed(name) {
32+
set++
33+
}
34+
}
35+
if set != 1 {
36+
return fmt.Errorf("exactly one of --%s must be specified", strings.Join(flagNames, ", --"))
37+
}
38+
return nil
39+
}
40+
41+
// confirmAction prompts the user for confirmation in interactive terminals.
42+
// Returns true if the user confirms, or if running in non-interactive / JSON / --force mode.
43+
func confirmAction(cmd *cobra.Command, message string) bool {
44+
if flagJSON {
45+
return true
46+
}
47+
force, _ := cmd.Flags().GetBool("force")
48+
if force {
49+
return true
50+
}
51+
if !term.IsTerminal(int(os.Stdin.Fd())) {
52+
return false
53+
}
54+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s [y/N]: ", message)
55+
scanner := bufio.NewScanner(cmd.InOrStdin())
56+
if scanner.Scan() {
57+
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
58+
return answer == "y" || answer == "yes"
59+
}
60+
return false
61+
}

internal/cli/command.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"io"
67

@@ -64,3 +65,18 @@ func (ctx *RunContext) PrintTotal(items any, cols []output.Column, total int) er
6465
func (ctx *RunContext) WriteResult(message string) {
6566
writeResult(ctx.Writer, message)
6667
}
68+
69+
// WriteResultJSON outputs structured data as JSON in --json mode,
70+
// or a human-readable message in table mode.
71+
func (ctx *RunContext) WriteResultJSON(data any, humanMessage string) error {
72+
if ctx.JSON {
73+
out, err := json.MarshalIndent(data, "", " ")
74+
if err != nil {
75+
return fmt.Errorf("failed to marshal JSON: %w", err)
76+
}
77+
_, _ = fmt.Fprintln(ctx.Writer, string(out))
78+
return nil
79+
}
80+
_, _ = fmt.Fprintln(ctx.Writer, humanMessage)
81+
return nil
82+
}

internal/cli/command_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ func (m *mockClient) CancelStatusPageMigration(context.Context, string) error {
215215
return fmt.Errorf("mockClient: CancelStatusPageMigration not implemented")
216216
}
217217

218+
// Phase 5: Team Management
219+
func (m *mockClient) GetTeamInfo(context.Context, *flashduty.TeamGetInput) (*flashduty.TeamItem, error) {
220+
return nil, fmt.Errorf("mockClient: GetTeamInfo not implemented")
221+
}
222+
223+
func (m *mockClient) UpsertTeam(context.Context, *flashduty.TeamUpsertInput) (*flashduty.TeamUpsertOutput, error) {
224+
return nil, fmt.Errorf("mockClient: UpsertTeam not implemented")
225+
}
226+
227+
func (m *mockClient) DeleteTeam(context.Context, *flashduty.TeamDeleteInput) error {
228+
return fmt.Errorf("mockClient: DeleteTeam not implemented")
229+
}
230+
218231
// saveAndResetGlobals saves the current state of all global vars that commands
219232
// mutate, resets them to safe defaults, and returns a restore function for
220233
// t.Cleanup.

internal/cli/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ type flashdutyClient interface {
8080
StartStatusPageEmailSubscriberMigration(ctx context.Context, input *flashduty.StartStatusPageEmailSubscriberMigrationInput) (*flashduty.StartStatusPageMigrationOutput, error)
8181
GetStatusPageMigrationStatus(ctx context.Context, jobID string) (*flashduty.StatusPageMigrationJob, error)
8282
CancelStatusPageMigration(ctx context.Context, jobID string) error
83+
84+
// === PHASE 5: Team Management ===
85+
GetTeamInfo(ctx context.Context, input *flashduty.TeamGetInput) (*flashduty.TeamItem, error)
86+
UpsertTeam(ctx context.Context, input *flashduty.TeamUpsertInput) (*flashduty.TeamUpsertOutput, error)
87+
DeleteTeam(ctx context.Context, input *flashduty.TeamDeleteInput) error
8388
}
8489

8590
// newClientFn creates a flashdutyClient. Override in tests to inject a mock.

0 commit comments

Comments
 (0)