package cmd import ( "fmt" "os" "strconv" "strings" "forgejo.zerova.net/public/fgj-sid/internal/git" "github.com/spf13/cobra" "github.com/spf13/viper" ) var cfgFile string var jsonErrors bool // version is set at build time via -ldflags "-X .../cmd.version=...". // Defaults to "dev" for plain `go build` / `go run`. var version = "dev" var rootCmd = &cobra.Command{ Use: "fgj", Short: "Forgejo CLI tool - work seamlessly with Forgejo from the command line", Long: `fgj is a command line tool for Forgejo instances (including Codeberg). It brings pull requests, issues, and other Forgejo concepts to the terminal.`, Version: version, SilenceErrors: true, } // JSONErrors reports whether the --json-errors flag is set. func JSONErrors() bool { return jsonErrors } func Execute() error { return rootCmd.Execute() } func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/fgj/config.yaml)") rootCmd.PersistentFlags().BoolVar(&jsonErrors, "json-errors", false, "output errors as structured JSON to stderr") rootCmd.PersistentFlags().String("hostname", "", "Forgejo instance hostname") _ = viper.BindPFlag("hostname", rootCmd.PersistentFlags().Lookup("hostname")) } func initConfig() { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { home, err := os.UserHomeDir() if err != nil { fmt.Fprintln(ios.ErrOut, err) os.Exit(1) } configDir := home + "/.config/fgj" _ = os.MkdirAll(configDir, 0755) viper.AddConfigPath(configDir) viper.SetConfigType("yaml") viper.SetConfigName("config") } viper.AutomaticEnv() viper.SetEnvPrefix("FGJ") _ = viper.ReadInConfig() } // parseRepo parses the repository string in the format "owner/name". // If not provided, it attempts to auto-detect from the git repository. func parseRepo(repo string) (string, string, error) { // If repo flag is provided, use it if repo != "" { parts := strings.Split(repo, "/") if len(parts) != 2 { return "", "", fmt.Errorf("invalid repository format: %s (expected: owner/name)", repo) } return parts[0], parts[1], nil } // Try to auto-detect from git owner, name, err := git.DetectRepo() if err != nil { return "", "", fmt.Errorf("repository flag is required (use -R owner/name) or run from a git repository: %w", err) } return owner, name, nil } // getDetectedHost attempts to auto-detect the Forgejo instance hostname. // Returns empty string if detection fails, which will fall back to other methods. func getDetectedHost() string { host, err := git.DetectHost() if err != nil { return "" } return host } // getCwd returns the current working directory, or "" on error. func getCwd() string { cwd, err := os.Getwd() if err != nil { return "" } return cwd } // promptLine prints a prompt to stderr and reads a line from stdin. func promptLine(prompt string) (string, error) { fmt.Fprint(ios.ErrOut, prompt) var buf [1024]byte n, err := ios.In.Read(buf[:]) if err != nil { return "", fmt.Errorf("reading input: %w", err) } return strings.TrimSpace(string(buf[:n])), nil } // parseIssueArg parses an issue/PR number from various formats: // "123", "#123", "https://host/owner/repo/pulls/123", "https://host/owner/repo/issues/123" func parseIssueArg(arg string) (int64, error) { arg = strings.TrimPrefix(arg, "#") // Try URL format if strings.HasPrefix(arg, "http") { parts := strings.Split(strings.TrimRight(arg, "/"), "/") arg = parts[len(parts)-1] } return strconv.ParseInt(arg, 10, 64) }