2025-12-08 09:49:07 +01:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"syscall"
|
|
|
|
|
|
feat: v0.3.0a — add api command, pr diff/comment/review, structured errors
New commands:
- fgj api: raw REST API passthrough with field inference and path interpolation
- fgj pr diff: view PR diffs with color, --name-only, --stat
- fgj pr comment: add comments to pull requests
- fgj pr review: approve, request changes, or comment on PRs
Agentic enhancements:
- --json-errors flag for structured JSON error output on stderr
- APIError type wrapping HTTP status codes for machine consumption
- Error codes: auth_required, not_found, api_error, invalid_input, etc.
Docs updated for forgejo.zerova.net/sid/fgj-sid fork.
2026-03-21 21:50:24 -06:00
|
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/api"
|
|
|
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/config"
|
2026-01-18 11:45:46 +01:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
"github.com/spf13/viper"
|
2025-12-08 09:49:07 +01:00
|
|
|
"golang.org/x/term"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var authCmd = &cobra.Command{
|
|
|
|
|
Use: "auth",
|
|
|
|
|
Short: "Authenticate fgj with a Forgejo instance",
|
|
|
|
|
Long: "Manage authentication state for Forgejo instances.",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var authLoginCmd = &cobra.Command{
|
|
|
|
|
Use: "login",
|
|
|
|
|
Short: "Authenticate with a Forgejo instance",
|
|
|
|
|
Long: "Authenticate with a Forgejo instance using a personal access token.",
|
|
|
|
|
RunE: runAuthLogin,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var authStatusCmd = &cobra.Command{
|
|
|
|
|
Use: "status",
|
|
|
|
|
Short: "View authentication status",
|
|
|
|
|
Long: "Display the authentication status for configured Forgejo instances.",
|
|
|
|
|
RunE: runAuthStatus,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 11:45:46 +01:00
|
|
|
var authLogoutCmd = &cobra.Command{
|
|
|
|
|
Use: "logout",
|
|
|
|
|
Short: "Remove authentication for a Forgejo instance",
|
|
|
|
|
Long: "Remove authentication for a configured Forgejo instance.",
|
|
|
|
|
RunE: runAuthLogout,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var authTokenCmd = &cobra.Command{
|
|
|
|
|
Use: "token",
|
|
|
|
|
Short: "Print the stored authentication token",
|
|
|
|
|
Long: "Print the stored authentication token for a configured Forgejo instance.",
|
|
|
|
|
RunE: runAuthToken,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 09:49:07 +01:00
|
|
|
func init() {
|
|
|
|
|
rootCmd.AddCommand(authCmd)
|
|
|
|
|
authCmd.AddCommand(authLoginCmd)
|
|
|
|
|
authCmd.AddCommand(authStatusCmd)
|
2026-01-18 11:45:46 +01:00
|
|
|
authCmd.AddCommand(authLogoutCmd)
|
|
|
|
|
authCmd.AddCommand(authTokenCmd)
|
2025-12-08 09:49:07 +01:00
|
|
|
|
|
|
|
|
authLoginCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)")
|
|
|
|
|
authLoginCmd.Flags().StringP("token", "t", "", "Personal access token")
|
2026-01-18 11:45:46 +01:00
|
|
|
authLogoutCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)")
|
|
|
|
|
authTokenCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)")
|
2025-12-08 09:49:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runAuthLogin(cmd *cobra.Command, args []string) error {
|
|
|
|
|
hostname, _ := cmd.Flags().GetString("hostname")
|
|
|
|
|
token, _ := cmd.Flags().GetString("token")
|
|
|
|
|
|
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
|
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
fmt.Print("Forgejo instance hostname (default: codeberg.org): ")
|
|
|
|
|
input, _ := reader.ReadString('\n')
|
|
|
|
|
hostname = strings.TrimSpace(input)
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
hostname = "codeberg.org"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if token == "" {
|
|
|
|
|
fmt.Print("Personal access token: ")
|
|
|
|
|
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to read token: %w", err)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println()
|
|
|
|
|
token = strings.TrimSpace(string(tokenBytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if token == "" {
|
|
|
|
|
return fmt.Errorf("token is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client, err := api.NewClient(hostname, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create client: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user, _, err := client.GetMyUserInfo()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("authentication failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg, err := config.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg.SetHost(hostname, config.HostConfig{
|
2026-01-18 11:45:46 +01:00
|
|
|
Hostname: hostname,
|
|
|
|
|
Token: token,
|
|
|
|
|
User: user.UserName,
|
2025-12-08 09:49:07 +01:00
|
|
|
GitProtocol: "https",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err := cfg.Save(); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("✓ Authenticated as %s on %s\n", user.UserName, hostname)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runAuthStatus(cmd *cobra.Command, args []string) error {
|
|
|
|
|
cfg, err := config.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(cfg.Hosts) == 0 {
|
|
|
|
|
fmt.Println("Not authenticated with any Forgejo instances")
|
|
|
|
|
fmt.Println("Run 'fgj auth login' to authenticate")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println("Authenticated instances:")
|
|
|
|
|
for hostname, host := range cfg.Hosts {
|
|
|
|
|
fmt.Printf(" • %s (user: %s)\n", hostname, host.User)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-01-18 11:45:46 +01:00
|
|
|
|
|
|
|
|
func runAuthLogout(cmd *cobra.Command, args []string) error {
|
|
|
|
|
cfg, err := config.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hostname, _ := cmd.Flags().GetString("hostname")
|
|
|
|
|
resolved, err := resolveAuthHostname(cfg, hostname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete(cfg.Hosts, resolved)
|
|
|
|
|
if err := cfg.Save(); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("✓ Logged out from %s\n", resolved)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runAuthToken(cmd *cobra.Command, args []string) error {
|
|
|
|
|
cfg, err := config.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hostname, _ := cmd.Flags().GetString("hostname")
|
|
|
|
|
resolved, err := resolveAuthHostname(cfg, hostname)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(cfg.Hosts[resolved].Token)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resolveAuthHostname(cfg *config.Config, hostname string) (string, error) {
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
hostname = viper.GetString("hostname")
|
|
|
|
|
}
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
hostname = os.Getenv("FGJ_HOST")
|
|
|
|
|
}
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
hostname = getDetectedHost()
|
|
|
|
|
}
|
|
|
|
|
if hostname == "" && len(cfg.Hosts) == 1 {
|
|
|
|
|
for host := range cfg.Hosts {
|
|
|
|
|
hostname = host
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
hostname = "codeberg.org"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, ok := cfg.Hosts[hostname]; !ok {
|
|
|
|
|
return "", fmt.Errorf("no configuration found for host %s", hostname)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hostname, nil
|
|
|
|
|
}
|