feat: add branch, notification, org, open, whoami commands

Ports five commands from tea that fgj-sid was missing:

- fgj branch {list,rename,delete} — list branches with protection
  status, rename, and delete with confirmation.
- fgj notification {list,read} — list user notifications (unread by
  default, --all for everything), mark individual threads read.
- fgj org {list,create,delete} — manage organizations on the host.
  Create accepts --description/--full-name/--website/--location and
  --visibility (public/limited/private).
- fgj open [number] — open the repo, issue, or PR in a browser.
  Auto-detects issue-vs-PR via GetIssue. Falls back to printing the
  URL when stdout is not a TTY or --url is passed.
- fgj whoami — display authenticated user + host.

All commands follow the established pattern (parseRepo + config.Load +
api.NewClientFromConfig + ios), support --json where list semantics
apply, and share a new loadClient helper for host-scoped (non-repo)
commands. Tested live against forgejo.zerova.net.

Refs audit recommendation.md §'v0.5.0 — Missing resources'.
This commit is contained in:
sid 2026-04-19 21:27:55 -06:00
parent d4b5b79541
commit 17ca49d0c5
5 changed files with 665 additions and 0 deletions

137
cmd/notification.go Normal file
View file

@ -0,0 +1,137 @@
package cmd
import (
"fmt"
"code.gitea.io/sdk/gitea"
"forgejo.zerova.net/public/fgj-sid/internal/api"
"forgejo.zerova.net/public/fgj-sid/internal/config"
"github.com/spf13/cobra"
)
var notificationCmd = &cobra.Command{
Use: "notification",
Aliases: []string{"notifications", "n"},
Short: "Manage user notifications",
Long: "List and mark notifications for the authenticated user.",
}
var notificationListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List notifications",
Long: "List notifications for the authenticated user. Shows unread by default.",
Example: ` # List unread notifications
fgj notification list
# Include read and pinned notifications
fgj notification list --all
# Limit number of results
fgj notification list -L 50
# Output as JSON
fgj notification list --json`,
RunE: runNotificationList,
}
var notificationReadCmd = &cobra.Command{
Use: "read <id>",
Aliases: []string{"r"},
Short: "Mark a notification as read",
Long: "Mark a single notification thread as read by its ID.",
Args: cobra.ExactArgs(1),
RunE: runNotificationRead,
}
func init() {
rootCmd.AddCommand(notificationCmd)
notificationCmd.AddCommand(notificationListCmd)
notificationCmd.AddCommand(notificationReadCmd)
notificationListCmd.Flags().Bool("all", false, "Include read and pinned notifications (not just unread)")
notificationListCmd.Flags().IntP("limit", "L", 30, "Maximum number of notifications to list")
addJSONFlags(notificationListCmd, "Output as JSON")
}
func runNotificationList(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return err
}
all, _ := cmd.Flags().GetBool("all")
limit, _ := cmd.Flags().GetInt("limit")
if limit <= 0 {
limit = 30
}
opt := gitea.ListNotificationOptions{
ListOptions: gitea.ListOptions{PageSize: limit},
Status: []gitea.NotifyStatus{gitea.NotifyStatusUnread},
}
if all {
opt.Status = []gitea.NotifyStatus{gitea.NotifyStatusUnread, gitea.NotifyStatusRead, gitea.NotifyStatusPinned}
}
threads, _, err := client.ListNotifications(opt)
if err != nil {
return fmt.Errorf("failed to list notifications: %w", err)
}
if wantJSON(cmd) {
return outputJSON(cmd, threads)
}
if len(threads) == 0 {
fmt.Fprintln(ios.Out, "No notifications.")
return nil
}
tp := ios.NewTablePrinter()
tp.AddHeader("ID", "REPO", "TYPE", "STATE", "TITLE")
for _, t := range threads {
repo := ""
if t.Repository != nil {
repo = t.Repository.FullName
}
subjType, subjState, title := "", "", ""
if t.Subject != nil {
subjType = string(t.Subject.Type)
subjState = string(t.Subject.State)
title = t.Subject.Title
}
tp.AddRow(fmt.Sprintf("%d", t.ID), repo, subjType, subjState, title)
}
return tp.Render()
}
func runNotificationRead(cmd *cobra.Command, args []string) error {
id, err := parseIssueArg(args[0])
if err != nil {
return fmt.Errorf("invalid notification id %q: %w", args[0], err)
}
cfg, err := config.Load()
if err != nil {
return err
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return err
}
if _, _, err := client.ReadNotification(id, gitea.NotifyStatusRead); err != nil {
return fmt.Errorf("failed to mark notification %d as read: %w", id, err)
}
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Marked notification %d as read\n", cs.SuccessIcon(), id)
return nil
}