feat: v0.3.0d — add PR checks, iostreams, aliases, and broad enhancements

Add PR checks command, iostreams/text packages for colored table output,
top-level run/workflow aliases matching gh CLI structure. Enhance actions,
issues, PRs, releases, repos, labels, milestones, and wiki commands with
improved flags, JSON output, and error handling.
This commit is contained in:
sid 2026-03-23 11:42:44 -06:00
parent 7c0dcc8696
commit 113505de95
29 changed files with 3131 additions and 542 deletions

View file

@ -5,29 +5,28 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"text/tabwriter"
"time"
"forgejo.zerova.net/sid/fgj-sid/internal/api"
"forgejo.zerova.net/sid/fgj-sid/internal/config"
"forgejo.zerova.net/sid/fgj-sid/internal/text"
"github.com/spf13/cobra"
)
// Wiki API response types
type wikiPageMeta struct {
Title string `json:"title"`
HTMLURL string `json:"html_url"`
SubURL string `json:"sub_url"`
Title string `json:"title"`
HTMLURL string `json:"html_url"`
SubURL string `json:"sub_url"`
LastCommit *wikiCommit `json:"last_commit"`
}
type wikiCommit struct {
ID string `json:"id"`
Author *wikiUser `json:"author"`
Committer *wikiUser `json:"committer"`
Message string `json:"message"`
ID string `json:"id"`
Author *wikiUser `json:"author"`
Committer *wikiUser `json:"committer"`
Message string `json:"message"`
}
type wikiUser struct {
@ -79,6 +78,9 @@ var wikiViewCmd = &cobra.Command{
Example: ` # View a wiki page
fgj wiki view Home
# Open in browser
fgj wiki view Home --web
# View a wiki page as JSON (includes content)
fgj wiki view Home --json
@ -133,6 +135,9 @@ var wikiDeleteCmd = &cobra.Command{
Example: ` # Delete a wiki page
fgj wiki delete "Old Page"
# Delete without confirmation
fgj wiki delete "Old Page" -y
# Delete a wiki page from a specific repo
fgj wiki delete "Outdated Guide" -R owner/repo`,
Args: cobra.ExactArgs(1),
@ -148,22 +153,24 @@ func init() {
wikiCmd.AddCommand(wikiDeleteCmd)
wikiListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
wikiListCmd.Flags().Bool("json", false, "Output as JSON")
addJSONFlags(wikiListCmd, "Output as JSON")
wikiViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
wikiViewCmd.Flags().Bool("json", false, "Output as JSON")
addJSONFlags(wikiViewCmd, "Output as JSON")
wikiViewCmd.Flags().BoolP("web", "w", false, "Open in web browser")
wikiCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
wikiCreateCmd.Flags().StringP("body", "b", "", "Wiki page content")
wikiCreateCmd.Flags().String("body-file", "", "Read content from file (use \"-\" for stdin)")
wikiCreateCmd.Flags().Bool("json", false, "Output created page as JSON")
addJSONFlags(wikiCreateCmd, "Output created page as JSON")
wikiEditCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
wikiEditCmd.Flags().StringP("body", "b", "", "Wiki page content")
wikiEditCmd.Flags().String("body-file", "", "Read content from file (use \"-\" for stdin)")
wikiEditCmd.Flags().Bool("json", false, "Output updated page as JSON")
addJSONFlags(wikiEditCmd, "Output updated page as JSON")
wikiDeleteCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
wikiDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
}
func newWikiClient(cmd *cobra.Command) (*api.Client, string, string, error) {
@ -194,37 +201,38 @@ func runWikiList(cmd *cobra.Command, args []string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", url.PathEscape(owner), url.PathEscape(name))
ios.StartSpinner("Fetching wiki pages...")
var pages []wikiPageMeta
if err := client.GetJSON(path, &pages); err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to list wiki pages: %w", err)
}
ios.StopSpinner()
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(pages)
if wantJSON(cmd) {
return outputJSON(cmd, pages)
}
if len(pages) == 0 {
fmt.Println("No wiki pages found")
fmt.Fprintln(ios.Out, "No wiki pages found")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "TITLE\tLAST UPDATED\n")
isTTY := ios.IsStdoutTTY()
tp := ios.NewTablePrinter()
tp.AddHeader("TITLE", "LAST UPDATED")
for _, p := range pages {
updated := ""
if p.LastCommit != nil && p.LastCommit.Committer != nil && p.LastCommit.Committer.Date != "" {
if t, err := time.Parse(time.RFC3339, p.LastCommit.Committer.Date); err == nil {
updated = t.Format("2006-01-02 15:04:05")
updated = text.FormatDate(t, isTTY)
} else {
updated = p.LastCommit.Committer.Date
}
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", p.Title, updated)
tp.AddRow(p.Title, updated)
}
_ = w.Flush()
return nil
return tp.Render()
}
func runWikiView(cmd *cobra.Command, args []string) error {
@ -238,27 +246,42 @@ func runWikiView(cmd *cobra.Command, args []string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s",
url.PathEscape(owner), url.PathEscape(name), url.PathEscape(title))
ios.StartSpinner("Fetching wiki page...")
var page wikiPage
if err := client.GetJSON(path, &page); err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to get wiki page: %w", err)
}
ios.StopSpinner()
content, err := base64.StdEncoding.DecodeString(page.ContentBase64)
if err != nil {
return fmt.Errorf("failed to decode wiki page content: %w", err)
}
if web, _ := cmd.Flags().GetBool("web"); web {
if page.HTMLURL != "" {
return ios.OpenInBrowser(page.HTMLURL)
}
return fmt.Errorf("wiki page has no HTML URL")
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
page.Content = string(content)
return writeJSON(page)
}
fmt.Printf("# %s\n\n", page.Title)
fmt.Print(string(content))
if err := ios.StartPager(); err != nil {
fmt.Fprintf(ios.ErrOut, "warning: failed to start pager: %v\n", err)
}
defer ios.StopPager()
fmt.Fprintf(ios.Out, "# %s\n\n", page.Title)
fmt.Fprint(ios.Out, string(content))
// Ensure trailing newline
if len(content) > 0 && content[len(content)-1] != '\n' {
fmt.Println()
fmt.Fprintln(ios.Out)
}
return nil
@ -288,17 +311,20 @@ func runWikiCreate(cmd *cobra.Command, args []string) error {
ContentBase64: base64.StdEncoding.EncodeToString([]byte(body)),
}
ios.StartSpinner("Creating wiki page...")
var page wikiPage
if _, err := client.DoJSON(http.MethodPost, path, reqBody, &page); err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to create wiki page: %w", err)
}
ios.StopSpinner()
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(page)
if wantJSON(cmd) {
return outputJSON(cmd, page)
}
fmt.Printf("Wiki page created: %s\n", title)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Wiki page created: %s\n", cs.SuccessIcon(), title)
return nil
}
@ -326,35 +352,54 @@ func runWikiEdit(cmd *cobra.Command, args []string) error {
ContentBase64: base64.StdEncoding.EncodeToString([]byte(body)),
}
ios.StartSpinner("Updating wiki page...")
var page wikiPage
if _, err := client.DoJSON(http.MethodPatch, path, reqBody, &page); err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to update wiki page: %w", err)
}
ios.StopSpinner()
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(page)
if wantJSON(cmd) {
return outputJSON(cmd, page)
}
fmt.Printf("Wiki page updated: %s\n", title)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Wiki page updated: %s\n", cs.SuccessIcon(), title)
return nil
}
func runWikiDelete(cmd *cobra.Command, args []string) error {
title := args[0]
yes, _ := cmd.Flags().GetBool("yes")
client, owner, name, err := newWikiClient(cmd)
if err != nil {
return err
}
if !yes {
confirmed, err := ios.ConfirmAction(fmt.Sprintf("Delete wiki page %q?", title))
if err != nil {
return err
}
if !confirmed {
fmt.Fprintln(ios.ErrOut, "Aborted")
return nil
}
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s",
url.PathEscape(owner), url.PathEscape(name), url.PathEscape(title))
ios.StartSpinner("Deleting wiki page...")
if _, err := client.DoJSON(http.MethodDelete, path, nil, nil); err != nil {
ios.StopSpinner()
return fmt.Errorf("failed to delete wiki page: %w", err)
}
ios.StopSpinner()
fmt.Printf("Wiki page deleted: %s\n", title)
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Wiki page deleted: %s\n", cs.SuccessIcon(), title)
return nil
}