package cmd import ( "bufio" "fmt" "os" "strings" "syscall" "forgejo.zerova.net/public/fj/internal/api" "forgejo.zerova.net/public/fj/internal/config" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/term" ) var authCmd = &cobra.Command{ Use: "auth", Short: "Authenticate fj 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, } 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, } func init() { rootCmd.AddCommand(authCmd) authCmd.AddCommand(authLoginCmd) authCmd.AddCommand(authStatusCmd) authCmd.AddCommand(authLogoutCmd) authCmd.AddCommand(authTokenCmd) authLoginCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)") authLoginCmd.Flags().StringP("token", "t", "", "Personal access token") authLogoutCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)") authTokenCmd.Flags().String("hostname", "", "Forgejo instance hostname (e.g., codeberg.org)") } 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.Fprint(ios.ErrOut, "Forgejo instance hostname (default: codeberg.org): ") input, _ := reader.ReadString('\n') hostname = strings.TrimSpace(input) if hostname == "" { hostname = "codeberg.org" } } if token == "" { fmt.Fprint(ios.ErrOut, "Personal access token: ") tokenBytes, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { return fmt.Errorf("failed to read token: %w", err) } fmt.Fprintln(ios.ErrOut) 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) } ios.StartSpinner("Authenticating...") user, _, err := client.GetMyUserInfo() ios.StopSpinner() 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{ Hostname: hostname, Token: token, User: user.UserName, GitProtocol: "https", }) if err := cfg.Save(); err != nil { return fmt.Errorf("failed to save config: %w", err) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Authenticated as %s on %s\n", cs.SuccessIcon(), 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.Fprintln(ios.Out, "Not authenticated with any Forgejo instances") fmt.Fprintln(ios.Out, "Run 'fj auth login' to authenticate") return nil } fmt.Fprintln(ios.Out, "Authenticated instances:") for hostname, host := range cfg.Hosts { cs := ios.ColorScheme() fmt.Fprintf(ios.Out, " %s %s (user: %s)\n", cs.SuccessIcon(), hostname, host.User) } return nil } 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) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Logged out from %s\n", cs.SuccessIcon(), 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.Fprintln(ios.Out, 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 }