fj/cmd/wiki.go
sid c2251d9932
Some checks failed
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
CI / functional (push) Has been cancelled
chore: migrate module path to public org
Move from forgejo.zerova.net/sid/fgj-sid to
forgejo.zerova.net/public/fgj-sid to reflect the new public org.
2026-04-11 10:34:34 -06:00

405 lines
10 KiB
Go

package cmd
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"time"
"forgejo.zerova.net/public/fgj-sid/internal/api"
"forgejo.zerova.net/public/fgj-sid/internal/config"
"forgejo.zerova.net/public/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"`
LastCommit *wikiCommit `json:"last_commit"`
}
type wikiCommit struct {
ID string `json:"id"`
Author *wikiUser `json:"author"`
Committer *wikiUser `json:"committer"`
Message string `json:"message"`
}
type wikiUser struct {
Name string `json:"name"`
Email string `json:"email"`
Date string `json:"date"`
}
type wikiPage struct {
Title string `json:"title"`
HTMLURL string `json:"html_url"`
SubURL string `json:"sub_url"`
ContentBase64 string `json:"content_base64"`
LastCommit *wikiCommit `json:"last_commit"`
// Decoded content for JSON output
Content string `json:"content,omitempty"`
}
type wikiCreateRequest struct {
Title string `json:"title"`
ContentBase64 string `json:"content_base64"`
}
var wikiCmd = &cobra.Command{
Use: "wiki",
Short: "Manage repository wiki pages",
Long: "View and manage wiki pages for a repository.",
}
var wikiListCmd = &cobra.Command{
Use: "list",
Short: "List wiki pages",
Long: "List all wiki pages for a repository.",
Example: ` # List wiki pages for the current repo
fgj wiki list
# List wiki pages for a specific repo
fgj wiki list -R owner/repo
# Output as JSON
fgj wiki list --json`,
RunE: runWikiList,
}
var wikiViewCmd = &cobra.Command{
Use: "view <title>",
Short: "View a wiki page",
Long: "Display the content of a wiki page.",
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
# View a wiki page from a specific repo
fgj wiki view "Getting-Started" -R owner/repo`,
Args: cobra.ExactArgs(1),
RunE: runWikiView,
}
var wikiCreateCmd = &cobra.Command{
Use: "create <title>",
Short: "Create a wiki page",
Long: "Create a new wiki page in the repository.",
Example: ` # Create a wiki page with inline content
fgj wiki create "Getting Started" -b "# Welcome\nThis is the getting started guide."
# Create a wiki page from a file
fgj wiki create "Setup Guide" --body-file setup.md
# Create a wiki page from stdin
echo "# FAQ" | fgj wiki create FAQ --body-file -
# Output as JSON
fgj wiki create "New Page" -b "Content here" --json`,
Args: cobra.ExactArgs(1),
RunE: runWikiCreate,
}
var wikiEditCmd = &cobra.Command{
Use: "edit <title>",
Short: "Edit a wiki page",
Long: "Edit an existing wiki page in the repository.",
Example: ` # Edit a wiki page with new content
fgj wiki edit Home -b "# Updated Home\nNew content here."
# Edit a wiki page from a file
fgj wiki edit "Setup Guide" --body-file updated-setup.md
# Edit a wiki page from stdin
cat new-content.md | fgj wiki edit Home --body-file -
# Output as JSON
fgj wiki edit Home -b "Updated content" --json`,
Args: cobra.ExactArgs(1),
RunE: runWikiEdit,
}
var wikiDeleteCmd = &cobra.Command{
Use: "delete <title>",
Short: "Delete a wiki page",
Long: "Delete a wiki page from the repository.",
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),
RunE: runWikiDelete,
}
func init() {
rootCmd.AddCommand(wikiCmd)
wikiCmd.AddCommand(wikiListCmd)
wikiCmd.AddCommand(wikiViewCmd)
wikiCmd.AddCommand(wikiCreateCmd)
wikiCmd.AddCommand(wikiEditCmd)
wikiCmd.AddCommand(wikiDeleteCmd)
wikiListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
addJSONFlags(wikiListCmd, "Output as JSON")
wikiViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
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)")
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)")
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) {
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return nil, "", "", err
}
cfg, err := config.Load()
if err != nil {
return nil, "", "", err
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return nil, "", "", err
}
return client, owner, name, nil
}
func runWikiList(cmd *cobra.Command, args []string) error {
client, owner, name, err := newWikiClient(cmd)
if err != nil {
return err
}
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()
if wantJSON(cmd) {
return outputJSON(cmd, pages)
}
if len(pages) == 0 {
fmt.Fprintln(ios.Out, "No wiki pages found")
return nil
}
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 = text.FormatDate(t, isTTY)
} else {
updated = p.LastCommit.Committer.Date
}
}
tp.AddRow(p.Title, updated)
}
return tp.Render()
}
func runWikiView(cmd *cobra.Command, args []string) error {
title := args[0]
client, owner, name, err := newWikiClient(cmd)
if err != nil {
return err
}
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)
}
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.Fprintln(ios.Out)
}
return nil
}
func runWikiCreate(cmd *cobra.Command, args []string) error {
title := args[0]
client, owner, name, err := newWikiClient(cmd)
if err != nil {
return err
}
body, err := readBody(cmd)
if err != nil {
return err
}
if body == "" {
return fmt.Errorf("content is required (use --body or --body-file)")
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new",
url.PathEscape(owner), url.PathEscape(name))
reqBody := wikiCreateRequest{
Title: title,
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()
if wantJSON(cmd) {
return outputJSON(cmd, page)
}
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Wiki page created: %s\n", cs.SuccessIcon(), title)
return nil
}
func runWikiEdit(cmd *cobra.Command, args []string) error {
title := args[0]
client, owner, name, err := newWikiClient(cmd)
if err != nil {
return err
}
body, err := readBody(cmd)
if err != nil {
return err
}
if body == "" {
return fmt.Errorf("content is required (use --body or --body-file)")
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s",
url.PathEscape(owner), url.PathEscape(name), url.PathEscape(title))
reqBody := wikiCreateRequest{
Title: title,
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()
if wantJSON(cmd) {
return outputJSON(cmd, page)
}
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()
cs := ios.ColorScheme()
fmt.Fprintf(ios.Out, "%s Wiki page deleted: %s\n", cs.SuccessIcon(), title)
return nil
}