From 915a2a41a0168684418c6f28155edf5be5218e98 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 6 Jan 2026 14:26:55 -0800 Subject: [PATCH 01/17] Getting started --- cmd/accounts.go | 743 +++++++++++++++++++++++++++++++++++++++ cmd/checkins.go | 405 +++++++++++++++++++++ cmd/comments.go | 349 ++++++++++++++++++ cmd/deployments.go | 223 ++++++++++++ cmd/environments.go | 357 +++++++++++++++++++ cmd/statuspages.go | 362 +++++++++++++++++++ cmd/teams.go | 838 ++++++++++++++++++++++++++++++++++++++++++++ cmd/uptime.go | 562 +++++++++++++++++++++++++++++ 8 files changed, 3839 insertions(+) create mode 100644 cmd/accounts.go create mode 100644 cmd/checkins.go create mode 100644 cmd/comments.go create mode 100644 cmd/deployments.go create mode 100644 cmd/environments.go create mode 100644 cmd/statuspages.go create mode 100644 cmd/teams.go create mode 100644 cmd/uptime.go diff --git a/cmd/accounts.go b/cmd/accounts.go new file mode 100644 index 0000000..d2edc7a --- /dev/null +++ b/cmd/accounts.go @@ -0,0 +1,743 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + accountsOutputFormat string + accountID int + accountUserID int + accountUserRole string + accountInvitationID int + accountCLIInputJSON string +) + +// accountsCmd represents the accounts command +var accountsCmd = &cobra.Command{ + Use: "accounts", + Short: "Manage Honeybadger accounts", + Long: `View and manage your Honeybadger accounts, users, and invitations.`, +} + +// accountsListCmd represents the accounts list command +var accountsListCmd = &cobra.Command{ + Use: "list", + Short: "List all accounts", + Long: `List all accounts accessible with your auth token.`, + RunE: func(_ *cobra.Command, _ []string) error { + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + accounts, err := client.Accounts.List(ctx) + if err != nil { + return fmt.Errorf("failed to list accounts: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(accounts, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tEMAIL") + for _, account := range accounts { + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", + account.ID, + account.Name, + account.Email) + } + _ = w.Flush() + } + + return nil + }, +} + +// accountsGetCmd represents the accounts get command +var accountsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get an account by ID", + Long: `Get detailed information about a specific account including quota and API stats.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + account, err := client.Accounts.Get(ctx, accountID) + if err != nil { + return fmt.Errorf("failed to get account: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(account, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Account Details:\n") + fmt.Printf(" ID: %d\n", account.ID) + fmt.Printf(" Name: %s\n", account.Name) + fmt.Printf(" Email: %s\n", account.Email) + if account.Active != nil { + fmt.Printf(" Active: %v\n", *account.Active) + } + if account.Parked != nil { + fmt.Printf(" Parked: %v\n", *account.Parked) + } + if account.QuotaConsumed != nil { + fmt.Printf(" Quota Consumed: %.2f%%\n", *account.QuotaConsumed) + } + } + + return nil + }, +} + +// accountsUsersListCmd represents the accounts users list command +var accountsUsersListCmd = &cobra.Command{ + Use: "list", + Short: "List users for an account", + Long: `List all users associated with an account.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + users, err := client.Accounts.ListUsers(ctx, accountID) + if err != nil { + return fmt.Errorf("failed to list users: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(users, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tEMAIL\tROLE") + for _, user := range users { + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", + user.ID, + user.Name, + user.Email, + user.Role) + } + _ = w.Flush() + } + + return nil + }, +} + +// accountsUsersGetCmd represents the accounts users get command +var accountsUsersGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a user by ID", + Long: `Get detailed information about a specific user in an account.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountUserID == 0 { + return fmt.Errorf("user ID is required. Set it using --user-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + user, err := client.Accounts.GetUser(ctx, accountID, accountUserID) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(user, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("User Details:\n") + fmt.Printf(" ID: %d\n", user.ID) + fmt.Printf(" Name: %s\n", user.Name) + fmt.Printf(" Email: %s\n", user.Email) + fmt.Printf(" Role: %s\n", user.Role) + } + + return nil + }, +} + +// accountsUsersUpdateCmd represents the accounts users update command +var accountsUsersUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a user's role", + Long: `Update a user's role in an account. Valid roles: Member, Billing, Admin, Owner.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountUserID == 0 { + return fmt.Errorf("user ID is required. Set it using --user-id flag") + } + if accountUserRole == "" { + return fmt.Errorf("role is required. Set it using --role flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + user, err := client.Accounts.UpdateUser(ctx, accountID, accountUserID, accountUserRole) + if err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(user, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("User updated successfully!\n") + fmt.Printf(" ID: %d\n", user.ID) + fmt.Printf(" Name: %s\n", user.Name) + fmt.Printf(" Role: %s\n", user.Role) + } + + return nil + }, +} + +// accountsUsersRemoveCmd represents the accounts users remove command +var accountsUsersRemoveCmd = &cobra.Command{ + Use: "remove", + Short: "Remove a user from an account", + Long: `Remove a user from an account. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountUserID == 0 { + return fmt.Errorf("user ID is required. Set it using --user-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Accounts.RemoveUser(ctx, accountID, accountUserID) + if err != nil { + return fmt.Errorf("failed to remove user: %w", err) + } + + fmt.Println("User removed successfully") + return nil + }, +} + +// accountsUsersCmd is the parent command for user operations +var accountsUsersCmd = &cobra.Command{ + Use: "users", + Short: "Manage account users", + Long: `View and manage users in your Honeybadger account.`, +} + +// accountsInvitationsListCmd represents the accounts invitations list command +var accountsInvitationsListCmd = &cobra.Command{ + Use: "list", + Short: "List invitations for an account", + Long: `List all pending invitations for an account.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + invitations, err := client.Accounts.ListInvitations(ctx, accountID) + if err != nil { + return fmt.Errorf("failed to list invitations: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitations, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tEMAIL\tROLE\tCREATED\tACCEPTED") + for _, inv := range invitations { + accepted := "No" + if inv.AcceptedAt != nil { + accepted = inv.AcceptedAt.Format("2006-01-02 15:04") + } + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", + inv.ID, + inv.Email, + inv.Role, + inv.CreatedAt.Format("2006-01-02 15:04"), + accepted) + } + _ = w.Flush() + } + + return nil + }, +} + +// accountsInvitationsGetCmd represents the accounts invitations get command +var accountsInvitationsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get an invitation by ID", + Long: `Get detailed information about a specific invitation.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountInvitationID == 0 { + return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + invitation, err := client.Accounts.GetInvitation(ctx, accountID, accountInvitationID) + if err != nil { + return fmt.Errorf("failed to get invitation: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitation, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Invitation Details:\n") + fmt.Printf(" ID: %d\n", invitation.ID) + fmt.Printf(" Email: %s\n", invitation.Email) + fmt.Printf(" Role: %s\n", invitation.Role) + fmt.Printf(" Token: %s\n", invitation.Token) + fmt.Printf(" Created: %s\n", invitation.CreatedAt.Format("2006-01-02 15:04:05")) + if invitation.AcceptedAt != nil { + fmt.Printf(" Accepted: %s\n", invitation.AcceptedAt.Format("2006-01-02 15:04:05")) + } + if len(invitation.TeamIDs) > 0 { + fmt.Printf(" Team IDs: %v\n", invitation.TeamIDs) + } + } + + return nil + }, +} + +// accountsInvitationsCreateCmd represents the accounts invitations create command +var accountsInvitationsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new invitation", + Long: `Create a new invitation to join an account. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "invitation": { + "email": "user@example.com", + "role": "Member", + "team_ids": [123, 456] + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(accountCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + Invitation hbapi.AccountInvitationParams `json:"invitation"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + invitation, err := client.Accounts.CreateInvitation(ctx, accountID, payload.Invitation) + if err != nil { + return fmt.Errorf("failed to create invitation: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitation, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Invitation created successfully!\n") + fmt.Printf(" ID: %d\n", invitation.ID) + fmt.Printf(" Email: %s\n", invitation.Email) + fmt.Printf(" Role: %s\n", invitation.Role) + fmt.Printf(" Token: %s\n", invitation.Token) + } + + return nil + }, +} + +// accountsInvitationsUpdateCmd represents the accounts invitations update command +var accountsInvitationsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an invitation", + Long: `Update an existing invitation. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "invitation": { + "role": "Admin", + "team_ids": [123] + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountInvitationID == 0 { + return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag") + } + if accountCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(accountCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + Invitation hbapi.AccountInvitationParams `json:"invitation"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + invitation, err := client.Accounts.UpdateInvitation( + ctx, + accountID, + accountInvitationID, + payload.Invitation, + ) + if err != nil { + return fmt.Errorf("failed to update invitation: %w", err) + } + + switch accountsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitation, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Invitation updated successfully!\n") + fmt.Printf(" ID: %d\n", invitation.ID) + fmt.Printf(" Email: %s\n", invitation.Email) + fmt.Printf(" Role: %s\n", invitation.Role) + } + + return nil + }, +} + +// accountsInvitationsDeleteCmd represents the accounts invitations delete command +var accountsInvitationsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete an invitation", + Long: `Delete a pending invitation. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if accountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if accountInvitationID == 0 { + return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Accounts.DeleteInvitation(ctx, accountID, accountInvitationID) + if err != nil { + return fmt.Errorf("failed to delete invitation: %w", err) + } + + fmt.Println("Invitation deleted successfully") + return nil + }, +} + +// accountsInvitationsCmd is the parent command for invitation operations +var accountsInvitationsCmd = &cobra.Command{ + Use: "invitations", + Short: "Manage account invitations", + Long: `View and manage invitations to your Honeybadger account.`, +} + +func init() { + rootCmd.AddCommand(accountsCmd) + + // Add subcommands + accountsCmd.AddCommand(accountsListCmd) + accountsCmd.AddCommand(accountsGetCmd) + accountsCmd.AddCommand(accountsUsersCmd) + accountsCmd.AddCommand(accountsInvitationsCmd) + + // Users subcommands + accountsUsersCmd.AddCommand(accountsUsersListCmd) + accountsUsersCmd.AddCommand(accountsUsersGetCmd) + accountsUsersCmd.AddCommand(accountsUsersUpdateCmd) + accountsUsersCmd.AddCommand(accountsUsersRemoveCmd) + + // Invitations subcommands + accountsInvitationsCmd.AddCommand(accountsInvitationsListCmd) + accountsInvitationsCmd.AddCommand(accountsInvitationsGetCmd) + accountsInvitationsCmd.AddCommand(accountsInvitationsCreateCmd) + accountsInvitationsCmd.AddCommand(accountsInvitationsUpdateCmd) + accountsInvitationsCmd.AddCommand(accountsInvitationsDeleteCmd) + + // Flags for list command + accountsListCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for get command + accountsGetCmd.Flags().IntVar(&accountID, "id", 0, "Account ID") + accountsGetCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = accountsGetCmd.MarkFlagRequired("id") + + // Common account ID flag for users subcommands + accountsUsersCmd.PersistentFlags().IntVar(&accountID, "account-id", 0, "Account ID") + + // Flags for users list + accountsUsersListCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for users get + accountsUsersGetCmd.Flags().IntVar(&accountUserID, "user-id", 0, "User ID") + accountsUsersGetCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = accountsUsersGetCmd.MarkFlagRequired("user-id") + + // Flags for users update + accountsUsersUpdateCmd.Flags().IntVar(&accountUserID, "user-id", 0, "User ID") + accountsUsersUpdateCmd.Flags(). + StringVar(&accountUserRole, "role", "", "New role (Member, Billing, Admin, Owner)") + accountsUsersUpdateCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = accountsUsersUpdateCmd.MarkFlagRequired("user-id") + _ = accountsUsersUpdateCmd.MarkFlagRequired("role") + + // Flags for users remove + accountsUsersRemoveCmd.Flags().IntVar(&accountUserID, "user-id", 0, "User ID") + _ = accountsUsersRemoveCmd.MarkFlagRequired("user-id") + + // Common account ID flag for invitations subcommands + accountsInvitationsCmd.PersistentFlags().IntVar(&accountID, "account-id", 0, "Account ID") + + // Flags for invitations list + accountsInvitationsListCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for invitations get + accountsInvitationsGetCmd.Flags(). + IntVar(&accountInvitationID, "invitation-id", 0, "Invitation ID") + accountsInvitationsGetCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = accountsInvitationsGetCmd.MarkFlagRequired("invitation-id") + + // Flags for invitations create + accountsInvitationsCreateCmd.Flags(). + StringVar(&accountCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + accountsInvitationsCreateCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = accountsInvitationsCreateCmd.MarkFlagRequired("cli-input-json") + + // Flags for invitations update + accountsInvitationsUpdateCmd.Flags(). + IntVar(&accountInvitationID, "invitation-id", 0, "Invitation ID") + accountsInvitationsUpdateCmd.Flags(). + StringVar(&accountCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + accountsInvitationsUpdateCmd.Flags(). + StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = accountsInvitationsUpdateCmd.MarkFlagRequired("invitation-id") + _ = accountsInvitationsUpdateCmd.MarkFlagRequired("cli-input-json") + + // Flags for invitations delete + accountsInvitationsDeleteCmd.Flags(). + IntVar(&accountInvitationID, "invitation-id", 0, "Invitation ID") + _ = accountsInvitationsDeleteCmd.MarkFlagRequired("invitation-id") +} diff --git a/cmd/checkins.go b/cmd/checkins.go new file mode 100644 index 0000000..27cc990 --- /dev/null +++ b/cmd/checkins.go @@ -0,0 +1,405 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + checkinsProjectID int + checkinID int + checkinsOutputFormat string + checkinCLIInputJSON string +) + +// checkinsCmd represents the checkins command +var checkinsCmd = &cobra.Command{ + Use: "checkins", + Short: "Manage Honeybadger check-ins", + Long: `View and manage check-ins (cron job monitoring) for your Honeybadger projects.`, +} + +// checkinsListCmd represents the checkins list command +var checkinsListCmd = &cobra.Command{ + Use: "list", + Short: "List check-ins for a project", + Long: `List all check-ins configured for a specific project.`, + RunE: func(_ *cobra.Command, _ []string) error { + if checkinsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + checkIns, err := client.CheckIns.List(ctx, checkinsProjectID) + if err != nil { + return fmt.Errorf("failed to list check-ins: %w", err) + } + + switch checkinsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(checkIns, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tSLUG\tTYPE\tSCHEDULE\tLAST CHECK-IN") + for _, ci := range checkIns { + schedule := "" + if ci.ScheduleType == "simple" && ci.ReportPeriod != nil { + schedule = *ci.ReportPeriod + } else if ci.ScheduleType == "cron" && ci.CronSchedule != nil { + schedule = *ci.CronSchedule + } + + lastCheckIn := "Never" + if ci.LastCheckInAt != nil { + lastCheckIn = ci.LastCheckInAt.Format("2006-01-02 15:04") + } + + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n", + ci.ID, + ci.Name, + ci.Slug, + ci.ScheduleType, + schedule, + lastCheckIn) + } + _ = w.Flush() + } + + return nil + }, +} + +// checkinsGetCmd represents the checkins get command +var checkinsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a check-in by ID", + Long: `Get detailed information about a specific check-in.`, + RunE: func(_ *cobra.Command, _ []string) error { + if checkinsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if checkinID == 0 { + return fmt.Errorf("check-in ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + checkIn, err := client.CheckIns.Get(ctx, checkinsProjectID, checkinID) + if err != nil { + return fmt.Errorf("failed to get check-in: %w", err) + } + + switch checkinsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(checkIn, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Check-in Details:\n") + fmt.Printf(" ID: %d\n", checkIn.ID) + fmt.Printf(" Name: %s\n", checkIn.Name) + fmt.Printf(" Slug: %s\n", checkIn.Slug) + fmt.Printf(" Schedule Type: %s\n", checkIn.ScheduleType) + if checkIn.ReportPeriod != nil { + fmt.Printf(" Report Period: %s\n", *checkIn.ReportPeriod) + } + if checkIn.GracePeriod != nil { + fmt.Printf(" Grace Period: %s\n", *checkIn.GracePeriod) + } + if checkIn.CronSchedule != nil { + fmt.Printf(" Cron Schedule: %s\n", *checkIn.CronSchedule) + } + if checkIn.CronTimezone != nil { + fmt.Printf(" Cron Timezone: %s\n", *checkIn.CronTimezone) + } + fmt.Printf(" Project ID: %d\n", checkIn.ProjectID) + fmt.Printf(" Created: %s\n", checkIn.CreatedAt.Format("2006-01-02 15:04:05")) + if checkIn.LastCheckInAt != nil { + fmt.Printf( + " Last Check-in: %s\n", + checkIn.LastCheckInAt.Format("2006-01-02 15:04:05"), + ) + } + } + + return nil + }, +} + +// checkinsCreateCmd represents the checkins create command +var checkinsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new check-in", + Long: `Create a new check-in for a project. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload for simple schedule: +{ + "check_in": { + "name": "Daily Backup", + "slug": "daily-backup", + "schedule_type": "simple", + "report_period": "1 day", + "grace_period": "15 minutes" + } +} + +Example JSON payload for cron schedule: +{ + "check_in": { + "name": "Hourly Task", + "slug": "hourly-task", + "schedule_type": "cron", + "cron_schedule": "0 * * * *", + "cron_timezone": "America/New_York" + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if checkinsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if checkinCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(checkinCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + CheckIn hbapi.CheckInParams `json:"check_in"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + checkIn, err := client.CheckIns.Create(ctx, checkinsProjectID, payload.CheckIn) + if err != nil { + return fmt.Errorf("failed to create check-in: %w", err) + } + + switch checkinsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(checkIn, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Check-in created successfully!\n") + fmt.Printf(" ID: %d\n", checkIn.ID) + fmt.Printf(" Name: %s\n", checkIn.Name) + fmt.Printf(" Slug: %s\n", checkIn.Slug) + } + + return nil + }, +} + +// checkinsUpdateCmd represents the checkins update command +var checkinsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing check-in", + Long: `Update an existing check-in's settings. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "check_in": { + "name": "Updated Name", + "report_period": "2 days" + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if checkinsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if checkinID == 0 { + return fmt.Errorf("check-in ID is required. Set it using --id flag") + } + if checkinCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(checkinCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + CheckIn hbapi.CheckInParams `json:"check_in"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + checkIn, err := client.CheckIns.Update(ctx, checkinsProjectID, checkinID, payload.CheckIn) + if err != nil { + return fmt.Errorf("failed to update check-in: %w", err) + } + + switch checkinsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(checkIn, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Check-in updated successfully!\n") + fmt.Printf(" ID: %d\n", checkIn.ID) + fmt.Printf(" Name: %s\n", checkIn.Name) + fmt.Printf(" Slug: %s\n", checkIn.Slug) + } + + return nil + }, +} + +// checkinsDeleteCmd represents the checkins delete command +var checkinsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a check-in", + Long: `Delete a check-in by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if checkinsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if checkinID == 0 { + return fmt.Errorf("check-in ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.CheckIns.Delete(ctx, checkinsProjectID, checkinID) + if err != nil { + return fmt.Errorf("failed to delete check-in: %w", err) + } + + fmt.Println("Check-in deleted successfully") + return nil + }, +} + +func init() { + rootCmd.AddCommand(checkinsCmd) + checkinsCmd.AddCommand(checkinsListCmd) + checkinsCmd.AddCommand(checkinsGetCmd) + checkinsCmd.AddCommand(checkinsCreateCmd) + checkinsCmd.AddCommand(checkinsUpdateCmd) + checkinsCmd.AddCommand(checkinsDeleteCmd) + + // Common flags + checkinsCmd.PersistentFlags().IntVar(&checkinsProjectID, "project-id", 0, "Project ID") + + // Flags for list command + checkinsListCmd.Flags(). + StringVarP(&checkinsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for get command + checkinsGetCmd.Flags().IntVar(&checkinID, "id", 0, "Check-in ID") + checkinsGetCmd.Flags(). + StringVarP(&checkinsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = checkinsGetCmd.MarkFlagRequired("id") + + // Flags for create command + checkinsCreateCmd.Flags(). + StringVar(&checkinCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + checkinsCreateCmd.Flags(). + StringVarP(&checkinsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = checkinsCreateCmd.MarkFlagRequired("cli-input-json") + + // Flags for update command + checkinsUpdateCmd.Flags().IntVar(&checkinID, "id", 0, "Check-in ID") + checkinsUpdateCmd.Flags(). + StringVar(&checkinCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + checkinsUpdateCmd.Flags(). + StringVarP(&checkinsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = checkinsUpdateCmd.MarkFlagRequired("id") + _ = checkinsUpdateCmd.MarkFlagRequired("cli-input-json") + + // Flags for delete command + checkinsDeleteCmd.Flags().IntVar(&checkinID, "id", 0, "Check-in ID") + _ = checkinsDeleteCmd.MarkFlagRequired("id") +} diff --git a/cmd/comments.go b/cmd/comments.go new file mode 100644 index 0000000..a336ee2 --- /dev/null +++ b/cmd/comments.go @@ -0,0 +1,349 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + commentsProjectID int + commentsFaultID int + commentID int + commentsOutputFormat string + commentBody string +) + +// commentsCmd represents the comments command +var commentsCmd = &cobra.Command{ + Use: "comments", + Short: "Manage fault comments", + Long: `View and manage comments on faults in your Honeybadger projects.`, +} + +// commentsListCmd represents the comments list command +var commentsListCmd = &cobra.Command{ + Use: "list", + Short: "List comments for a fault", + Long: `List all comments on a specific fault.`, + RunE: func(_ *cobra.Command, _ []string) error { + if commentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if commentsFaultID == 0 { + return fmt.Errorf("fault ID is required. Set it using --fault-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + comments, err := client.Comments.List(ctx, commentsProjectID, commentsFaultID) + if err != nil { + return fmt.Errorf("failed to list comments: %w", err) + } + + switch commentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(comments, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tAUTHOR\tEVENT\tCREATED\tBODY") + for _, c := range comments { + author := "System" + if c.Author != nil { + author = c.Author.Name + } + + body := c.Body + if len(body) > 40 { + body = body[:37] + "..." + } + + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", + c.ID, + author, + c.Event, + c.CreatedAt.Format("2006-01-02 15:04"), + body) + } + _ = w.Flush() + } + + return nil + }, +} + +// commentsGetCmd represents the comments get command +var commentsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a comment by ID", + Long: `Get detailed information about a specific comment.`, + RunE: func(_ *cobra.Command, _ []string) error { + if commentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if commentsFaultID == 0 { + return fmt.Errorf("fault ID is required. Set it using --fault-id flag") + } + if commentID == 0 { + return fmt.Errorf("comment ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + comment, err := client.Comments.Get(ctx, commentsProjectID, commentsFaultID, commentID) + if err != nil { + return fmt.Errorf("failed to get comment: %w", err) + } + + switch commentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(comment, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Comment Details:\n") + fmt.Printf(" ID: %d\n", comment.ID) + fmt.Printf(" Fault ID: %d\n", comment.FaultID) + fmt.Printf(" Event: %s\n", comment.Event) + fmt.Printf(" Source: %s\n", comment.Source) + if comment.Author != nil { + fmt.Printf(" Author: %s <%s>\n", comment.Author.Name, comment.Author.Email) + } + fmt.Printf(" Created: %s\n", comment.CreatedAt.Format("2006-01-02 15:04:05")) + fmt.Printf(" Notices Count: %d\n", comment.NoticesCount) + fmt.Printf(" Body:\n %s\n", comment.Body) + } + + return nil + }, +} + +// commentsCreateCmd represents the comments create command +var commentsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new comment", + Long: `Create a new comment on a fault.`, + RunE: func(_ *cobra.Command, _ []string) error { + if commentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if commentsFaultID == 0 { + return fmt.Errorf("fault ID is required. Set it using --fault-id flag") + } + if commentBody == "" { + return fmt.Errorf("comment body is required. Set it using --body flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + comment, err := client.Comments.Create(ctx, commentsProjectID, commentsFaultID, commentBody) + if err != nil { + return fmt.Errorf("failed to create comment: %w", err) + } + + switch commentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(comment, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Comment created successfully!\n") + fmt.Printf(" ID: %d\n", comment.ID) + fmt.Printf(" Body: %s\n", comment.Body) + } + + return nil + }, +} + +// commentsUpdateCmd represents the comments update command +var commentsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing comment", + Long: `Update the body of an existing comment.`, + RunE: func(_ *cobra.Command, _ []string) error { + if commentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if commentsFaultID == 0 { + return fmt.Errorf("fault ID is required. Set it using --fault-id flag") + } + if commentID == 0 { + return fmt.Errorf("comment ID is required. Set it using --id flag") + } + if commentBody == "" { + return fmt.Errorf("comment body is required. Set it using --body flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + comment, err := client.Comments.Update( + ctx, + commentsProjectID, + commentsFaultID, + commentID, + commentBody, + ) + if err != nil { + return fmt.Errorf("failed to update comment: %w", err) + } + + switch commentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(comment, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Comment updated successfully!\n") + fmt.Printf(" ID: %d\n", comment.ID) + fmt.Printf(" Body: %s\n", comment.Body) + } + + return nil + }, +} + +// commentsDeleteCmd represents the comments delete command +var commentsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a comment", + Long: `Delete a comment by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if commentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if commentsFaultID == 0 { + return fmt.Errorf("fault ID is required. Set it using --fault-id flag") + } + if commentID == 0 { + return fmt.Errorf("comment ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Comments.Delete(ctx, commentsProjectID, commentsFaultID, commentID) + if err != nil { + return fmt.Errorf("failed to delete comment: %w", err) + } + + fmt.Println("Comment deleted successfully") + return nil + }, +} + +func init() { + rootCmd.AddCommand(commentsCmd) + commentsCmd.AddCommand(commentsListCmd) + commentsCmd.AddCommand(commentsGetCmd) + commentsCmd.AddCommand(commentsCreateCmd) + commentsCmd.AddCommand(commentsUpdateCmd) + commentsCmd.AddCommand(commentsDeleteCmd) + + // Common flags + commentsCmd.PersistentFlags().IntVar(&commentsProjectID, "project-id", 0, "Project ID") + commentsCmd.PersistentFlags().IntVar(&commentsFaultID, "fault-id", 0, "Fault ID") + + // Flags for list command + commentsListCmd.Flags(). + StringVarP(&commentsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for get command + commentsGetCmd.Flags().IntVar(&commentID, "id", 0, "Comment ID") + commentsGetCmd.Flags(). + StringVarP(&commentsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = commentsGetCmd.MarkFlagRequired("id") + + // Flags for create command + commentsCreateCmd.Flags().StringVar(&commentBody, "body", "", "Comment body text") + commentsCreateCmd.Flags(). + StringVarP(&commentsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = commentsCreateCmd.MarkFlagRequired("body") + + // Flags for update command + commentsUpdateCmd.Flags().IntVar(&commentID, "id", 0, "Comment ID") + commentsUpdateCmd.Flags().StringVar(&commentBody, "body", "", "New comment body text") + commentsUpdateCmd.Flags(). + StringVarP(&commentsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = commentsUpdateCmd.MarkFlagRequired("id") + _ = commentsUpdateCmd.MarkFlagRequired("body") + + // Flags for delete command + commentsDeleteCmd.Flags().IntVar(&commentID, "id", 0, "Comment ID") + _ = commentsDeleteCmd.MarkFlagRequired("id") +} diff --git a/cmd/deployments.go b/cmd/deployments.go new file mode 100644 index 0000000..6c8c9a2 --- /dev/null +++ b/cmd/deployments.go @@ -0,0 +1,223 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + deploymentsProjectID int + deploymentID int + deploymentsOutputFormat string + deploymentsEnvironment string + deploymentsLocalUser string + deploymentsCreatedAfter int64 + deploymentsCreatedBefore int64 + deploymentsLimit int +) + +// deploymentsCmd represents the deployments command +var deploymentsCmd = &cobra.Command{ + Use: "deployments", + Short: "View and manage deployments", + Long: `View and manage deployment records for your Honeybadger projects.`, +} + +// deploymentsListCmd represents the deployments list command +var deploymentsListCmd = &cobra.Command{ + Use: "list", + Short: "List deployments for a project", + Long: `List all deployments for a specific project with optional filtering.`, + RunE: func(_ *cobra.Command, _ []string) error { + if deploymentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + options := hbapi.DeploymentListOptions{ + Environment: deploymentsEnvironment, + LocalUsername: deploymentsLocalUser, + CreatedAfter: deploymentsCreatedAfter, + CreatedBefore: deploymentsCreatedBefore, + Limit: deploymentsLimit, + } + + ctx := context.Background() + deployments, err := client.Deployments.List(ctx, deploymentsProjectID, options) + if err != nil { + return fmt.Errorf("failed to list deployments: %w", err) + } + + switch deploymentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(deployments, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tENVIRONMENT\tREVISION\tUSER\tCREATED") + for _, d := range deployments { + revision := d.Revision + if len(revision) > 12 { + revision = revision[:12] + } + + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", + d.ID, + d.Environment, + revision, + d.LocalUsername, + d.CreatedAt.Format("2006-01-02 15:04")) + } + _ = w.Flush() + } + + return nil + }, +} + +// deploymentsGetCmd represents the deployments get command +var deploymentsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a deployment by ID", + Long: `Get detailed information about a specific deployment.`, + RunE: func(_ *cobra.Command, _ []string) error { + if deploymentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if deploymentID == 0 { + return fmt.Errorf("deployment ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + deployment, err := client.Deployments.Get(ctx, deploymentsProjectID, deploymentID) + if err != nil { + return fmt.Errorf("failed to get deployment: %w", err) + } + + switch deploymentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(deployment, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Deployment Details:\n") + fmt.Printf(" ID: %d\n", deployment.ID) + fmt.Printf(" Environment: %s\n", deployment.Environment) + fmt.Printf(" Revision: %s\n", deployment.Revision) + fmt.Printf(" Repository: %s\n", deployment.Repository) + fmt.Printf(" Local Username: %s\n", deployment.LocalUsername) + fmt.Printf(" Project ID: %d\n", deployment.ProjectID) + fmt.Printf(" Created: %s\n", deployment.CreatedAt.Format("2006-01-02 15:04:05")) + } + + return nil + }, +} + +// deploymentsDeleteCmd represents the deployments delete command +var deploymentsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a deployment", + Long: `Delete a deployment record by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if deploymentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if deploymentID == 0 { + return fmt.Errorf("deployment ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Deployments.Delete(ctx, deploymentsProjectID, deploymentID) + if err != nil { + return fmt.Errorf("failed to delete deployment: %w", err) + } + + fmt.Println("Deployment deleted successfully") + return nil + }, +} + +func init() { + rootCmd.AddCommand(deploymentsCmd) + deploymentsCmd.AddCommand(deploymentsListCmd) + deploymentsCmd.AddCommand(deploymentsGetCmd) + deploymentsCmd.AddCommand(deploymentsDeleteCmd) + + // Common flags + deploymentsCmd.PersistentFlags().IntVar(&deploymentsProjectID, "project-id", 0, "Project ID") + + // Flags for list command + deploymentsListCmd.Flags(). + StringVarP(&deploymentsOutputFormat, "output", "o", "table", "Output format (table or json)") + deploymentsListCmd.Flags(). + StringVarP(&deploymentsEnvironment, "environment", "e", "", "Filter by environment") + deploymentsListCmd.Flags(). + StringVar(&deploymentsLocalUser, "local-user", "", "Filter by local username") + deploymentsListCmd.Flags(). + Int64Var(&deploymentsCreatedAfter, "created-after", 0, "Filter by creation time (Unix timestamp)") + deploymentsListCmd.Flags(). + Int64Var(&deploymentsCreatedBefore, "created-before", 0, "Filter by creation time (Unix timestamp)") + deploymentsListCmd.Flags(). + IntVar(&deploymentsLimit, "limit", 25, "Maximum number of deployments to return (max 25)") + + // Flags for get command + deploymentsGetCmd.Flags().IntVar(&deploymentID, "id", 0, "Deployment ID") + deploymentsGetCmd.Flags(). + StringVarP(&deploymentsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = deploymentsGetCmd.MarkFlagRequired("id") + + // Flags for delete command + deploymentsDeleteCmd.Flags().IntVar(&deploymentID, "id", 0, "Deployment ID") + _ = deploymentsDeleteCmd.MarkFlagRequired("id") +} diff --git a/cmd/environments.go b/cmd/environments.go new file mode 100644 index 0000000..fcf2ab9 --- /dev/null +++ b/cmd/environments.go @@ -0,0 +1,357 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + environmentsProjectID int + environmentID int + environmentsOutputFormat string + environmentCLIInputJSON string +) + +// environmentsCmd represents the environments command +var environmentsCmd = &cobra.Command{ + Use: "environments", + Short: "Manage project environments", + Long: `View and manage environments for your Honeybadger projects.`, +} + +// environmentsListCmd represents the environments list command +var environmentsListCmd = &cobra.Command{ + Use: "list", + Short: "List environments for a project", + Long: `List all environments configured for a specific project.`, + RunE: func(_ *cobra.Command, _ []string) error { + if environmentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + environments, err := client.Environments.List(ctx, environmentsProjectID) + if err != nil { + return fmt.Errorf("failed to list environments: %w", err) + } + + switch environmentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(environments, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tNOTIFICATIONS\tCREATED") + for _, env := range environments { + notifications := "No" + if env.Notifications { + notifications = "Yes" + } + + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", + env.ID, + env.Name, + notifications, + env.CreatedAt.Format("2006-01-02 15:04")) + } + _ = w.Flush() + } + + return nil + }, +} + +// environmentsGetCmd represents the environments get command +var environmentsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get an environment by ID", + Long: `Get detailed information about a specific environment.`, + RunE: func(_ *cobra.Command, _ []string) error { + if environmentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if environmentID == 0 { + return fmt.Errorf("environment ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + environment, err := client.Environments.Get(ctx, environmentsProjectID, environmentID) + if err != nil { + return fmt.Errorf("failed to get environment: %w", err) + } + + switch environmentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(environment, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Environment Details:\n") + fmt.Printf(" ID: %d\n", environment.ID) + fmt.Printf(" Name: %s\n", environment.Name) + fmt.Printf(" Project ID: %d\n", environment.ProjectID) + fmt.Printf(" Notifications: %v\n", environment.Notifications) + fmt.Printf(" Created: %s\n", environment.CreatedAt.Format("2006-01-02 15:04:05")) + fmt.Printf(" Updated: %s\n", environment.UpdatedAt.Format("2006-01-02 15:04:05")) + } + + return nil + }, +} + +// environmentsCreateCmd represents the environments create command +var environmentsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new environment", + Long: `Create a new environment for a project. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "environment": { + "name": "staging", + "notifications": true + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if environmentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if environmentCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(environmentCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + Environment hbapi.EnvironmentParams `json:"environment"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + environment, err := client.Environments.Create( + ctx, + environmentsProjectID, + payload.Environment, + ) + if err != nil { + return fmt.Errorf("failed to create environment: %w", err) + } + + switch environmentsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(environment, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Environment created successfully!\n") + fmt.Printf(" ID: %d\n", environment.ID) + fmt.Printf(" Name: %s\n", environment.Name) + } + + return nil + }, +} + +// environmentsUpdateCmd represents the environments update command +var environmentsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing environment", + Long: `Update an existing environment's settings. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "environment": { + "name": "production", + "notifications": false + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if environmentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if environmentID == 0 { + return fmt.Errorf("environment ID is required. Set it using --id flag") + } + if environmentCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(environmentCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + Environment hbapi.EnvironmentParams `json:"environment"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + err = client.Environments.Update( + ctx, + environmentsProjectID, + environmentID, + payload.Environment, + ) + if err != nil { + return fmt.Errorf("failed to update environment: %w", err) + } + + fmt.Println("Environment updated successfully") + return nil + }, +} + +// environmentsDeleteCmd represents the environments delete command +var environmentsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete an environment", + Long: `Delete an environment by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if environmentsProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if environmentID == 0 { + return fmt.Errorf("environment ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Environments.Delete(ctx, environmentsProjectID, environmentID) + if err != nil { + return fmt.Errorf("failed to delete environment: %w", err) + } + + fmt.Println("Environment deleted successfully") + return nil + }, +} + +func init() { + rootCmd.AddCommand(environmentsCmd) + environmentsCmd.AddCommand(environmentsListCmd) + environmentsCmd.AddCommand(environmentsGetCmd) + environmentsCmd.AddCommand(environmentsCreateCmd) + environmentsCmd.AddCommand(environmentsUpdateCmd) + environmentsCmd.AddCommand(environmentsDeleteCmd) + + // Common flags + environmentsCmd.PersistentFlags().IntVar(&environmentsProjectID, "project-id", 0, "Project ID") + + // Flags for list command + environmentsListCmd.Flags(). + StringVarP(&environmentsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for get command + environmentsGetCmd.Flags().IntVar(&environmentID, "id", 0, "Environment ID") + environmentsGetCmd.Flags(). + StringVarP(&environmentsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = environmentsGetCmd.MarkFlagRequired("id") + + // Flags for create command + environmentsCreateCmd.Flags(). + StringVar(&environmentCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + environmentsCreateCmd.Flags(). + StringVarP(&environmentsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = environmentsCreateCmd.MarkFlagRequired("cli-input-json") + + // Flags for update command + environmentsUpdateCmd.Flags().IntVar(&environmentID, "id", 0, "Environment ID") + environmentsUpdateCmd.Flags(). + StringVar(&environmentCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + _ = environmentsUpdateCmd.MarkFlagRequired("id") + _ = environmentsUpdateCmd.MarkFlagRequired("cli-input-json") + + // Flags for delete command + environmentsDeleteCmd.Flags().IntVar(&environmentID, "id", 0, "Environment ID") + _ = environmentsDeleteCmd.MarkFlagRequired("id") +} diff --git a/cmd/statuspages.go b/cmd/statuspages.go new file mode 100644 index 0000000..0027f09 --- /dev/null +++ b/cmd/statuspages.go @@ -0,0 +1,362 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + statuspagesAccountID int + statuspageID int + statuspagesOutputFormat string + statuspageCLIInputJSON string +) + +// statuspagesCmd represents the statuspages command +var statuspagesCmd = &cobra.Command{ + Use: "statuspages", + Short: "Manage status pages", + Long: `View and manage status pages for your Honeybadger accounts.`, +} + +// statuspagesListCmd represents the statuspages list command +var statuspagesListCmd = &cobra.Command{ + Use: "list", + Short: "List status pages for an account", + Long: `List all status pages configured for a specific account.`, + RunE: func(_ *cobra.Command, _ []string) error { + if statuspagesAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + statusPages, err := client.StatusPages.List(ctx, statuspagesAccountID) + if err != nil { + return fmt.Errorf("failed to list status pages: %w", err) + } + + switch statuspagesOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(statusPages, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tURL\tSITES\tCHECK-INS") + for _, sp := range statusPages { + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\n", + sp.ID, + sp.Name, + sp.URL, + len(sp.Sites), + len(sp.CheckIns)) + } + _ = w.Flush() + } + + return nil + }, +} + +// statuspagesGetCmd represents the statuspages get command +var statuspagesGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a status page by ID", + Long: `Get detailed information about a specific status page.`, + RunE: func(_ *cobra.Command, _ []string) error { + if statuspagesAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if statuspageID == 0 { + return fmt.Errorf("status page ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + statusPage, err := client.StatusPages.Get(ctx, statuspagesAccountID, statuspageID) + if err != nil { + return fmt.Errorf("failed to get status page: %w", err) + } + + switch statuspagesOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(statusPage, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Status Page Details:\n") + fmt.Printf(" ID: %d\n", statusPage.ID) + fmt.Printf(" Name: %s\n", statusPage.Name) + fmt.Printf(" URL: %s\n", statusPage.URL) + fmt.Printf(" Account ID: %d\n", statusPage.AccountID) + if statusPage.Domain != nil { + fmt.Printf(" Domain: %s\n", *statusPage.Domain) + } + fmt.Printf(" Created: %s\n", statusPage.CreatedAt.Format("2006-01-02 15:04:05")) + if statusPage.DomainVerifiedAt != nil { + fmt.Printf( + " Domain Verified: %s\n", + statusPage.DomainVerifiedAt.Format("2006-01-02 15:04:05"), + ) + } + if len(statusPage.Sites) > 0 { + fmt.Printf(" Sites: %v\n", statusPage.Sites) + } + if len(statusPage.CheckIns) > 0 { + fmt.Printf(" Check-ins: %v\n", statusPage.CheckIns) + } + } + + return nil + }, +} + +// statuspagesCreateCmd represents the statuspages create command +var statuspagesCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new status page", + Long: `Create a new status page for an account. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "status_page": { + "name": "My Status Page", + "domain": "status.example.com", + "sites": ["site-id-1", "site-id-2"], + "check_ins": ["checkin-slug-1"], + "hide_branding": false + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if statuspagesAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if statuspageCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(statuspageCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + StatusPage hbapi.StatusPageParams `json:"status_page"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + statusPage, err := client.StatusPages.Create(ctx, statuspagesAccountID, payload.StatusPage) + if err != nil { + return fmt.Errorf("failed to create status page: %w", err) + } + + switch statuspagesOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(statusPage, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Status page created successfully!\n") + fmt.Printf(" ID: %d\n", statusPage.ID) + fmt.Printf(" Name: %s\n", statusPage.Name) + fmt.Printf(" URL: %s\n", statusPage.URL) + } + + return nil + }, +} + +// statuspagesUpdateCmd represents the statuspages update command +var statuspagesUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing status page", + Long: `Update an existing status page's settings. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "status_page": { + "name": "Updated Status Page", + "sites": ["site-id-1", "site-id-3"] + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if statuspagesAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if statuspageID == 0 { + return fmt.Errorf("status page ID is required. Set it using --id flag") + } + if statuspageCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(statuspageCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + StatusPage hbapi.StatusPageParams `json:"status_page"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + err = client.StatusPages.Update(ctx, statuspagesAccountID, statuspageID, payload.StatusPage) + if err != nil { + return fmt.Errorf("failed to update status page: %w", err) + } + + fmt.Println("Status page updated successfully") + return nil + }, +} + +// statuspagesDeleteCmd represents the statuspages delete command +var statuspagesDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a status page", + Long: `Delete a status page by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if statuspagesAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if statuspageID == 0 { + return fmt.Errorf("status page ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.StatusPages.Delete(ctx, statuspagesAccountID, statuspageID) + if err != nil { + return fmt.Errorf("failed to delete status page: %w", err) + } + + fmt.Println("Status page deleted successfully") + return nil + }, +} + +func init() { + rootCmd.AddCommand(statuspagesCmd) + statuspagesCmd.AddCommand(statuspagesListCmd) + statuspagesCmd.AddCommand(statuspagesGetCmd) + statuspagesCmd.AddCommand(statuspagesCreateCmd) + statuspagesCmd.AddCommand(statuspagesUpdateCmd) + statuspagesCmd.AddCommand(statuspagesDeleteCmd) + + // Common flags + statuspagesCmd.PersistentFlags().IntVar(&statuspagesAccountID, "account-id", 0, "Account ID") + + // Flags for list command + statuspagesListCmd.Flags(). + StringVarP(&statuspagesOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for get command + statuspagesGetCmd.Flags().IntVar(&statuspageID, "id", 0, "Status page ID") + statuspagesGetCmd.Flags(). + StringVarP(&statuspagesOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = statuspagesGetCmd.MarkFlagRequired("id") + + // Flags for create command + statuspagesCreateCmd.Flags(). + StringVar(&statuspageCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + statuspagesCreateCmd.Flags(). + StringVarP(&statuspagesOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = statuspagesCreateCmd.MarkFlagRequired("cli-input-json") + + // Flags for update command + statuspagesUpdateCmd.Flags().IntVar(&statuspageID, "id", 0, "Status page ID") + statuspagesUpdateCmd.Flags(). + StringVar(&statuspageCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + _ = statuspagesUpdateCmd.MarkFlagRequired("id") + _ = statuspagesUpdateCmd.MarkFlagRequired("cli-input-json") + + // Flags for delete command + statuspagesDeleteCmd.Flags().IntVar(&statuspageID, "id", 0, "Status page ID") + _ = statuspagesDeleteCmd.MarkFlagRequired("id") +} diff --git a/cmd/teams.go b/cmd/teams.go new file mode 100644 index 0000000..7115bd4 --- /dev/null +++ b/cmd/teams.go @@ -0,0 +1,838 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + teamsAccountID int + teamID int + teamsOutputFormat string + teamName string + teamMemberID int + teamMemberAdmin bool + teamInvitationID int + teamCLIInputJSON string +) + +// teamsCmd represents the teams command +var teamsCmd = &cobra.Command{ + Use: "teams", + Short: "Manage Honeybadger teams", + Long: `View and manage teams, team members, and team invitations.`, +} + +// teamsListCmd represents the teams list command +var teamsListCmd = &cobra.Command{ + Use: "list", + Short: "List teams for an account", + Long: `List all teams for a specific account.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamsAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + teams, err := client.Teams.List(ctx, teamsAccountID) + if err != nil { + return fmt.Errorf("failed to list teams: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(teams, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tCREATED") + for _, team := range teams { + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", + team.ID, + team.Name, + team.CreatedAt.Format("2006-01-02 15:04")) + } + _ = w.Flush() + } + + return nil + }, +} + +// teamsGetCmd represents the teams get command +var teamsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a team by ID", + Long: `Get detailed information about a specific team.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + team, err := client.Teams.Get(ctx, teamID) + if err != nil { + return fmt.Errorf("failed to get team: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(team, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team Details:\n") + fmt.Printf(" ID: %d\n", team.ID) + fmt.Printf(" Name: %s\n", team.Name) + fmt.Printf(" Account ID: %d\n", team.AccountID) + fmt.Printf(" Created: %s\n", team.CreatedAt.Format("2006-01-02 15:04:05")) + } + + return nil + }, +} + +// teamsCreateCmd represents the teams create command +var teamsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new team", + Long: `Create a new team for an account.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamsAccountID == 0 { + return fmt.Errorf("account ID is required. Set it using --account-id flag") + } + if teamName == "" { + return fmt.Errorf("team name is required. Set it using --name flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + team, err := client.Teams.Create(ctx, teamsAccountID, teamName) + if err != nil { + return fmt.Errorf("failed to create team: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(team, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team created successfully!\n") + fmt.Printf(" ID: %d\n", team.ID) + fmt.Printf(" Name: %s\n", team.Name) + } + + return nil + }, +} + +// teamsUpdateCmd represents the teams update command +var teamsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing team", + Long: `Update an existing team's name.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --id flag") + } + if teamName == "" { + return fmt.Errorf("team name is required. Set it using --name flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + team, err := client.Teams.Update(ctx, teamID, teamName) + if err != nil { + return fmt.Errorf("failed to update team: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(team, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team updated successfully!\n") + fmt.Printf(" ID: %d\n", team.ID) + fmt.Printf(" Name: %s\n", team.Name) + } + + return nil + }, +} + +// teamsDeleteCmd represents the teams delete command +var teamsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a team", + Long: `Delete a team by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Teams.Delete(ctx, teamID) + if err != nil { + return fmt.Errorf("failed to delete team: %w", err) + } + + fmt.Println("Team deleted successfully") + return nil + }, +} + +// teamsMembersCmd is the parent command for team member operations +var teamsMembersCmd = &cobra.Command{ + Use: "members", + Short: "Manage team members", + Long: `View and manage members of a team.`, +} + +// teamsMembersListCmd represents the teams members list command +var teamsMembersListCmd = &cobra.Command{ + Use: "list", + Short: "List members of a team", + Long: `List all members of a specific team.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + members, err := client.Teams.ListMembers(ctx, teamID) + if err != nil { + return fmt.Errorf("failed to list team members: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(members, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tEMAIL\tADMIN") + for _, member := range members { + admin := " " + if member.Admin { + admin = "Yes" + } + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", + member.ID, + member.Name, + member.Email, + admin) + } + _ = w.Flush() + } + + return nil + }, +} + +// teamsMembersUpdateCmd represents the teams members update command +var teamsMembersUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a team member's permissions", + Long: `Update a team member's admin status.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + if teamMemberID == 0 { + return fmt.Errorf("member ID is required. Set it using --member-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + member, err := client.Teams.UpdateMember(ctx, teamID, teamMemberID, teamMemberAdmin) + if err != nil { + return fmt.Errorf("failed to update team member: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(member, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team member updated successfully!\n") + fmt.Printf(" ID: %d\n", member.ID) + fmt.Printf(" Name: %s\n", member.Name) + fmt.Printf(" Admin: %v\n", member.Admin) + } + + return nil + }, +} + +// teamsMembersRemoveCmd represents the teams members remove command +var teamsMembersRemoveCmd = &cobra.Command{ + Use: "remove", + Short: "Remove a member from a team", + Long: `Remove a member from a team. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + if teamMemberID == 0 { + return fmt.Errorf("member ID is required. Set it using --member-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Teams.RemoveMember(ctx, teamID, teamMemberID) + if err != nil { + return fmt.Errorf("failed to remove team member: %w", err) + } + + fmt.Println("Team member removed successfully") + return nil + }, +} + +// teamsInvitationsCmd is the parent command for team invitation operations +var teamsInvitationsCmd = &cobra.Command{ + Use: "invitations", + Short: "Manage team invitations", + Long: `View and manage invitations to join a team.`, +} + +// teamsInvitationsListCmd represents the teams invitations list command +var teamsInvitationsListCmd = &cobra.Command{ + Use: "list", + Short: "List invitations for a team", + Long: `List all pending invitations for a team.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + invitations, err := client.Teams.ListInvitations(ctx, teamID) + if err != nil { + return fmt.Errorf("failed to list team invitations: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitations, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tEMAIL\tADMIN\tCREATED\tACCEPTED") + for _, inv := range invitations { + admin := " " + if inv.Admin { + admin = "Yes" + } + accepted := "No" + if inv.AcceptedAt != nil { + accepted = inv.AcceptedAt.Format("2006-01-02 15:04") + } + _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", + inv.ID, + inv.Email, + admin, + inv.CreatedAt.Format("2006-01-02 15:04"), + accepted) + } + _ = w.Flush() + } + + return nil + }, +} + +// teamsInvitationsGetCmd represents the teams invitations get command +var teamsInvitationsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a team invitation by ID", + Long: `Get detailed information about a specific team invitation.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + if teamInvitationID == 0 { + return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + invitation, err := client.Teams.GetInvitation(ctx, teamID, teamInvitationID) + if err != nil { + return fmt.Errorf("failed to get team invitation: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitation, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team Invitation Details:\n") + fmt.Printf(" ID: %d\n", invitation.ID) + fmt.Printf(" Email: %s\n", invitation.Email) + fmt.Printf(" Admin: %v\n", invitation.Admin) + fmt.Printf(" Token: %s\n", invitation.Token) + fmt.Printf(" Created: %s\n", invitation.CreatedAt.Format("2006-01-02 15:04:05")) + if invitation.AcceptedAt != nil { + fmt.Printf(" Accepted: %s\n", invitation.AcceptedAt.Format("2006-01-02 15:04:05")) + } + if invitation.Message != nil { + fmt.Printf(" Message: %s\n", *invitation.Message) + } + } + + return nil + }, +} + +// teamsInvitationsCreateCmd represents the teams invitations create command +var teamsInvitationsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new team invitation", + Long: `Create a new invitation to join a team. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "team_invitation": { + "email": "user@example.com", + "admin": false, + "message": "Welcome to the team!" + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + if teamCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(teamCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + TeamInvitation hbapi.TeamInvitationParams `json:"team_invitation"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + invitation, err := client.Teams.CreateInvitation(ctx, teamID, payload.TeamInvitation) + if err != nil { + return fmt.Errorf("failed to create team invitation: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitation, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team invitation created successfully!\n") + fmt.Printf(" ID: %d\n", invitation.ID) + fmt.Printf(" Email: %s\n", invitation.Email) + fmt.Printf(" Token: %s\n", invitation.Token) + } + + return nil + }, +} + +// teamsInvitationsUpdateCmd represents the teams invitations update command +var teamsInvitationsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a team invitation", + Long: `Update an existing team invitation. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "team_invitation": { + "admin": true + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + if teamInvitationID == 0 { + return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag") + } + if teamCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(teamCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + TeamInvitation hbapi.TeamInvitationParams `json:"team_invitation"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + invitation, err := client.Teams.UpdateInvitation( + ctx, + teamID, + teamInvitationID, + payload.TeamInvitation, + ) + if err != nil { + return fmt.Errorf("failed to update team invitation: %w", err) + } + + switch teamsOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(invitation, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Team invitation updated successfully!\n") + fmt.Printf(" ID: %d\n", invitation.ID) + fmt.Printf(" Email: %s\n", invitation.Email) + fmt.Printf(" Admin: %v\n", invitation.Admin) + } + + return nil + }, +} + +// teamsInvitationsDeleteCmd represents the teams invitations delete command +var teamsInvitationsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a team invitation", + Long: `Delete a pending team invitation. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if teamID == 0 { + return fmt.Errorf("team ID is required. Set it using --team-id flag") + } + if teamInvitationID == 0 { + return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Teams.DeleteInvitation(ctx, teamID, teamInvitationID) + if err != nil { + return fmt.Errorf("failed to delete team invitation: %w", err) + } + + fmt.Println("Team invitation deleted successfully") + return nil + }, +} + +func init() { + rootCmd.AddCommand(teamsCmd) + + // Add subcommands + teamsCmd.AddCommand(teamsListCmd) + teamsCmd.AddCommand(teamsGetCmd) + teamsCmd.AddCommand(teamsCreateCmd) + teamsCmd.AddCommand(teamsUpdateCmd) + teamsCmd.AddCommand(teamsDeleteCmd) + teamsCmd.AddCommand(teamsMembersCmd) + teamsCmd.AddCommand(teamsInvitationsCmd) + + // Members subcommands + teamsMembersCmd.AddCommand(teamsMembersListCmd) + teamsMembersCmd.AddCommand(teamsMembersUpdateCmd) + teamsMembersCmd.AddCommand(teamsMembersRemoveCmd) + + // Invitations subcommands + teamsInvitationsCmd.AddCommand(teamsInvitationsListCmd) + teamsInvitationsCmd.AddCommand(teamsInvitationsGetCmd) + teamsInvitationsCmd.AddCommand(teamsInvitationsCreateCmd) + teamsInvitationsCmd.AddCommand(teamsInvitationsUpdateCmd) + teamsInvitationsCmd.AddCommand(teamsInvitationsDeleteCmd) + + // Flags for list command + teamsListCmd.Flags().IntVar(&teamsAccountID, "account-id", 0, "Account ID") + teamsListCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "table", "Output format (table or json)") + _ = teamsListCmd.MarkFlagRequired("account-id") + + // Flags for get command + teamsGetCmd.Flags().IntVar(&teamID, "id", 0, "Team ID") + teamsGetCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsGetCmd.MarkFlagRequired("id") + + // Flags for create command + teamsCreateCmd.Flags().IntVar(&teamsAccountID, "account-id", 0, "Account ID") + teamsCreateCmd.Flags().StringVar(&teamName, "name", "", "Team name") + teamsCreateCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsCreateCmd.MarkFlagRequired("account-id") + _ = teamsCreateCmd.MarkFlagRequired("name") + + // Flags for update command + teamsUpdateCmd.Flags().IntVar(&teamID, "id", 0, "Team ID") + teamsUpdateCmd.Flags().StringVar(&teamName, "name", "", "New team name") + teamsUpdateCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsUpdateCmd.MarkFlagRequired("id") + _ = teamsUpdateCmd.MarkFlagRequired("name") + + // Flags for delete command + teamsDeleteCmd.Flags().IntVar(&teamID, "id", 0, "Team ID") + _ = teamsDeleteCmd.MarkFlagRequired("id") + + // Common team ID flag for members subcommands + teamsMembersCmd.PersistentFlags().IntVar(&teamID, "team-id", 0, "Team ID") + + // Flags for members list + teamsMembersListCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for members update + teamsMembersUpdateCmd.Flags().IntVar(&teamMemberID, "member-id", 0, "Member ID") + teamsMembersUpdateCmd.Flags().BoolVar(&teamMemberAdmin, "admin", false, "Set admin status") + teamsMembersUpdateCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsMembersUpdateCmd.MarkFlagRequired("member-id") + + // Flags for members remove + teamsMembersRemoveCmd.Flags().IntVar(&teamMemberID, "member-id", 0, "Member ID") + _ = teamsMembersRemoveCmd.MarkFlagRequired("member-id") + + // Common team ID flag for invitations subcommands + teamsInvitationsCmd.PersistentFlags().IntVar(&teamID, "team-id", 0, "Team ID") + + // Flags for invitations list + teamsInvitationsListCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for invitations get + teamsInvitationsGetCmd.Flags().IntVar(&teamInvitationID, "invitation-id", 0, "Invitation ID") + teamsInvitationsGetCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsInvitationsGetCmd.MarkFlagRequired("invitation-id") + + // Flags for invitations create + teamsInvitationsCreateCmd.Flags(). + StringVar(&teamCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + teamsInvitationsCreateCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsInvitationsCreateCmd.MarkFlagRequired("cli-input-json") + + // Flags for invitations update + teamsInvitationsUpdateCmd.Flags().IntVar(&teamInvitationID, "invitation-id", 0, "Invitation ID") + teamsInvitationsUpdateCmd.Flags(). + StringVar(&teamCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + teamsInvitationsUpdateCmd.Flags(). + StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = teamsInvitationsUpdateCmd.MarkFlagRequired("invitation-id") + _ = teamsInvitationsUpdateCmd.MarkFlagRequired("cli-input-json") + + // Flags for invitations delete + teamsInvitationsDeleteCmd.Flags().IntVar(&teamInvitationID, "invitation-id", 0, "Invitation ID") + _ = teamsInvitationsDeleteCmd.MarkFlagRequired("invitation-id") +} diff --git a/cmd/uptime.go b/cmd/uptime.go new file mode 100644 index 0000000..118b6ac --- /dev/null +++ b/cmd/uptime.go @@ -0,0 +1,562 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + hbapi "github.com/honeybadger-io/api-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + uptimeProjectID int + uptimeSiteID string + uptimeOutputFormat string + uptimeCLIInputJSON string + uptimeCreatedAfter int64 + uptimeCreatedBefore int64 + uptimeLimit int +) + +// uptimeCmd represents the uptime command +var uptimeCmd = &cobra.Command{ + Use: "uptime", + Short: "Manage uptime monitoring", + Long: `View and manage uptime monitoring sites, outages, and checks for your Honeybadger projects.`, +} + +// uptimeSitesCmd represents the uptime sites parent command +var uptimeSitesCmd = &cobra.Command{ + Use: "sites", + Short: "Manage uptime sites", + Long: `View and manage uptime monitoring sites.`, +} + +// uptimeSitesListCmd represents the uptime sites list command +var uptimeSitesListCmd = &cobra.Command{ + Use: "list", + Short: "List uptime sites for a project", + Long: `List all uptime monitoring sites configured for a specific project.`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + sites, err := client.Uptime.List(ctx, uptimeProjectID) + if err != nil { + return fmt.Errorf("failed to list uptime sites: %w", err) + } + + switch uptimeOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(sites, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "ID\tNAME\tURL\tSTATE\tACTIVE\tFREQ") + for _, site := range sites { + active := " " + if site.Active { + active = "Yes" + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%dm\n", + site.ID, + site.Name, + site.URL, + site.State, + active, + site.Frequency) + } + _ = w.Flush() + } + + return nil + }, +} + +// uptimeSitesGetCmd represents the uptime sites get command +var uptimeSitesGetCmd = &cobra.Command{ + Use: "get", + Short: "Get an uptime site by ID", + Long: `Get detailed information about a specific uptime site.`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if uptimeSiteID == "" { + return fmt.Errorf("site ID is required. Set it using --site-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + site, err := client.Uptime.Get(ctx, uptimeProjectID, uptimeSiteID) + if err != nil { + return fmt.Errorf("failed to get uptime site: %w", err) + } + + switch uptimeOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(site, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Uptime Site Details:\n") + fmt.Printf(" ID: %s\n", site.ID) + fmt.Printf(" Name: %s\n", site.Name) + fmt.Printf(" URL: %s\n", site.URL) + fmt.Printf(" State: %s\n", site.State) + fmt.Printf(" Active: %v\n", site.Active) + fmt.Printf(" Frequency: %d minutes\n", site.Frequency) + fmt.Printf(" Match Type: %s\n", site.MatchType) + if site.Match != nil { + fmt.Printf(" Match: %s\n", *site.Match) + } + if site.LastCheckedAt != nil { + fmt.Printf(" Last Checked: %s\n", site.LastCheckedAt.Format("2006-01-02 15:04:05")) + } + } + + return nil + }, +} + +// uptimeSitesCreateCmd represents the uptime sites create command +var uptimeSitesCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new uptime site", + Long: `Create a new uptime monitoring site for a project. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "site": { + "name": "My Website", + "url": "https://example.com", + "frequency": 5, + "match_type": "success", + "locations": ["Virginia", "Oregon"], + "validate_ssl": true + } +} + +Available options: +- frequency: 1, 5, or 15 (minutes) +- match_type: "success", "exact", "include", "exclude" +- request_method: "GET", "POST", "PUT", "PATCH", "DELETE" +- locations: "Virginia", "Oregon", "Frankfurt", "Singapore", "London"`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if uptimeCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(uptimeCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + Site hbapi.SiteParams `json:"site"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + site, err := client.Uptime.Create(ctx, uptimeProjectID, payload.Site) + if err != nil { + return fmt.Errorf("failed to create uptime site: %w", err) + } + + switch uptimeOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(site, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Uptime site created successfully!\n") + fmt.Printf(" ID: %s\n", site.ID) + fmt.Printf(" Name: %s\n", site.Name) + fmt.Printf(" URL: %s\n", site.URL) + } + + return nil + }, +} + +// uptimeSitesUpdateCmd represents the uptime sites update command +var uptimeSitesUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing uptime site", + Long: `Update an existing uptime monitoring site's settings. + +The --cli-input-json flag accepts either a JSON string or a file path prefixed with 'file://'. + +Example JSON payload: +{ + "site": { + "name": "Updated Website", + "frequency": 15, + "active": false + } +}`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if uptimeSiteID == "" { + return fmt.Errorf("site ID is required. Set it using --site-id flag") + } + if uptimeCLIInputJSON == "" { + return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + jsonData, err := readJSONInput(uptimeCLIInputJSON) + if err != nil { + return fmt.Errorf("failed to read JSON input: %w", err) + } + + var payload struct { + Site hbapi.SiteParams `json:"site"` + } + if err := json.Unmarshal(jsonData, &payload); err != nil { + return fmt.Errorf("failed to parse JSON payload: %w", err) + } + + ctx := context.Background() + site, err := client.Uptime.Update(ctx, uptimeProjectID, uptimeSiteID, payload.Site) + if err != nil { + return fmt.Errorf("failed to update uptime site: %w", err) + } + + switch uptimeOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(site, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + fmt.Printf("Uptime site updated successfully!\n") + fmt.Printf(" ID: %s\n", site.ID) + fmt.Printf(" Name: %s\n", site.Name) + } + + return nil + }, +} + +// uptimeSitesDeleteCmd represents the uptime sites delete command +var uptimeSitesDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete an uptime site", + Long: `Delete an uptime monitoring site by ID. This action cannot be undone.`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if uptimeSiteID == "" { + return fmt.Errorf("site ID is required. Set it using --site-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + ctx := context.Background() + err := client.Uptime.Delete(ctx, uptimeProjectID, uptimeSiteID) + if err != nil { + return fmt.Errorf("failed to delete uptime site: %w", err) + } + + fmt.Println("Uptime site deleted successfully") + return nil + }, +} + +// uptimeOutagesCmd represents the uptime outages command +var uptimeOutagesCmd = &cobra.Command{ + Use: "outages", + Short: "List outages for a site", + Long: `List outages recorded for a specific uptime monitoring site.`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if uptimeSiteID == "" { + return fmt.Errorf("site ID is required. Set it using --site-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + options := hbapi.OutageListOptions{ + CreatedAfter: uptimeCreatedAfter, + CreatedBefore: uptimeCreatedBefore, + Limit: uptimeLimit, + } + + ctx := context.Background() + outages, err := client.Uptime.ListOutages(ctx, uptimeProjectID, uptimeSiteID, options) + if err != nil { + return fmt.Errorf("failed to list outages: %w", err) + } + + switch uptimeOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(outages, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "DOWN AT\tUP AT\tSTATUS\tREASON") + for _, outage := range outages { + upAt := "Still down" + if outage.UpAt != nil { + upAt = outage.UpAt.Format("2006-01-02 15:04") + } + + reason := outage.Reason + if len(reason) > 40 { + reason = reason[:37] + "..." + } + + _, _ = fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", + outage.DownAt.Format("2006-01-02 15:04"), + upAt, + outage.Status, + reason) + } + _ = w.Flush() + } + + return nil + }, +} + +// uptimeChecksCmd represents the uptime checks command +var uptimeChecksCmd = &cobra.Command{ + Use: "checks", + Short: "List uptime checks for a site", + Long: `List individual uptime checks performed for a specific site.`, + RunE: func(_ *cobra.Command, _ []string) error { + if uptimeProjectID == 0 { + return fmt.Errorf("project ID is required. Set it using --project-id flag") + } + if uptimeSiteID == "" { + return fmt.Errorf("site ID is required. Set it using --site-id flag") + } + + authToken := viper.GetString("auth_token") + if authToken == "" { + return fmt.Errorf( + "auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable", + ) + } + + endpoint := convertEndpointForDataAPI(viper.GetString("endpoint")) + + client := hbapi.NewClient(). + WithBaseURL(endpoint). + WithAuthToken(authToken) + + options := hbapi.UptimeCheckListOptions{ + CreatedAfter: uptimeCreatedAfter, + CreatedBefore: uptimeCreatedBefore, + Limit: uptimeLimit, + } + + ctx := context.Background() + checks, err := client.Uptime.ListUptimeChecks(ctx, uptimeProjectID, uptimeSiteID, options) + if err != nil { + return fmt.Errorf("failed to list uptime checks: %w", err) + } + + switch uptimeOutputFormat { + case "json": + jsonBytes, err := json.MarshalIndent(checks, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + default: + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintln(w, "CREATED\tLOCATION\tUP\tDURATION") + for _, check := range checks { + up := "No" + if check.Up { + up = "Yes" + } + + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%dms\n", + check.CreatedAt.Format("2006-01-02 15:04:05"), + check.Location, + up, + check.Duration) + } + _ = w.Flush() + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(uptimeCmd) + + // Add subcommands + uptimeCmd.AddCommand(uptimeSitesCmd) + uptimeCmd.AddCommand(uptimeOutagesCmd) + uptimeCmd.AddCommand(uptimeChecksCmd) + + // Sites subcommands + uptimeSitesCmd.AddCommand(uptimeSitesListCmd) + uptimeSitesCmd.AddCommand(uptimeSitesGetCmd) + uptimeSitesCmd.AddCommand(uptimeSitesCreateCmd) + uptimeSitesCmd.AddCommand(uptimeSitesUpdateCmd) + uptimeSitesCmd.AddCommand(uptimeSitesDeleteCmd) + + // Common flags + uptimeCmd.PersistentFlags().IntVar(&uptimeProjectID, "project-id", 0, "Project ID") + + // Flags for sites list + uptimeSitesListCmd.Flags(). + StringVarP(&uptimeOutputFormat, "output", "o", "table", "Output format (table or json)") + + // Flags for sites get + uptimeSitesGetCmd.Flags().StringVar(&uptimeSiteID, "site-id", "", "Site ID") + uptimeSitesGetCmd.Flags(). + StringVarP(&uptimeOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = uptimeSitesGetCmd.MarkFlagRequired("site-id") + + // Flags for sites create + uptimeSitesCreateCmd.Flags(). + StringVar(&uptimeCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + uptimeSitesCreateCmd.Flags(). + StringVarP(&uptimeOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = uptimeSitesCreateCmd.MarkFlagRequired("cli-input-json") + + // Flags for sites update + uptimeSitesUpdateCmd.Flags().StringVar(&uptimeSiteID, "site-id", "", "Site ID") + uptimeSitesUpdateCmd.Flags(). + StringVar(&uptimeCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") + uptimeSitesUpdateCmd.Flags(). + StringVarP(&uptimeOutputFormat, "output", "o", "text", "Output format (text or json)") + _ = uptimeSitesUpdateCmd.MarkFlagRequired("site-id") + _ = uptimeSitesUpdateCmd.MarkFlagRequired("cli-input-json") + + // Flags for sites delete + uptimeSitesDeleteCmd.Flags().StringVar(&uptimeSiteID, "site-id", "", "Site ID") + _ = uptimeSitesDeleteCmd.MarkFlagRequired("site-id") + + // Flags for outages + uptimeOutagesCmd.Flags().StringVar(&uptimeSiteID, "site-id", "", "Site ID") + uptimeOutagesCmd.Flags(). + Int64Var(&uptimeCreatedAfter, "created-after", 0, "Filter by creation time (Unix timestamp)") + uptimeOutagesCmd.Flags(). + Int64Var(&uptimeCreatedBefore, "created-before", 0, "Filter by creation time (Unix timestamp)") + uptimeOutagesCmd.Flags(). + IntVar(&uptimeLimit, "limit", 25, "Maximum number of outages to return (max 25)") + uptimeOutagesCmd.Flags(). + StringVarP(&uptimeOutputFormat, "output", "o", "table", "Output format (table or json)") + _ = uptimeOutagesCmd.MarkFlagRequired("site-id") + + // Flags for checks + uptimeChecksCmd.Flags().StringVar(&uptimeSiteID, "site-id", "", "Site ID") + uptimeChecksCmd.Flags(). + Int64Var(&uptimeCreatedAfter, "created-after", 0, "Filter by creation time (Unix timestamp)") + uptimeChecksCmd.Flags(). + Int64Var(&uptimeCreatedBefore, "created-before", 0, "Filter by creation time (Unix timestamp)") + uptimeChecksCmd.Flags(). + IntVar(&uptimeLimit, "limit", 25, "Maximum number of checks to return (max 25)") + uptimeChecksCmd.Flags(). + StringVarP(&uptimeOutputFormat, "output", "o", "table", "Output format (table or json)") + _ = uptimeChecksCmd.MarkFlagRequired("site-id") +} From 246b9c2862e81073856ae0ff4f02df9855f1f97c Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 6 Jan 2026 14:33:14 -0800 Subject: [PATCH 02/17] Group the help output by API being accessed (and auth required) --- cmd/accounts.go | 7 ++++--- cmd/agent.go | 5 +++-- cmd/checkins.go | 7 ++++--- cmd/comments.go | 7 ++++--- cmd/deploy.go | 5 +++-- cmd/deployments.go | 7 ++++--- cmd/environments.go | 7 ++++--- cmd/faults.go | 7 ++++--- cmd/insights.go | 7 ++++--- cmd/projects.go | 7 ++++--- cmd/root.go | 27 +++++++++++++++++++++++++-- cmd/statuspages.go | 7 ++++--- cmd/teams.go | 7 ++++--- cmd/uptime.go | 7 ++++--- 14 files changed, 75 insertions(+), 39 deletions(-) diff --git a/cmd/accounts.go b/cmd/accounts.go index d2edc7a..8425771 100644 --- a/cmd/accounts.go +++ b/cmd/accounts.go @@ -23,9 +23,10 @@ var ( // accountsCmd represents the accounts command var accountsCmd = &cobra.Command{ - Use: "accounts", - Short: "Manage Honeybadger accounts", - Long: `View and manage your Honeybadger accounts, users, and invitations.`, + Use: "accounts", + Short: "Manage Honeybadger accounts", + GroupID: GroupDataAPI, + Long: `View and manage your Honeybadger accounts, users, and invitations.`, } // accountsListCmd represents the accounts list command diff --git a/cmd/agent.go b/cmd/agent.go index f9abb85..be4bb89 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -59,8 +59,9 @@ type diskPayload struct { // agentCmd represents the agent command var agentCmd = &cobra.Command{ - Use: "agent", - Short: "Start a metrics reporting agent", + Use: "agent", + Short: "Start a metrics reporting agent", + GroupID: GroupReportingAPI, Long: `Start a persistent process that periodically reports host metrics to Honeybadger's Insights API. This command collects and reports system metrics such as CPU usage, memory usage, disk usage, and load averages. Metrics are aggregated and reported at a configurable interval (default: 60 seconds).`, diff --git a/cmd/checkins.go b/cmd/checkins.go index 27cc990..5c2d4cd 100644 --- a/cmd/checkins.go +++ b/cmd/checkins.go @@ -21,9 +21,10 @@ var ( // checkinsCmd represents the checkins command var checkinsCmd = &cobra.Command{ - Use: "checkins", - Short: "Manage Honeybadger check-ins", - Long: `View and manage check-ins (cron job monitoring) for your Honeybadger projects.`, + Use: "checkins", + Short: "Manage Honeybadger check-ins", + GroupID: GroupDataAPI, + Long: `View and manage check-ins (cron job monitoring) for your Honeybadger projects.`, } // checkinsListCmd represents the checkins list command diff --git a/cmd/comments.go b/cmd/comments.go index a336ee2..09bd54a 100644 --- a/cmd/comments.go +++ b/cmd/comments.go @@ -22,9 +22,10 @@ var ( // commentsCmd represents the comments command var commentsCmd = &cobra.Command{ - Use: "comments", - Short: "Manage fault comments", - Long: `View and manage comments on faults in your Honeybadger projects.`, + Use: "comments", + Short: "Manage fault comments", + GroupID: GroupDataAPI, + Long: `View and manage comments on faults in your Honeybadger projects.`, } // commentsListCmd represents the comments list command diff --git a/cmd/deploy.go b/cmd/deploy.go index ab92a74..01e39a7 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -31,8 +31,9 @@ type deployPayload struct { // deployCmd represents the deploy command var deployCmd = &cobra.Command{ - Use: "deploy", - Short: "Report a deployment to Honeybadger", + Use: "deploy", + Short: "Report a deployment to Honeybadger", + GroupID: GroupReportingAPI, Long: `Report a deployment to Honeybadger's Reporting API. This command sends deployment information including environment, repository, revision, and the local username of the person deploying.`, diff --git a/cmd/deployments.go b/cmd/deployments.go index 6c8c9a2..ef16cb4 100644 --- a/cmd/deployments.go +++ b/cmd/deployments.go @@ -25,9 +25,10 @@ var ( // deploymentsCmd represents the deployments command var deploymentsCmd = &cobra.Command{ - Use: "deployments", - Short: "View and manage deployments", - Long: `View and manage deployment records for your Honeybadger projects.`, + Use: "deployments", + Short: "View and manage deployments", + GroupID: GroupDataAPI, + Long: `View and manage deployment records for your Honeybadger projects.`, } // deploymentsListCmd represents the deployments list command diff --git a/cmd/environments.go b/cmd/environments.go index fcf2ab9..381ce6e 100644 --- a/cmd/environments.go +++ b/cmd/environments.go @@ -21,9 +21,10 @@ var ( // environmentsCmd represents the environments command var environmentsCmd = &cobra.Command{ - Use: "environments", - Short: "Manage project environments", - Long: `View and manage environments for your Honeybadger projects.`, + Use: "environments", + Short: "Manage project environments", + GroupID: GroupDataAPI, + Long: `View and manage environments for your Honeybadger projects.`, } // environmentsListCmd represents the environments list command diff --git a/cmd/faults.go b/cmd/faults.go index 60319aa..5d4cbf5 100644 --- a/cmd/faults.go +++ b/cmd/faults.go @@ -24,9 +24,10 @@ var ( // faultsCmd represents the faults command var faultsCmd = &cobra.Command{ - Use: "faults", - Short: "Manage Honeybadger faults", - Long: `View and manage faults (errors) in your Honeybadger projects.`, + Use: "faults", + Short: "Manage Honeybadger faults", + GroupID: GroupDataAPI, + Long: `View and manage faults (errors) in your Honeybadger projects.`, } // faultsListCmd represents the faults list command diff --git a/cmd/insights.go b/cmd/insights.go index 1e40a35..c9ed602 100644 --- a/cmd/insights.go +++ b/cmd/insights.go @@ -23,9 +23,10 @@ var ( // insightsCmd represents the insights command var insightsCmd = &cobra.Command{ - Use: "insights", - Short: "Query Honeybadger Insights data", - Long: `Execute BadgerQL queries against your Honeybadger Insights data.`, + Use: "insights", + Short: "Query Honeybadger Insights data", + GroupID: GroupDataAPI, + Long: `Execute BadgerQL queries against your Honeybadger Insights data.`, } // insightsQueryCmd represents the insights query command diff --git a/cmd/projects.go b/cmd/projects.go index 4b3a604..7df4275 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -28,9 +28,10 @@ var ( // projectsCmd represents the projects command var projectsCmd = &cobra.Command{ - Use: "projects", - Short: "Manage Honeybadger projects", - Long: `View and manage your Honeybadger projects.`, + Use: "projects", + Short: "Manage Honeybadger projects", + GroupID: GroupDataAPI, + Long: `View and manage your Honeybadger projects.`, } // projectsListCmd represents the projects list command diff --git a/cmd/root.go b/cmd/root.go index 59a042e..98edfcc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,12 +24,25 @@ var ( Date string ) +// Command group IDs +const ( + GroupReportingAPI = "reporting" + GroupDataAPI = "data" +) + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "hb", Short: "Honeybadger CLI tool", - Long: `A command line interface for interacting with Honeybadger's Reporting API. -This tool allows you to manage deployments and other reporting features.`, + Long: `A command line interface for interacting with Honeybadger. + +This tool provides access to two APIs: + + Reporting API - For sending data to Honeybadger (deployments, metrics) + Authenticate with --api-key or HONEYBADGER_API_KEY + + Data API - For reading and managing your Honeybadger data + Authenticate with --auth-token or HONEYBADGER_AUTH_TOKEN`, } // Execute adds all child commands to the root command and sets flags appropriately. @@ -40,6 +53,16 @@ func Execute() error { func init() { cobra.OnInitialize(initConfig) + // Add command groups + rootCmd.AddGroup(&cobra.Group{ + ID: GroupReportingAPI, + Title: "Reporting API Commands (use --api-key):", + }) + rootCmd.AddGroup(&cobra.Group{ + ID: GroupDataAPI, + Title: "Data API Commands (use --auth-token):", + }) + rootCmd.PersistentFlags(). StringVar(&cfgFile, "config", "", "config file (default is config/honeybadger.yml)") rootCmd.PersistentFlags(). diff --git a/cmd/statuspages.go b/cmd/statuspages.go index 0027f09..bde6076 100644 --- a/cmd/statuspages.go +++ b/cmd/statuspages.go @@ -21,9 +21,10 @@ var ( // statuspagesCmd represents the statuspages command var statuspagesCmd = &cobra.Command{ - Use: "statuspages", - Short: "Manage status pages", - Long: `View and manage status pages for your Honeybadger accounts.`, + Use: "statuspages", + Short: "Manage status pages", + GroupID: GroupDataAPI, + Long: `View and manage status pages for your Honeybadger accounts.`, } // statuspagesListCmd represents the statuspages list command diff --git a/cmd/teams.go b/cmd/teams.go index 7115bd4..6c74692 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -25,9 +25,10 @@ var ( // teamsCmd represents the teams command var teamsCmd = &cobra.Command{ - Use: "teams", - Short: "Manage Honeybadger teams", - Long: `View and manage teams, team members, and team invitations.`, + Use: "teams", + Short: "Manage Honeybadger teams", + GroupID: GroupDataAPI, + Long: `View and manage teams, team members, and team invitations.`, } // teamsListCmd represents the teams list command diff --git a/cmd/uptime.go b/cmd/uptime.go index 118b6ac..240a368 100644 --- a/cmd/uptime.go +++ b/cmd/uptime.go @@ -24,9 +24,10 @@ var ( // uptimeCmd represents the uptime command var uptimeCmd = &cobra.Command{ - Use: "uptime", - Short: "Manage uptime monitoring", - Long: `View and manage uptime monitoring sites, outages, and checks for your Honeybadger projects.`, + Use: "uptime", + Short: "Manage uptime monitoring", + GroupID: GroupDataAPI, + Long: `View and manage uptime monitoring sites, outages, and checks for your Honeybadger projects.`, } // uptimeSitesCmd represents the uptime sites parent command From 9f0e8ceedd2874cba1646ed6369e22928ee84d90 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 6 Jan 2026 14:56:08 -0800 Subject: [PATCH 03/17] Add tests --- cmd/accounts.go | 32 ++-- cmd/accounts_test.go | 383 +++++++++++++++++++++++++++++++++++++++++++ cmd/checkins_test.go | 212 ++++++++++++++++++++++++ cmd/dataapi_test.go | 323 ++++++++++++++++++++++++++++++++++++ cmd/teams_test.go | 363 ++++++++++++++++++++++++++++++++++++++++ cmd/uptime_test.go | 214 ++++++++++++++++++++++++ 6 files changed, 1511 insertions(+), 16 deletions(-) create mode 100644 cmd/accounts_test.go create mode 100644 cmd/checkins_test.go create mode 100644 cmd/dataapi_test.go create mode 100644 cmd/teams_test.go create mode 100644 cmd/uptime_test.go diff --git a/cmd/accounts.go b/cmd/accounts.go index 8425771..7f45654 100644 --- a/cmd/accounts.go +++ b/cmd/accounts.go @@ -14,7 +14,7 @@ import ( var ( accountsOutputFormat string - accountID int + accountID string accountUserID int accountUserRole string accountInvitationID int @@ -65,7 +65,7 @@ var accountsListCmd = &cobra.Command{ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) _, _ = fmt.Fprintln(w, "ID\tNAME\tEMAIL") for _, account := range accounts { - _, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", account.ID, account.Name, account.Email) @@ -83,7 +83,7 @@ var accountsGetCmd = &cobra.Command{ Short: "Get an account by ID", Long: `Get detailed information about a specific account including quota and API stats.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --id flag") } @@ -115,7 +115,7 @@ var accountsGetCmd = &cobra.Command{ fmt.Println(string(jsonBytes)) default: fmt.Printf("Account Details:\n") - fmt.Printf(" ID: %d\n", account.ID) + fmt.Printf(" ID: %s\n", account.ID) fmt.Printf(" Name: %s\n", account.Name) fmt.Printf(" Email: %s\n", account.Email) if account.Active != nil { @@ -139,7 +139,7 @@ var accountsUsersListCmd = &cobra.Command{ Short: "List users for an account", Long: `List all users associated with an account.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } @@ -192,7 +192,7 @@ var accountsUsersGetCmd = &cobra.Command{ Short: "Get a user by ID", Long: `Get detailed information about a specific user in an account.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountUserID == 0 { @@ -243,7 +243,7 @@ var accountsUsersUpdateCmd = &cobra.Command{ Short: "Update a user's role", Long: `Update a user's role in an account. Valid roles: Member, Billing, Admin, Owner.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountUserID == 0 { @@ -296,7 +296,7 @@ var accountsUsersRemoveCmd = &cobra.Command{ Short: "Remove a user from an account", Long: `Remove a user from an account. This action cannot be undone.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountUserID == 0 { @@ -340,7 +340,7 @@ var accountsInvitationsListCmd = &cobra.Command{ Short: "List invitations for an account", Long: `List all pending invitations for an account.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } @@ -398,7 +398,7 @@ var accountsInvitationsGetCmd = &cobra.Command{ Short: "Get an invitation by ID", Long: `Get detailed information about a specific invitation.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountInvitationID == 0 { @@ -467,7 +467,7 @@ Example JSON payload: } }`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountCLIInputJSON == "" { @@ -540,7 +540,7 @@ Example JSON payload: } }`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountInvitationID == 0 { @@ -610,7 +610,7 @@ var accountsInvitationsDeleteCmd = &cobra.Command{ Short: "Delete an invitation", Long: `Delete a pending invitation. This action cannot be undone.`, RunE: func(_ *cobra.Command, _ []string) error { - if accountID == 0 { + if accountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if accountInvitationID == 0 { @@ -675,13 +675,13 @@ func init() { StringVarP(&accountsOutputFormat, "output", "o", "table", "Output format (table or json)") // Flags for get command - accountsGetCmd.Flags().IntVar(&accountID, "id", 0, "Account ID") + accountsGetCmd.Flags().StringVar(&accountID, "id", "", "Account ID") accountsGetCmd.Flags(). StringVarP(&accountsOutputFormat, "output", "o", "text", "Output format (text or json)") _ = accountsGetCmd.MarkFlagRequired("id") // Common account ID flag for users subcommands - accountsUsersCmd.PersistentFlags().IntVar(&accountID, "account-id", 0, "Account ID") + accountsUsersCmd.PersistentFlags().StringVar(&accountID, "account-id", "", "Account ID") // Flags for users list accountsUsersListCmd.Flags(). @@ -707,7 +707,7 @@ func init() { _ = accountsUsersRemoveCmd.MarkFlagRequired("user-id") // Common account ID flag for invitations subcommands - accountsInvitationsCmd.PersistentFlags().IntVar(&accountID, "account-id", 0, "Account ID") + accountsInvitationsCmd.PersistentFlags().StringVar(&accountID, "account-id", "", "Account ID") // Flags for invitations list accountsInvitationsListCmd.Flags(). diff --git a/cmd/accounts_test.go b/cmd/accounts_test.go new file mode 100644 index 0000000..dd956fd --- /dev/null +++ b/cmd/accounts_test.go @@ -0,0 +1,383 @@ +package cmd + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestAccountsListCommand(t *testing.T) { + tests := []struct { + name string + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `{ + "results": [ + {"id": "abc123", "email": "test@example.com", "name": "Test Account"} + ] + }`, + expectedError: false, + }, + { + name: "missing auth token", + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + { + name: "unauthorized", + authToken: "invalid-token", + serverStatus: http.StatusUnauthorized, + serverBody: `{"error": "Unauthorized"}`, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test server only if we expect to make requests + var serverURL string + if tt.authToken != "" { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/v2/accounts", r.URL.Path) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" // Won't be called + } + + // Reset viper and set config + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + // Reset command state + accountsOutputFormat = "table" + + // Execute command + err := accountsListCmd.RunE(accountsListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAccountsGetCommand(t *testing.T) { + tests := []struct { + name string + accountIDValue string + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful get", + accountIDValue: "abc123", + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `{ + "id": "abc123", + "email": "test@example.com", + "name": "Test Account" + }`, + expectedError: false, + }, + { + name: "missing account ID", + accountIDValue: "", + authToken: "test-token", + expectedError: true, + errorContains: "account ID is required", + }, + { + name: "missing auth token", + accountIDValue: "abc123", + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + { + name: "not found", + accountIDValue: "notfound", + authToken: "test-token", + serverStatus: http.StatusNotFound, + serverBody: `{"error": "Not found"}`, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.accountIDValue != "" { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Contains(t, r.URL.Path, "/v2/accounts/") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + // Set global state that the command reads + accountID = tt.accountIDValue + accountsOutputFormat = "text" + + err := accountsGetCmd.RunE(accountsGetCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAccountsOutputFormat(t *testing.T) { + mockResponse := `{ + "results": [ + {"id": "abc123", "email": "test@example.com", "name": "Test Account"} + ] + }` + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(mockResponse)) + }), + ) + defer server.Close() + + tests := []struct { + name string + format string + }{ + {name: "table format", format: "table"}, + {name: "json format", format: "json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + viper.Set("endpoint", server.URL) + viper.Set("auth_token", "test-token") + + accountsOutputFormat = tt.format + + err := accountsListCmd.RunE(accountsListCmd, []string{}) + assert.NoError(t, err) + }) + } +} + +func TestAccountsUsersListCommand(t *testing.T) { + tests := []struct { + name string + accountIDValue string + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list users", + accountIDValue: "abc123", + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[ + {"id": 1, "name": "User 1", "email": "user1@example.com", "role": "Owner"} + ]`, + expectedError: false, + }, + { + name: "missing account ID", + accountIDValue: "", + authToken: "test-token", + expectedError: true, + errorContains: "account ID is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.accountIDValue != "" { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + accountID = tt.accountIDValue + accountsOutputFormat = "table" + + err := accountsUsersListCmd.RunE(accountsUsersListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAccountsInvitationsCreateCommand(t *testing.T) { + tests := []struct { + name string + accountIDValue string + authToken string + cliInputJSON string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful create", + accountIDValue: "abc123", + authToken: "test-token", + cliInputJSON: `{"invitation": {"email": "new@example.com", "role": "Member"}}`, + serverStatus: http.StatusCreated, + serverBody: `{ + "id": 1, + "email": "new@example.com", + "role": "Member", + "token": "invite-token", + "created_at": "2024-01-01T00:00:00Z" + }`, + expectedError: false, + }, + { + name: "missing account ID", + accountIDValue: "", + authToken: "test-token", + cliInputJSON: `{"invitation": {"email": "new@example.com"}}`, + expectedError: true, + errorContains: "account ID is required", + }, + { + name: "missing JSON payload", + accountIDValue: "abc123", + authToken: "test-token", + cliInputJSON: "", + expectedError: true, + errorContains: "JSON payload is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.accountIDValue != "" && tt.cliInputJSON != "" { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + + // Verify JSON payload + var payload map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&payload) + assert.NoError(t, err) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + accountID = tt.accountIDValue + accountCLIInputJSON = tt.cliInputJSON + accountsOutputFormat = "text" + + err := accountsInvitationsCreateCmd.RunE(accountsInvitationsCreateCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/checkins_test.go b/cmd/checkins_test.go new file mode 100644 index 0000000..7559b6f --- /dev/null +++ b/cmd/checkins_test.go @@ -0,0 +1,212 @@ +package cmd + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestCheckinsListCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + projectIDValue: 123, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[ + {"id": 1, "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", "report_period": "1 day"} + ]`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing auth token", + projectIDValue: 123, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + checkinsProjectID = tt.projectIDValue + checkinsOutputFormat = "table" + + err := checkinsListCmd.RunE(checkinsListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCheckinsGetCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + checkinIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful get", + projectIDValue: 123, + checkinIDValue: 1, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `{ + "id": 1, + "name": "Daily Backup", + "slug": "daily-backup", + "schedule_type": "simple", + "report_period": "1 day" + }`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + checkinIDValue: 1, + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing checkin ID", + projectIDValue: 123, + checkinIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "check-in ID is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 && tt.checkinIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + checkinsProjectID = tt.projectIDValue + checkinID = tt.checkinIDValue + checkinsOutputFormat = "text" + + err := checkinsGetCmd.RunE(checkinsGetCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCheckinsOutputFormat(t *testing.T) { + mockResponse := `[ + {"id": 1, "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", "report_period": "1 day"} + ]` + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(mockResponse)) + }), + ) + defer server.Close() + + tests := []struct { + name string + format string + }{ + {name: "table format", format: "table"}, + {name: "json format", format: "json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + viper.Set("endpoint", server.URL) + viper.Set("auth_token", "test-token") + + checkinsProjectID = 123 + checkinsOutputFormat = tt.format + + err := checkinsListCmd.RunE(checkinsListCmd, []string{}) + assert.NoError(t, err) + }) + } +} diff --git a/cmd/dataapi_test.go b/cmd/dataapi_test.go new file mode 100644 index 0000000..a29adb2 --- /dev/null +++ b/cmd/dataapi_test.go @@ -0,0 +1,323 @@ +package cmd + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +// TestCommentsListCommand tests the comments list command validation +func TestCommentsListCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + faultIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + projectIDValue: 123, + faultIDValue: 456, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[]`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + faultIDValue: 456, + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing fault ID", + projectIDValue: 123, + faultIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "fault ID is required", + }, + { + name: "missing auth token", + projectIDValue: 123, + faultIDValue: 456, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 && tt.faultIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + commentsProjectID = tt.projectIDValue + commentsFaultID = tt.faultIDValue + commentsOutputFormat = "table" + + err := commentsListCmd.RunE(commentsListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +// TestDeploymentsListCommand tests the deployments list command validation +func TestDeploymentsListCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + projectIDValue: 123, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[]`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing auth token", + projectIDValue: 123, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + deploymentsProjectID = tt.projectIDValue + deploymentsOutputFormat = "table" + + err := deploymentsListCmd.RunE(deploymentsListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +// TestEnvironmentsListCommand tests the environments list command validation +func TestEnvironmentsListCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + projectIDValue: 123, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[]`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing auth token", + projectIDValue: 123, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + environmentsProjectID = tt.projectIDValue + environmentsOutputFormat = "table" + + err := environmentsListCmd.RunE(environmentsListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +// TestStatuspagesListCommand tests the statuspages list command validation +func TestStatuspagesListCommand(t *testing.T) { + tests := []struct { + name string + accountIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + accountIDValue: 123, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `{"results": []}`, + expectedError: false, + }, + { + name: "missing account ID", + accountIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "account ID is required", + }, + { + name: "missing auth token", + accountIDValue: 123, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.accountIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + statuspagesAccountID = tt.accountIDValue + statuspagesOutputFormat = "table" + + err := statuspagesListCmd.RunE(statuspagesListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/teams_test.go b/cmd/teams_test.go new file mode 100644 index 0000000..9923d01 --- /dev/null +++ b/cmd/teams_test.go @@ -0,0 +1,363 @@ +package cmd + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestTeamsListCommand(t *testing.T) { + tests := []struct { + name string + accountIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + accountIDValue: 123, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[ + {"id": 1, "name": "Team 1", "account_id": 123, "created_at": "2024-01-01T00:00:00Z"} + ]`, + expectedError: false, + }, + { + name: "missing account ID", + accountIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "account ID is required", + }, + { + name: "missing auth token", + accountIDValue: 123, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.accountIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + teamsAccountID = tt.accountIDValue + teamsOutputFormat = "table" + + err := teamsListCmd.RunE(teamsListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTeamsGetCommand(t *testing.T) { + tests := []struct { + name string + teamIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful get", + teamIDValue: 1, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `{ + "id": 1, + "name": "Team 1", + "account_id": 123, + "created_at": "2024-01-01T00:00:00Z" + }`, + expectedError: false, + }, + { + name: "missing team ID", + teamIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "team ID is required", + }, + { + name: "missing auth token", + teamIDValue: 1, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.teamIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + teamID = tt.teamIDValue + teamsOutputFormat = "text" + + err := teamsGetCmd.RunE(teamsGetCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTeamsCreateCommand(t *testing.T) { + tests := []struct { + name string + accountIDValue int + teamNameValue string + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful create", + accountIDValue: 123, + teamNameValue: "New Team", + authToken: "test-token", + serverStatus: http.StatusCreated, + serverBody: `{ + "id": 1, + "name": "New Team", + "account_id": 123, + "created_at": "2024-01-01T00:00:00Z" + }`, + expectedError: false, + }, + { + name: "missing account ID", + accountIDValue: 0, + teamNameValue: "New Team", + authToken: "test-token", + expectedError: true, + errorContains: "account ID is required", + }, + { + name: "missing team name", + accountIDValue: 123, + teamNameValue: "", + authToken: "test-token", + expectedError: true, + errorContains: "team name is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.accountIDValue != 0 && tt.teamNameValue != "" { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + teamsAccountID = tt.accountIDValue + teamName = tt.teamNameValue + teamsOutputFormat = "text" + + err := teamsCreateCmd.RunE(teamsCreateCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTeamsMembersListCommand(t *testing.T) { + tests := []struct { + name string + teamIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list members", + teamIDValue: 1, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[ + {"id": 1, "name": "Member 1", "email": "member1@example.com", "admin": true} + ]`, + expectedError: false, + }, + { + name: "missing team ID", + teamIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "team ID is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.teamIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + teamID = tt.teamIDValue + teamsOutputFormat = "table" + + err := teamsMembersListCmd.RunE(teamsMembersListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestTeamsOutputFormat(t *testing.T) { + mockResponse := `[ + {"id": 1, "name": "Team 1", "account_id": 123, "created_at": "2024-01-01T00:00:00Z"} + ]` + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(mockResponse)) + }), + ) + defer server.Close() + + tests := []struct { + name string + format string + }{ + {name: "table format", format: "table"}, + {name: "json format", format: "json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + viper.Set("endpoint", server.URL) + viper.Set("auth_token", "test-token") + + teamsAccountID = 123 + teamsOutputFormat = tt.format + + err := teamsListCmd.RunE(teamsListCmd, []string{}) + assert.NoError(t, err) + }) + } +} diff --git a/cmd/uptime_test.go b/cmd/uptime_test.go new file mode 100644 index 0000000..d72110f --- /dev/null +++ b/cmd/uptime_test.go @@ -0,0 +1,214 @@ +package cmd + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestUptimeSitesListCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful list", + projectIDValue: 123, + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `[ + {"id": "site1", "name": "Site 1", "url": "https://example.com", "state": "up", "active": true, "frequency": 5} + ]`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing auth token", + projectIDValue: 123, + authToken: "", + expectedError: true, + errorContains: "auth token is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + uptimeProjectID = tt.projectIDValue + uptimeOutputFormat = "table" + + err := uptimeSitesListCmd.RunE(uptimeSitesListCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUptimeSitesGetCommand(t *testing.T) { + tests := []struct { + name string + projectIDValue int + siteIDValue string + authToken string + serverStatus int + serverBody string + expectedError bool + errorContains string + }{ + { + name: "successful get", + projectIDValue: 123, + siteIDValue: "site1", + authToken: "test-token", + serverStatus: http.StatusOK, + serverBody: `{ + "id": "site1", + "name": "Site 1", + "url": "https://example.com", + "state": "up", + "active": true, + "frequency": 5, + "match_type": "contains" + }`, + expectedError: false, + }, + { + name: "missing project ID", + projectIDValue: 0, + siteIDValue: "site1", + authToken: "test-token", + expectedError: true, + errorContains: "project ID is required", + }, + { + name: "missing site ID", + projectIDValue: 123, + siteIDValue: "", + authToken: "test-token", + expectedError: true, + errorContains: "site ID is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var serverURL string + if tt.authToken != "" && tt.projectIDValue != 0 && tt.siteIDValue != "" { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.serverStatus) + _, _ = w.Write([]byte(tt.serverBody)) + }), + ) + defer server.Close() + serverURL = server.URL + } else { + serverURL = "http://localhost:9999" + } + + viper.Reset() + viper.Set("endpoint", serverURL) + if tt.authToken != "" { + viper.Set("auth_token", tt.authToken) + } + + uptimeProjectID = tt.projectIDValue + uptimeSiteID = tt.siteIDValue + uptimeOutputFormat = "text" + + err := uptimeSitesGetCmd.RunE(uptimeSitesGetCmd, []string{}) + + if tt.expectedError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUptimeOutputFormat(t *testing.T) { + mockResponse := `[ + {"id": "site1", "name": "Site 1", "url": "https://example.com", "state": "up", "active": true, "frequency": 5} + ]` + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(mockResponse)) + }), + ) + defer server.Close() + + tests := []struct { + name string + format string + }{ + {name: "table format", format: "table"}, + {name: "json format", format: "json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Reset() + viper.Set("endpoint", server.URL) + viper.Set("auth_token", "test-token") + + uptimeProjectID = 123 + uptimeOutputFormat = tt.format + + err := uptimeSitesListCmd.RunE(uptimeSitesListCmd, []string{}) + assert.NoError(t, err) + }) + } +} From 5a9fb7cf1de0161e162f30b8c9d7107298f6ebf6 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 6 Jan 2026 15:05:59 -0800 Subject: [PATCH 04/17] Upgrade github.com/shoenig/go-m1cpu --- go.mod | 2 +- go.sum | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 6a2eae9..ac839c8 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/shoenig/go-m1cpu v0.1.7 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect diff --git a/go.sum b/go.sum index ff51a87..606db9b 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/honeybadger-io/api-go v0.1.0 h1:KmzuDflewX85Elq5CbQvbsKvZursulaXXcinqvKQaRk= -github.com/honeybadger-io/api-go v0.1.0/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -41,10 +39,10 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0= +github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= +github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= +github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= From 9157cfaccd7d09c09cdcf92e92a1548f359c6b98 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Wed, 7 Jan 2026 13:07:07 -0800 Subject: [PATCH 05/17] Bump github.com/honeybadger-io/api-go --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ac839c8..e0f814d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/honeybadger-io/cli go 1.23 require ( - github.com/honeybadger-io/api-go v0.1.0 + github.com/honeybadger-io/api-go v0.1.1-0.20260107210008-3111ea9586ee github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 From e5cb45d5de9ece9f2f3dfe29d2ac450489902c8c Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Wed, 7 Jan 2026 13:09:06 -0800 Subject: [PATCH 06/17] go mod tidy --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 606db9b..f3eb437 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/honeybadger-io/api-go v0.1.1-0.20260107210008-3111ea9586ee h1:ktb4AGHDhrxKBqN2KqTdhT+xvgobQB93SoiHxkrbpyA= +github.com/honeybadger-io/api-go v0.1.1-0.20260107210008-3111ea9586ee/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= From fe786a14b51bed153a3ea391966996bc50ba3c8c Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Thu, 8 Jan 2026 10:59:15 -0800 Subject: [PATCH 07/17] Bump github.com/honeybadger-io/api-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e0f814d..fc4e2cc 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/honeybadger-io/cli go 1.23 require ( - github.com/honeybadger-io/api-go v0.1.1-0.20260107210008-3111ea9586ee + github.com/honeybadger-io/api-go v0.1.1-0.20260108174510-d7fcd5c14e0d github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 diff --git a/go.sum b/go.sum index f3eb437..4400886 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/honeybadger-io/api-go v0.1.1-0.20260107210008-3111ea9586ee h1:ktb4AGHDhrxKBqN2KqTdhT+xvgobQB93SoiHxkrbpyA= -github.com/honeybadger-io/api-go v0.1.1-0.20260107210008-3111ea9586ee/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= +github.com/honeybadger-io/api-go v0.1.1-0.20260108174510-d7fcd5c14e0d h1:PeVc8jkuNQgLHlpttUXZKrYoe7iNKVV0hhSy+sjSiWo= +github.com/honeybadger-io/api-go v0.1.1-0.20260108174510-d7fcd5c14e0d/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= From ac3dd4db2beecd311db57ded5f6b273429d90556 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 13:22:04 -0800 Subject: [PATCH 08/17] Update CLI for api-go type changes and wrapped responses - Change teamsAccountID and checkinID from int to string - Update Comment.Author handling (now string instead of *User) - Update test mock data to use wrapped response format {results: [...]} Co-Authored-By: Claude Opus 4.5 --- cmd/accounts_test.go | 7 ++++--- cmd/checkins.go | 33 +++++++++++++++---------------- cmd/checkins_test.go | 26 +++++++++++++----------- cmd/comments.go | 9 ++++----- cmd/dataapi_test.go | 16 +++++++-------- cmd/statuspages.go | 47 +++++++++++++++++++++++++------------------- cmd/teams.go | 10 +++++----- cmd/teams_test.go | 43 +++++++++++++++++++++------------------- cmd/uptime_test.go | 14 +++++++------ 9 files changed, 109 insertions(+), 96 deletions(-) diff --git a/cmd/accounts_test.go b/cmd/accounts_test.go index dd956fd..a7d41dc 100644 --- a/cmd/accounts_test.go +++ b/cmd/accounts_test.go @@ -234,9 +234,10 @@ func TestAccountsUsersListCommand(t *testing.T) { accountIDValue: "abc123", authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[ - {"id": 1, "name": "User 1", "email": "user1@example.com", "role": "Owner"} - ]`, + serverBody: `{ + "results": [{"id": 1, "name": "User 1", "email": "user1@example.com", "role": "Owner"}], + "links": {"self": "/v2/accounts/abc123/account_users"} + }`, expectedError: false, }, { diff --git a/cmd/checkins.go b/cmd/checkins.go index 5c2d4cd..ccef352 100644 --- a/cmd/checkins.go +++ b/cmd/checkins.go @@ -14,7 +14,7 @@ import ( var ( checkinsProjectID int - checkinID int + checkinID string checkinsOutputFormat string checkinCLIInputJSON string ) @@ -75,11 +75,11 @@ var checkinsListCmd = &cobra.Command{ } lastCheckIn := "Never" - if ci.LastCheckInAt != nil { - lastCheckIn = ci.LastCheckInAt.Format("2006-01-02 15:04") + if ci.ReportedAt != nil { + lastCheckIn = ci.ReportedAt.Format("2006-01-02 15:04") } - _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n", + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", ci.ID, ci.Name, ci.Slug, @@ -103,7 +103,7 @@ var checkinsGetCmd = &cobra.Command{ if checkinsProjectID == 0 { return fmt.Errorf("project ID is required. Set it using --project-id flag") } - if checkinID == 0 { + if checkinID == "" { return fmt.Errorf("check-in ID is required. Set it using --id flag") } @@ -135,9 +135,10 @@ var checkinsGetCmd = &cobra.Command{ fmt.Println(string(jsonBytes)) default: fmt.Printf("Check-in Details:\n") - fmt.Printf(" ID: %d\n", checkIn.ID) + fmt.Printf(" ID: %s\n", checkIn.ID) fmt.Printf(" Name: %s\n", checkIn.Name) fmt.Printf(" Slug: %s\n", checkIn.Slug) + fmt.Printf(" State: %s\n", checkIn.State) fmt.Printf(" Schedule Type: %s\n", checkIn.ScheduleType) if checkIn.ReportPeriod != nil { fmt.Printf(" Report Period: %s\n", *checkIn.ReportPeriod) @@ -151,12 +152,10 @@ var checkinsGetCmd = &cobra.Command{ if checkIn.CronTimezone != nil { fmt.Printf(" Cron Timezone: %s\n", *checkIn.CronTimezone) } - fmt.Printf(" Project ID: %d\n", checkIn.ProjectID) - fmt.Printf(" Created: %s\n", checkIn.CreatedAt.Format("2006-01-02 15:04:05")) - if checkIn.LastCheckInAt != nil { + if checkIn.ReportedAt != nil { fmt.Printf( " Last Check-in: %s\n", - checkIn.LastCheckInAt.Format("2006-01-02 15:04:05"), + checkIn.ReportedAt.Format("2006-01-02 15:04:05"), ) } } @@ -242,7 +241,7 @@ Example JSON payload for cron schedule: fmt.Println(string(jsonBytes)) default: fmt.Printf("Check-in created successfully!\n") - fmt.Printf(" ID: %d\n", checkIn.ID) + fmt.Printf(" ID: %s\n", checkIn.ID) fmt.Printf(" Name: %s\n", checkIn.Name) fmt.Printf(" Slug: %s\n", checkIn.Slug) } @@ -270,7 +269,7 @@ Example JSON payload: if checkinsProjectID == 0 { return fmt.Errorf("project ID is required. Set it using --project-id flag") } - if checkinID == 0 { + if checkinID == "" { return fmt.Errorf("check-in ID is required. Set it using --id flag") } if checkinCLIInputJSON == "" { @@ -317,7 +316,7 @@ Example JSON payload: fmt.Println(string(jsonBytes)) default: fmt.Printf("Check-in updated successfully!\n") - fmt.Printf(" ID: %d\n", checkIn.ID) + fmt.Printf(" ID: %s\n", checkIn.ID) fmt.Printf(" Name: %s\n", checkIn.Name) fmt.Printf(" Slug: %s\n", checkIn.Slug) } @@ -335,7 +334,7 @@ var checkinsDeleteCmd = &cobra.Command{ if checkinsProjectID == 0 { return fmt.Errorf("project ID is required. Set it using --project-id flag") } - if checkinID == 0 { + if checkinID == "" { return fmt.Errorf("check-in ID is required. Set it using --id flag") } @@ -379,7 +378,7 @@ func init() { StringVarP(&checkinsOutputFormat, "output", "o", "table", "Output format (table or json)") // Flags for get command - checkinsGetCmd.Flags().IntVar(&checkinID, "id", 0, "Check-in ID") + checkinsGetCmd.Flags().StringVar(&checkinID, "id", "", "Check-in ID") checkinsGetCmd.Flags(). StringVarP(&checkinsOutputFormat, "output", "o", "text", "Output format (text or json)") _ = checkinsGetCmd.MarkFlagRequired("id") @@ -392,7 +391,7 @@ func init() { _ = checkinsCreateCmd.MarkFlagRequired("cli-input-json") // Flags for update command - checkinsUpdateCmd.Flags().IntVar(&checkinID, "id", 0, "Check-in ID") + checkinsUpdateCmd.Flags().StringVar(&checkinID, "id", "", "Check-in ID") checkinsUpdateCmd.Flags(). StringVar(&checkinCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") checkinsUpdateCmd.Flags(). @@ -401,6 +400,6 @@ func init() { _ = checkinsUpdateCmd.MarkFlagRequired("cli-input-json") // Flags for delete command - checkinsDeleteCmd.Flags().IntVar(&checkinID, "id", 0, "Check-in ID") + checkinsDeleteCmd.Flags().StringVar(&checkinID, "id", "", "Check-in ID") _ = checkinsDeleteCmd.MarkFlagRequired("id") } diff --git a/cmd/checkins_test.go b/cmd/checkins_test.go index 7559b6f..332da02 100644 --- a/cmd/checkins_test.go +++ b/cmd/checkins_test.go @@ -24,9 +24,10 @@ func TestCheckinsListCommand(t *testing.T) { projectIDValue: 123, authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[ - {"id": 1, "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", "report_period": "1 day"} - ]`, + serverBody: `{ + "results": [{"id": "abc123", "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", "report_period": "1 day"}], + "links": {"self": "/v2/projects/123/check_ins"} + }`, expectedError: false, }, { @@ -91,7 +92,7 @@ func TestCheckinsGetCommand(t *testing.T) { tests := []struct { name string projectIDValue int - checkinIDValue int + checkinIDValue string authToken string serverStatus int serverBody string @@ -101,11 +102,11 @@ func TestCheckinsGetCommand(t *testing.T) { { name: "successful get", projectIDValue: 123, - checkinIDValue: 1, + checkinIDValue: "abc123", authToken: "test-token", serverStatus: http.StatusOK, serverBody: `{ - "id": 1, + "id": "abc123", "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", @@ -116,7 +117,7 @@ func TestCheckinsGetCommand(t *testing.T) { { name: "missing project ID", projectIDValue: 0, - checkinIDValue: 1, + checkinIDValue: "abc123", authToken: "test-token", expectedError: true, errorContains: "project ID is required", @@ -124,7 +125,7 @@ func TestCheckinsGetCommand(t *testing.T) { { name: "missing checkin ID", projectIDValue: 123, - checkinIDValue: 0, + checkinIDValue: "", authToken: "test-token", expectedError: true, errorContains: "check-in ID is required", @@ -134,7 +135,7 @@ func TestCheckinsGetCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var serverURL string - if tt.authToken != "" && tt.projectIDValue != 0 && tt.checkinIDValue != 0 { + if tt.authToken != "" && tt.projectIDValue != 0 && tt.checkinIDValue != "" { server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) @@ -175,9 +176,10 @@ func TestCheckinsGetCommand(t *testing.T) { } func TestCheckinsOutputFormat(t *testing.T) { - mockResponse := `[ - {"id": 1, "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", "report_period": "1 day"} - ]` + mockResponse := `{ + "results": [{"id": "abc123", "name": "Daily Backup", "slug": "daily-backup", "schedule_type": "simple", "report_period": "1 day"}], + "links": {"self": "/v2/projects/123/check_ins"} + }` server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { diff --git a/cmd/comments.go b/cmd/comments.go index 09bd54a..fee9c7d 100644 --- a/cmd/comments.go +++ b/cmd/comments.go @@ -72,8 +72,8 @@ var commentsListCmd = &cobra.Command{ _, _ = fmt.Fprintln(w, "ID\tAUTHOR\tEVENT\tCREATED\tBODY") for _, c := range comments { author := "System" - if c.Author != nil { - author = c.Author.Name + if c.Author != "" { + author = c.Author } body := c.Body @@ -143,11 +143,10 @@ var commentsGetCmd = &cobra.Command{ fmt.Printf(" Fault ID: %d\n", comment.FaultID) fmt.Printf(" Event: %s\n", comment.Event) fmt.Printf(" Source: %s\n", comment.Source) - if comment.Author != nil { - fmt.Printf(" Author: %s <%s>\n", comment.Author.Name, comment.Author.Email) + if comment.Author != "" { + fmt.Printf(" Author: %s\n", comment.Author) } fmt.Printf(" Created: %s\n", comment.CreatedAt.Format("2006-01-02 15:04:05")) - fmt.Printf(" Notices Count: %d\n", comment.NoticesCount) fmt.Printf(" Body:\n %s\n", comment.Body) } diff --git a/cmd/dataapi_test.go b/cmd/dataapi_test.go index a29adb2..b2a18f7 100644 --- a/cmd/dataapi_test.go +++ b/cmd/dataapi_test.go @@ -27,7 +27,7 @@ func TestCommentsListCommand(t *testing.T) { faultIDValue: 456, authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[]`, + serverBody: `{"results": [], "links": {"self": "/v2/projects/123/faults/456/comments"}}`, expectedError: false, }, { @@ -113,7 +113,7 @@ func TestDeploymentsListCommand(t *testing.T) { projectIDValue: 123, authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[]`, + serverBody: `{"results": [], "links": {"self": "/v2/projects/123/deploys"}}`, expectedError: false, }, { @@ -188,7 +188,7 @@ func TestEnvironmentsListCommand(t *testing.T) { projectIDValue: 123, authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[]`, + serverBody: `{"results": [], "links": {"self": "/v2/projects/123/environments"}}`, expectedError: false, }, { @@ -251,7 +251,7 @@ func TestEnvironmentsListCommand(t *testing.T) { func TestStatuspagesListCommand(t *testing.T) { tests := []struct { name string - accountIDValue int + accountIDValue string authToken string serverStatus int serverBody string @@ -260,7 +260,7 @@ func TestStatuspagesListCommand(t *testing.T) { }{ { name: "successful list", - accountIDValue: 123, + accountIDValue: "123", authToken: "test-token", serverStatus: http.StatusOK, serverBody: `{"results": []}`, @@ -268,14 +268,14 @@ func TestStatuspagesListCommand(t *testing.T) { }, { name: "missing account ID", - accountIDValue: 0, + accountIDValue: "", authToken: "test-token", expectedError: true, errorContains: "account ID is required", }, { name: "missing auth token", - accountIDValue: 123, + accountIDValue: "123", authToken: "", expectedError: true, errorContains: "auth token is required", @@ -285,7 +285,7 @@ func TestStatuspagesListCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var serverURL string - if tt.authToken != "" && tt.accountIDValue != 0 { + if tt.authToken != "" && tt.accountIDValue != "" { server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/cmd/statuspages.go b/cmd/statuspages.go index bde6076..85b8f71 100644 --- a/cmd/statuspages.go +++ b/cmd/statuspages.go @@ -13,8 +13,8 @@ import ( ) var ( - statuspagesAccountID int - statuspageID int + statuspagesAccountID string + statuspageID string statuspagesOutputFormat string statuspageCLIInputJSON string ) @@ -33,7 +33,7 @@ var statuspagesListCmd = &cobra.Command{ Short: "List status pages for an account", Long: `List all status pages configured for a specific account.`, RunE: func(_ *cobra.Command, _ []string) error { - if statuspagesAccountID == 0 { + if statuspagesAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } @@ -67,7 +67,7 @@ var statuspagesListCmd = &cobra.Command{ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) _, _ = fmt.Fprintln(w, "ID\tNAME\tURL\tSITES\tCHECK-INS") for _, sp := range statusPages { - _, _ = fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\n", + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%d\n", sp.ID, sp.Name, sp.URL, @@ -87,10 +87,10 @@ var statuspagesGetCmd = &cobra.Command{ Short: "Get a status page by ID", Long: `Get detailed information about a specific status page.`, RunE: func(_ *cobra.Command, _ []string) error { - if statuspagesAccountID == 0 { + if statuspagesAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } - if statuspageID == 0 { + if statuspageID == "" { return fmt.Errorf("status page ID is required. Set it using --id flag") } @@ -122,10 +122,10 @@ var statuspagesGetCmd = &cobra.Command{ fmt.Println(string(jsonBytes)) default: fmt.Printf("Status Page Details:\n") - fmt.Printf(" ID: %d\n", statusPage.ID) + fmt.Printf(" ID: %s\n", statusPage.ID) fmt.Printf(" Name: %s\n", statusPage.Name) fmt.Printf(" URL: %s\n", statusPage.URL) - fmt.Printf(" Account ID: %d\n", statusPage.AccountID) + fmt.Printf(" Account ID: %s\n", statusPage.AccountID) if statusPage.Domain != nil { fmt.Printf(" Domain: %s\n", *statusPage.Domain) } @@ -137,10 +137,16 @@ var statuspagesGetCmd = &cobra.Command{ ) } if len(statusPage.Sites) > 0 { - fmt.Printf(" Sites: %v\n", statusPage.Sites) + fmt.Printf(" Sites:\n") + for _, site := range statusPage.Sites { + fmt.Printf(" - %s (%s)\n", site.DisplayName, site.State) + } } if len(statusPage.CheckIns) > 0 { - fmt.Printf(" Check-ins: %v\n", statusPage.CheckIns) + fmt.Printf(" Check-ins:\n") + for _, ci := range statusPage.CheckIns { + fmt.Printf(" - %s (%s)\n", ci.DisplayName, ci.State) + } } } @@ -167,7 +173,7 @@ Example JSON payload: } }`, RunE: func(_ *cobra.Command, _ []string) error { - if statuspagesAccountID == 0 { + if statuspagesAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if statuspageCLIInputJSON == "" { @@ -214,7 +220,7 @@ Example JSON payload: fmt.Println(string(jsonBytes)) default: fmt.Printf("Status page created successfully!\n") - fmt.Printf(" ID: %d\n", statusPage.ID) + fmt.Printf(" ID: %s\n", statusPage.ID) fmt.Printf(" Name: %s\n", statusPage.Name) fmt.Printf(" URL: %s\n", statusPage.URL) } @@ -239,10 +245,10 @@ Example JSON payload: } }`, RunE: func(_ *cobra.Command, _ []string) error { - if statuspagesAccountID == 0 { + if statuspagesAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } - if statuspageID == 0 { + if statuspageID == "" { return fmt.Errorf("status page ID is required. Set it using --id flag") } if statuspageCLIInputJSON == "" { @@ -291,10 +297,10 @@ var statuspagesDeleteCmd = &cobra.Command{ Short: "Delete a status page", Long: `Delete a status page by ID. This action cannot be undone.`, RunE: func(_ *cobra.Command, _ []string) error { - if statuspagesAccountID == 0 { + if statuspagesAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } - if statuspageID == 0 { + if statuspageID == "" { return fmt.Errorf("status page ID is required. Set it using --id flag") } @@ -331,14 +337,15 @@ func init() { statuspagesCmd.AddCommand(statuspagesDeleteCmd) // Common flags - statuspagesCmd.PersistentFlags().IntVar(&statuspagesAccountID, "account-id", 0, "Account ID") + statuspagesCmd.PersistentFlags(). + StringVar(&statuspagesAccountID, "account-id", "", "Account ID") // Flags for list command statuspagesListCmd.Flags(). StringVarP(&statuspagesOutputFormat, "output", "o", "table", "Output format (table or json)") // Flags for get command - statuspagesGetCmd.Flags().IntVar(&statuspageID, "id", 0, "Status page ID") + statuspagesGetCmd.Flags().StringVar(&statuspageID, "id", "", "Status page ID") statuspagesGetCmd.Flags(). StringVarP(&statuspagesOutputFormat, "output", "o", "text", "Output format (text or json)") _ = statuspagesGetCmd.MarkFlagRequired("id") @@ -351,13 +358,13 @@ func init() { _ = statuspagesCreateCmd.MarkFlagRequired("cli-input-json") // Flags for update command - statuspagesUpdateCmd.Flags().IntVar(&statuspageID, "id", 0, "Status page ID") + statuspagesUpdateCmd.Flags().StringVar(&statuspageID, "id", "", "Status page ID") statuspagesUpdateCmd.Flags(). StringVar(&statuspageCLIInputJSON, "cli-input-json", "", "JSON payload (string or file://path)") _ = statuspagesUpdateCmd.MarkFlagRequired("id") _ = statuspagesUpdateCmd.MarkFlagRequired("cli-input-json") // Flags for delete command - statuspagesDeleteCmd.Flags().IntVar(&statuspageID, "id", 0, "Status page ID") + statuspagesDeleteCmd.Flags().StringVar(&statuspageID, "id", "", "Status page ID") _ = statuspagesDeleteCmd.MarkFlagRequired("id") } diff --git a/cmd/teams.go b/cmd/teams.go index 6c74692..de9cde6 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -13,7 +13,7 @@ import ( ) var ( - teamsAccountID int + teamsAccountID string teamID int teamsOutputFormat string teamName string @@ -37,7 +37,7 @@ var teamsListCmd = &cobra.Command{ Short: "List teams for an account", Long: `List all teams for a specific account.`, RunE: func(_ *cobra.Command, _ []string) error { - if teamsAccountID == 0 { + if teamsAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } @@ -137,7 +137,7 @@ var teamsCreateCmd = &cobra.Command{ Short: "Create a new team", Long: `Create a new team for an account.`, RunE: func(_ *cobra.Command, _ []string) error { - if teamsAccountID == 0 { + if teamsAccountID == "" { return fmt.Errorf("account ID is required. Set it using --account-id flag") } if teamName == "" { @@ -755,7 +755,7 @@ func init() { teamsInvitationsCmd.AddCommand(teamsInvitationsDeleteCmd) // Flags for list command - teamsListCmd.Flags().IntVar(&teamsAccountID, "account-id", 0, "Account ID") + teamsListCmd.Flags().StringVar(&teamsAccountID, "account-id", "", "Account ID") teamsListCmd.Flags(). StringVarP(&teamsOutputFormat, "output", "o", "table", "Output format (table or json)") _ = teamsListCmd.MarkFlagRequired("account-id") @@ -767,7 +767,7 @@ func init() { _ = teamsGetCmd.MarkFlagRequired("id") // Flags for create command - teamsCreateCmd.Flags().IntVar(&teamsAccountID, "account-id", 0, "Account ID") + teamsCreateCmd.Flags().StringVar(&teamsAccountID, "account-id", "", "Account ID") teamsCreateCmd.Flags().StringVar(&teamName, "name", "", "Team name") teamsCreateCmd.Flags(). StringVarP(&teamsOutputFormat, "output", "o", "text", "Output format (text or json)") diff --git a/cmd/teams_test.go b/cmd/teams_test.go index 9923d01..7342886 100644 --- a/cmd/teams_test.go +++ b/cmd/teams_test.go @@ -12,7 +12,7 @@ import ( func TestTeamsListCommand(t *testing.T) { tests := []struct { name string - accountIDValue int + accountIDValue string authToken string serverStatus int serverBody string @@ -21,24 +21,25 @@ func TestTeamsListCommand(t *testing.T) { }{ { name: "successful list", - accountIDValue: 123, + accountIDValue: "123", authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[ - {"id": 1, "name": "Team 1", "account_id": 123, "created_at": "2024-01-01T00:00:00Z"} - ]`, + serverBody: `{ + "results": [{"id": 1, "name": "Team 1", "account_id": 123, "created_at": "2024-01-01T00:00:00Z"}], + "links": {"self": "/v2/accounts/123/teams"} + }`, expectedError: false, }, { name: "missing account ID", - accountIDValue: 0, + accountIDValue: "", authToken: "test-token", expectedError: true, errorContains: "account ID is required", }, { name: "missing auth token", - accountIDValue: 123, + accountIDValue: "123", authToken: "", expectedError: true, errorContains: "auth token is required", @@ -48,7 +49,7 @@ func TestTeamsListCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var serverURL string - if tt.authToken != "" && tt.accountIDValue != 0 { + if tt.authToken != "" && tt.accountIDValue != "" { server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) @@ -171,7 +172,7 @@ func TestTeamsGetCommand(t *testing.T) { func TestTeamsCreateCommand(t *testing.T) { tests := []struct { name string - accountIDValue int + accountIDValue string teamNameValue string authToken string serverStatus int @@ -181,7 +182,7 @@ func TestTeamsCreateCommand(t *testing.T) { }{ { name: "successful create", - accountIDValue: 123, + accountIDValue: "123", teamNameValue: "New Team", authToken: "test-token", serverStatus: http.StatusCreated, @@ -195,7 +196,7 @@ func TestTeamsCreateCommand(t *testing.T) { }, { name: "missing account ID", - accountIDValue: 0, + accountIDValue: "", teamNameValue: "New Team", authToken: "test-token", expectedError: true, @@ -203,7 +204,7 @@ func TestTeamsCreateCommand(t *testing.T) { }, { name: "missing team name", - accountIDValue: 123, + accountIDValue: "123", teamNameValue: "", authToken: "test-token", expectedError: true, @@ -214,7 +215,7 @@ func TestTeamsCreateCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var serverURL string - if tt.authToken != "" && tt.accountIDValue != 0 && tt.teamNameValue != "" { + if tt.authToken != "" && tt.accountIDValue != "" && tt.teamNameValue != "" { server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) @@ -269,9 +270,10 @@ func TestTeamsMembersListCommand(t *testing.T) { teamIDValue: 1, authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[ - {"id": 1, "name": "Member 1", "email": "member1@example.com", "admin": true} - ]`, + serverBody: `{ + "results": [{"id": 1, "name": "Member 1", "email": "member1@example.com", "admin": true}], + "links": {"self": "/v2/teams/1/team_members"} + }`, expectedError: false, }, { @@ -326,9 +328,10 @@ func TestTeamsMembersListCommand(t *testing.T) { } func TestTeamsOutputFormat(t *testing.T) { - mockResponse := `[ - {"id": 1, "name": "Team 1", "account_id": 123, "created_at": "2024-01-01T00:00:00Z"} - ]` + mockResponse := `{ + "results": [{"id": 1, "name": "Team 1", "account_id": 123, "created_at": "2024-01-01T00:00:00Z"}], + "links": {"self": "/v2/accounts/123/teams"} + }` server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -353,7 +356,7 @@ func TestTeamsOutputFormat(t *testing.T) { viper.Set("endpoint", server.URL) viper.Set("auth_token", "test-token") - teamsAccountID = 123 + teamsAccountID = "123" teamsOutputFormat = tt.format err := teamsListCmd.RunE(teamsListCmd, []string{}) diff --git a/cmd/uptime_test.go b/cmd/uptime_test.go index d72110f..b324113 100644 --- a/cmd/uptime_test.go +++ b/cmd/uptime_test.go @@ -24,9 +24,10 @@ func TestUptimeSitesListCommand(t *testing.T) { projectIDValue: 123, authToken: "test-token", serverStatus: http.StatusOK, - serverBody: `[ - {"id": "site1", "name": "Site 1", "url": "https://example.com", "state": "up", "active": true, "frequency": 5} - ]`, + serverBody: `{ + "results": [{"id": "site1", "name": "Site 1", "url": "https://example.com", "state": "up", "active": true, "frequency": 5}], + "links": {"self": "/v2/projects/123/sites"} + }`, expectedError: false, }, { @@ -177,9 +178,10 @@ func TestUptimeSitesGetCommand(t *testing.T) { } func TestUptimeOutputFormat(t *testing.T) { - mockResponse := `[ - {"id": "site1", "name": "Site 1", "url": "https://example.com", "state": "up", "active": true, "frequency": 5} - ]` + mockResponse := `{ + "results": [{"id": "site1", "name": "Site 1", "url": "https://example.com", "state": "up", "active": true, "frequency": 5}], + "links": {"self": "/v2/projects/123/sites"} + }` server := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { From b74485c7df5d41e64573598da28645b727b3992a Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 13:29:04 -0800 Subject: [PATCH 09/17] Use "check-ins", not "checkins" for the command --- cmd/checkins.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/checkins.go b/cmd/checkins.go index ccef352..f23f02e 100644 --- a/cmd/checkins.go +++ b/cmd/checkins.go @@ -21,7 +21,7 @@ var ( // checkinsCmd represents the checkins command var checkinsCmd = &cobra.Command{ - Use: "checkins", + Use: "check-ins", Short: "Manage Honeybadger check-ins", GroupID: GroupDataAPI, Long: `View and manage check-ins (cron job monitoring) for your Honeybadger projects.`, From a2b6bde922a7f092bcc742c9bd4ec65d5336e304 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 13:30:45 -0800 Subject: [PATCH 10/17] Get the latest api-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fc4e2cc..80de6f0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/honeybadger-io/cli go 1.23 require ( - github.com/honeybadger-io/api-go v0.1.1-0.20260108174510-d7fcd5c14e0d + github.com/honeybadger-io/api-go v0.1.1-0.20260112212135-8d6771183f77 github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 diff --git a/go.sum b/go.sum index 4400886..0a859a0 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/honeybadger-io/api-go v0.1.1-0.20260108174510-d7fcd5c14e0d h1:PeVc8jkuNQgLHlpttUXZKrYoe7iNKVV0hhSy+sjSiWo= -github.com/honeybadger-io/api-go v0.1.1-0.20260108174510-d7fcd5c14e0d/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= +github.com/honeybadger-io/api-go v0.1.1-0.20260112212135-8d6771183f77 h1:+Fl2zKOZzWiMQ+Eh49nvI1Q5RSe8MklK0hdNYVtRuns= +github.com/honeybadger-io/api-go v0.1.1-0.20260112212135-8d6771183f77/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= From ffce8613890532989573bb416300a6146dd0af3c Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 13:43:47 -0800 Subject: [PATCH 11/17] Fix golines formatting in root.go Co-Authored-By: Claude Opus 4.5 --- cmd/root.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 98edfcc..fdd7ead 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -72,13 +72,16 @@ func init() { rootCmd.PersistentFlags(). StringVar(&endpoint, "endpoint", defaultEndpoint, "Honeybadger endpoint") - if err := viper.BindPFlag("api_key", rootCmd.PersistentFlags().Lookup("api-key")); err != nil { + err := viper.BindPFlag("api_key", rootCmd.PersistentFlags().Lookup("api-key")) + if err != nil { fmt.Printf("error binding api-key flag: %v\n", err) } - if err := viper.BindPFlag("auth_token", rootCmd.PersistentFlags().Lookup("auth-token")); err != nil { + err = viper.BindPFlag("auth_token", rootCmd.PersistentFlags().Lookup("auth-token")) + if err != nil { fmt.Printf("error binding auth-token flag: %v\n", err) } - if err := viper.BindPFlag("endpoint", rootCmd.PersistentFlags().Lookup("endpoint")); err != nil { + err = viper.BindPFlag("endpoint", rootCmd.PersistentFlags().Lookup("endpoint")) + if err != nil { fmt.Printf("error binding endpoint flag: %v\n", err) } } From 16f132aa7f29babfa284f39b469063234b79068d Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 15:18:08 -0800 Subject: [PATCH 12/17] Handle 204 responses on updates --- cmd/accounts.go | 18 +++++++++++++----- cmd/checkins.go | 8 ++++++-- cmd/comments.go | 10 +++++++--- cmd/teams.go | 37 ++++++++++++++++++++++++++++++------- go.mod | 2 +- go.sum | 4 ++-- 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/cmd/accounts.go b/cmd/accounts.go index 7f45654..53f3df9 100644 --- a/cmd/accounts.go +++ b/cmd/accounts.go @@ -267,11 +267,15 @@ var accountsUsersUpdateCmd = &cobra.Command{ WithAuthToken(authToken) ctx := context.Background() - user, err := client.Accounts.UpdateUser(ctx, accountID, accountUserID, accountUserRole) - if err != nil { + if err := client.Accounts.UpdateUser(ctx, accountID, accountUserID, accountUserRole); err != nil { return fmt.Errorf("failed to update user: %w", err) } + user, err := client.Accounts.GetUser(ctx, accountID, accountUserID) + if err != nil { + return fmt.Errorf("failed to fetch updated user: %w", err) + } + switch accountsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(user, "", " ") @@ -576,16 +580,20 @@ Example JSON payload: } ctx := context.Background() - invitation, err := client.Accounts.UpdateInvitation( + if err := client.Accounts.UpdateInvitation( ctx, accountID, accountInvitationID, payload.Invitation, - ) - if err != nil { + ); err != nil { return fmt.Errorf("failed to update invitation: %w", err) } + invitation, err := client.Accounts.GetInvitation(ctx, accountID, accountInvitationID) + if err != nil { + return fmt.Errorf("failed to fetch updated invitation: %w", err) + } + switch accountsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(invitation, "", " ") diff --git a/cmd/checkins.go b/cmd/checkins.go index f23f02e..84d6c82 100644 --- a/cmd/checkins.go +++ b/cmd/checkins.go @@ -302,11 +302,15 @@ Example JSON payload: } ctx := context.Background() - checkIn, err := client.CheckIns.Update(ctx, checkinsProjectID, checkinID, payload.CheckIn) - if err != nil { + if err := client.CheckIns.Update(ctx, checkinsProjectID, checkinID, payload.CheckIn); err != nil { return fmt.Errorf("failed to update check-in: %w", err) } + checkIn, err := client.CheckIns.Get(ctx, checkinsProjectID, checkinID) + if err != nil { + return fmt.Errorf("failed to fetch updated check-in: %w", err) + } + switch checkinsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(checkIn, "", " ") diff --git a/cmd/comments.go b/cmd/comments.go index fee9c7d..23f68fa 100644 --- a/cmd/comments.go +++ b/cmd/comments.go @@ -239,17 +239,21 @@ var commentsUpdateCmd = &cobra.Command{ WithAuthToken(authToken) ctx := context.Background() - comment, err := client.Comments.Update( + if err := client.Comments.Update( ctx, commentsProjectID, commentsFaultID, commentID, commentBody, - ) - if err != nil { + ); err != nil { return fmt.Errorf("failed to update comment: %w", err) } + comment, err := client.Comments.Get(ctx, commentsProjectID, commentsFaultID, commentID) + if err != nil { + return fmt.Errorf("failed to fetch updated comment: %w", err) + } + switch commentsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(comment, "", " ") diff --git a/cmd/teams.go b/cmd/teams.go index de9cde6..6f182fc 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -207,11 +207,15 @@ var teamsUpdateCmd = &cobra.Command{ WithAuthToken(authToken) ctx := context.Background() - team, err := client.Teams.Update(ctx, teamID, teamName) - if err != nil { + if err := client.Teams.Update(ctx, teamID, teamName); err != nil { return fmt.Errorf("failed to update team: %w", err) } + team, err := client.Teams.Get(ctx, teamID) + if err != nil { + return fmt.Errorf("failed to fetch updated team: %w", err) + } + switch teamsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(team, "", " ") @@ -354,11 +358,26 @@ var teamsMembersUpdateCmd = &cobra.Command{ WithAuthToken(authToken) ctx := context.Background() - member, err := client.Teams.UpdateMember(ctx, teamID, teamMemberID, teamMemberAdmin) - if err != nil { + if err := client.Teams.UpdateMember(ctx, teamID, teamMemberID, teamMemberAdmin); err != nil { return fmt.Errorf("failed to update team member: %w", err) } + members, err := client.Teams.ListMembers(ctx, teamID) + if err != nil { + return fmt.Errorf("failed to fetch updated team member: %w", err) + } + + var member *hbapi.TeamMember + for i := range members { + if members[i].ID == teamMemberID { + member = &members[i] + break + } + } + if member == nil { + return fmt.Errorf("updated team member not found: %d", teamMemberID) + } + switch teamsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(member, "", " ") @@ -665,16 +684,20 @@ Example JSON payload: } ctx := context.Background() - invitation, err := client.Teams.UpdateInvitation( + if err := client.Teams.UpdateInvitation( ctx, teamID, teamInvitationID, payload.TeamInvitation, - ) - if err != nil { + ); err != nil { return fmt.Errorf("failed to update team invitation: %w", err) } + invitation, err := client.Teams.GetInvitation(ctx, teamID, teamInvitationID) + if err != nil { + return fmt.Errorf("failed to fetch updated team invitation: %w", err) + } + switch teamsOutputFormat { case "json": jsonBytes, err := json.MarshalIndent(invitation, "", " ") diff --git a/go.mod b/go.mod index 80de6f0..bbb899a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/honeybadger-io/cli go 1.23 require ( - github.com/honeybadger-io/api-go v0.1.1-0.20260112212135-8d6771183f77 + github.com/honeybadger-io/api-go v0.1.1-0.20260112231002-e8d4216d8454 github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 diff --git a/go.sum b/go.sum index 0a859a0..6b610e0 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/honeybadger-io/api-go v0.1.1-0.20260112212135-8d6771183f77 h1:+Fl2zKOZzWiMQ+Eh49nvI1Q5RSe8MklK0hdNYVtRuns= -github.com/honeybadger-io/api-go v0.1.1-0.20260112212135-8d6771183f77/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= +github.com/honeybadger-io/api-go v0.1.1-0.20260112231002-e8d4216d8454 h1:2P2ngHXghF5ZkzlIpvNPTH61oW4M7kYedzZe5dSkAFQ= +github.com/honeybadger-io/api-go v0.1.1-0.20260112231002-e8d4216d8454/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= From 987482389a6386da79a7c0992bf44798db2c463b Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 15:18:51 -0800 Subject: [PATCH 13/17] Fix endpoint variable access --- cmd/agent.go | 3 ++- cmd/agent_test.go | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/agent.go b/cmd/agent.go index be4bb89..dc27bc1 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -110,9 +110,10 @@ func sendMetric(payload interface{}) error { return fmt.Errorf("error marshaling metrics: %w", err) } + apiEndpoint := viper.GetString("endpoint") req, err := http.NewRequest( "POST", - fmt.Sprintf("%s/v1/events", endpoint), + fmt.Sprintf("%s/v1/events", apiEndpoint), strings.NewReader(string(jsonData)+"\n"), ) if err != nil { diff --git a/cmd/agent_test.go b/cmd/agent_test.go index bf34e05..e711bb1 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -74,6 +74,7 @@ func TestMetricsCollection(t *testing.T) { // Set invalid endpoint endpoint = "http://invalid-endpoint" + viper.Set("endpoint", endpoint) hostname, err := os.Hostname() require.NoError(t, err) @@ -111,6 +112,7 @@ func TestMetricsCollection(t *testing.T) { // Configure viper viper.Set("api_key", "test-api-key") endpoint = server.URL + viper.Set("endpoint", endpoint) // Set up a short interval for testing interval = 1 // 1 second From 89b05663664d5acd252324708aea9a3a36b0befe Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Mon, 12 Jan 2026 15:19:09 -0800 Subject: [PATCH 14/17] Normalize endpoint URL --- cmd/root.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index fdd7ead..5ffcb01 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "net/url" "os" "strings" @@ -119,13 +120,31 @@ func initConfig() { // convertEndpointForDataAPI converts api.honeybadger.io to app.honeybadger.io for Data API calls func convertEndpointForDataAPI(endpoint string) string { - switch endpoint { + trimmed := strings.TrimSpace(endpoint) + if trimmed == "" { + return endpoint + } + + parsed, err := url.Parse(trimmed) + if err == nil && parsed.Scheme != "" && parsed.Host != "" { + switch parsed.Host { + case "api.honeybadger.io": + parsed.Host = "app.honeybadger.io" + case "eu-api.honeybadger.io": + parsed.Host = "eu-app.honeybadger.io" + } + return parsed.String() + } + + normalized := strings.TrimRight(trimmed, "/") + switch normalized { case "https://api.honeybadger.io": return "https://app.honeybadger.io" case "https://eu-api.honeybadger.io": return "https://eu-app.honeybadger.io" + default: + return trimmed } - return endpoint } // readJSONInput reads JSON from either a direct string or a file path prefixed with 'file://' From 449ea26a97c0424d562774fdf0752c10ab80e231 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 13 Jan 2026 10:02:37 -0800 Subject: [PATCH 15/17] Fix formatting --- cmd/accounts.go | 7 ++++++- cmd/checkins.go | 7 ++++++- cmd/teams.go | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/accounts.go b/cmd/accounts.go index 53f3df9..ea328db 100644 --- a/cmd/accounts.go +++ b/cmd/accounts.go @@ -267,7 +267,12 @@ var accountsUsersUpdateCmd = &cobra.Command{ WithAuthToken(authToken) ctx := context.Background() - if err := client.Accounts.UpdateUser(ctx, accountID, accountUserID, accountUserRole); err != nil { + if err := client.Accounts.UpdateUser( + ctx, + accountID, + accountUserID, + accountUserRole, + ); err != nil { return fmt.Errorf("failed to update user: %w", err) } diff --git a/cmd/checkins.go b/cmd/checkins.go index 84d6c82..990c910 100644 --- a/cmd/checkins.go +++ b/cmd/checkins.go @@ -302,7 +302,12 @@ Example JSON payload: } ctx := context.Background() - if err := client.CheckIns.Update(ctx, checkinsProjectID, checkinID, payload.CheckIn); err != nil { + if err := client.CheckIns.Update( + ctx, + checkinsProjectID, + checkinID, + payload.CheckIn, + ); err != nil { return fmt.Errorf("failed to update check-in: %w", err) } diff --git a/cmd/teams.go b/cmd/teams.go index 6f182fc..66b748f 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -358,7 +358,12 @@ var teamsMembersUpdateCmd = &cobra.Command{ WithAuthToken(authToken) ctx := context.Background() - if err := client.Teams.UpdateMember(ctx, teamID, teamMemberID, teamMemberAdmin); err != nil { + if err := client.Teams.UpdateMember( + ctx, + teamID, + teamMemberID, + teamMemberAdmin, + ); err != nil { return fmt.Errorf("failed to update team member: %w", err) } From 0ae47db6f63e4d673fa212e13567b5fd77c2314c Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 13 Jan 2026 13:36:43 -0800 Subject: [PATCH 16/17] Update README.md --- README.md | 296 ++++++------------------------------------------------ 1 file changed, 30 insertions(+), 266 deletions(-) diff --git a/README.md b/README.md index 234b564..69f8a76 100644 --- a/README.md +++ b/README.md @@ -48,291 +48,55 @@ Global flags that apply to all commands: ## Usage -### Deploy Command +Use `hb --help` to see detailed usage information for any command. -Report a deployment to Honeybadger. **Requires**: `--api-key` or `HONEYBADGER_API_KEY` (project API key) +### Reporting API Commands -```bash -hb deploy --environment production --repository github.com/org/repo --revision abc123 --user johndoe -``` +These commands use `--api-key` or `HONEYBADGER_API_KEY` (project API key): -**Required:** +| Command | Description | +|---------|-------------| +| `hb deploy` | Report a deployment to Honeybadger | +| `hb agent` | Start a metrics reporting agent that sends system metrics to Insights | - * `-e, --environment` - Environment being deployed to (e.g., `production`) +### Data API Commands -**Optional:** +These commands use `--auth-token` or `HONEYBADGER_AUTH_TOKEN` (personal auth token): - * `-r, --repository` - Repository being deployed (e.g., `github.com/org/repo`) - * `-v, --revision` - Revision or commit SHA being deployed - * `-u, --user` - Local username of the person deploying +| Command | Description | +|---------|-------------| +| `hb accounts` | Manage Honeybadger accounts and team members | +| `hb check-ins` | Manage check-ins for cron job and scheduled task monitoring | +| `hb comments` | Manage comments on faults | +| `hb deployments` | View and manage deployment history | +| `hb environments` | Manage project environments | +| `hb faults` | View and manage faults (errors) in your projects | +| `hb insights` | Execute BadgerQL queries against your Insights data | +| `hb projects` | Manage Honeybadger projects | +| `hb statuspages` | Manage public status pages | +| `hb teams` | Manage teams and team memberships | +| `hb uptime` | Manage uptime monitoring checks | -### Agent Command - -Start a metrics reporting agent that collects and sends system metrics to Honeybadger Insights. **Requires**: `--api-key` or `HONEYBADGER_API_KEY` (project API key) +### Examples ```bash -hb agent -``` - -The agent collects and reports the following metrics: - * CPU usage and load averages - * Memory usage (total, used, free, available) - * Disk usage for all mounted filesystems - -**Optional:** - - * `--interval` - Reporting interval in seconds (default: `60`) +# Report a deployment +hb deploy --environment production --revision abc123 -### Projects Command +# Start the metrics agent +hb agent --interval 60 -Manage Honeybadger projects. **Requires**: `--auth-token` or `HONEYBADGER_AUTH_TOKEN` (personal auth token) - -```bash # List all projects hb projects list -# List projects by account ID -hb projects list --account-id 12345 - -# Get project details -hb projects get --id 12345 - -# Create a new project (using inline JSON) -hb projects create --account-id 12345 --cli-input-json '{"project": {"name": "My Project"}}' - -# Create a new project (using a JSON file) -hb projects create --account-id 12345 --cli-input-json file://project.json - -# Update a project (using inline JSON) -hb projects update --id 12345 --cli-input-json '{"project": {"name": "Updated Name", "resolve_errors_on_deploy": true}}' - -# Update a project (using a JSON file) -hb projects update --id 12345 --cli-input-json file://updates.json - -# Delete a project -hb projects delete --id 12345 - -# Get occurrence counts for all projects -hb projects occurrences --period day --environment production - -# Get occurrence counts for a specific project -hb projects occurrences --id 12345 --period hour - -# Get integrations for a project -hb projects integrations --id 12345 - -# Get report data for a project -hb projects reports --id 12345 --type notices_per_day --start 2024-01-01T00:00:00Z --stop 2024-01-31T23:59:59Z -``` - -#### list - -**Optional:** - - * `--account-id` - Filter projects by account ID - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -#### get - -**Required:** - - * `--id` - Project ID - -**Optional:** - - * `-o, --output` - Output format: `text` or `json` (default: `text`) - -#### create - -**Required:** - - * `--account-id` - Account ID to create the project in - * `--cli-input-json` - JSON payload (inline string or `file://path`) - -**Optional:** - - * `-o, --output` - Output format: `text` or `json` (default: `text`) - -JSON payload format: -```json -{ - "project": { - "name": "My Project", - "resolve_errors_on_deploy": true, - "disable_public_links": false, - "language": "ruby", - "user_url": "https://myapp.com/users/[user_id]", - "source_url": "https://github.com/myorg/myrepo/blob/main/[filename]#L[line]", - "purge_days": 90, - "user_search_field": "user_id" - } -} -``` - -#### update - -**Required:** - - * `--id` - Project ID - * `--cli-input-json` - JSON payload (inline string or `file://path`) - -JSON payload format (all fields optional): -```json -{ - "project": { - "name": "Updated Name", - "resolve_errors_on_deploy": false, - "purge_days": 120 - } -} -``` - -#### delete - -**Required:** - - * `--id` - Project ID - -#### occurrences - -**Optional:** - - * `--id` - Project ID (if omitted, shows data for all projects) - * `--period` - Time period: `hour`, `day`, `week`, or `month` (default: `day`) - * `--environment` - Filter by environment name - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -#### integrations - -**Required:** - - * `--id` - Project ID - -**Optional:** - - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -#### reports - -**Required:** - - * `--id` - Project ID - * `--type` - Report type: `notices_by_class`, `notices_by_location`, `notices_by_user`, or `notices_per_day` - -**Optional:** - - * `--start` - Start time in RFC3339 format (e.g., `2024-01-01T00:00:00Z`) - * `--stop` - Stop time in RFC3339 format (e.g., `2024-01-31T23:59:59Z`) - * `--environment` - Filter by environment name - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -See https://docs.honeybadger.io/api/projects/ for more information. - -### Faults Command - -View and manage faults (errors) in your Honeybadger projects. **Requires**: `--auth-token` or `HONEYBADGER_AUTH_TOKEN` (personal auth token) +# Query Insights data +hb insights query --project-id 12345 --query "fields @ts, @preview | sort @ts" -```bash # List faults for a project hb faults list --project-id 12345 - -# List with filtering -hb faults list --project-id 12345 --query "class:RuntimeError" --order recent --limit 10 - -# Get fault details -hb faults get --project-id 12345 --id 67890 - -# List notices for a fault -hb faults notices --project-id 12345 --id 67890 - -# Get fault counts -hb faults counts --project-id 12345 - -# List users affected by a fault -hb faults affected-users --project-id 12345 --id 67890 ``` -**Note:** All faults commands require `--project-id` (Project ID) - -#### list - -**Optional:** - - * `-q, --query` - Search query string - * `--order` - Sort order: `recent` or `frequent` - * `--limit` - Maximum number of results (max: 25) - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -#### get - -**Required:** - - * `--id` - Fault ID - -**Optional:** - - * `-o, --output` - Output format: `text` or `json` (default: `text`) - -#### notices - -**Required:** - - * `--id` - Fault ID - -**Optional:** - - * `--limit` - Maximum number of results (max: 25) - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -#### counts - -**Optional:** - - * `-o, --output` - Output format: `text` or `json` (default: `text`) - -#### affected-users - -**Required:** - - * `--id` - Fault ID - -**Optional:** - - * `-q, --query` - Search query to filter users - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -See https://docs.honeybadger.io/api/faults/ for more information. - -### Insights Command - -Execute BadgerQL queries against your Honeybadger Insights data. **Requires**: `--auth-token` or `HONEYBADGER_AUTH_TOKEN` (personal auth token) - -```bash -# Basic query for timestamps and previews -hb insights query --project-id 12345 --query "fields @ts, @preview | sort @ts" - -# Query with timezone -hb insights query --project-id 12345 --query "fields @ts, @preview | sort @ts" --timezone "America/New_York" - -# Query at a specific timestamp -hb insights query --project-id 12345 --query "fields @ts, @preview | sort @ts" --ts "PT1H" - -# Output as JSON -hb insights query --project-id 12345 --query "fields @ts, @preview | sort @ts" --output json -``` - -**Required:** - - * `--project-id` - Project ID - * `-q, --query` - BadgerQL query to execute - -**Optional:** - - * `--ts` - Timestamp range for the query (e.g., `PT1H`) - * `--timezone` - Timezone for the query (e.g., `America/New_York`) - * `-o, --output` - Output format: `table` or `json` (default: `table`) - -See https://docs.honeybadger.io/api/insights/#query-insights-data for more information. +See the [Honeybadger CLI documentation](https://docs.honeybadger.io/resources/cli/) for more information. ## Development From 6f5dfd1568add1f0b0ff4a23609a21f707ea68ad Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 13 Jan 2026 13:54:46 -0800 Subject: [PATCH 17/17] Bump github.com/honeybadger-io/api-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bbb899a..d9588ae 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/honeybadger-io/cli go 1.23 require ( - github.com/honeybadger-io/api-go v0.1.1-0.20260112231002-e8d4216d8454 + github.com/honeybadger-io/api-go v0.2.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 diff --git a/go.sum b/go.sum index 6b610e0..e7ac228 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/honeybadger-io/api-go v0.1.1-0.20260112231002-e8d4216d8454 h1:2P2ngHXghF5ZkzlIpvNPTH61oW4M7kYedzZe5dSkAFQ= -github.com/honeybadger-io/api-go v0.1.1-0.20260112231002-e8d4216d8454/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= +github.com/honeybadger-io/api-go v0.2.0 h1:ezoDgaBomS8eP022F4rmzFgKhyRmzqvcYDiMSKhd6sg= +github.com/honeybadger-io/api-go v0.2.0/go.mod h1:VurinfWN9qR1Bd9Zju2pIDgG1vIi5Qq3GpcsfToQiBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=