package cmd import ( "encoding/base64" "fmt" "net/http" "net/url" "time" "forgejo.zerova.net/public/fj/internal/api" "forgejo.zerova.net/public/fj/internal/config" "forgejo.zerova.net/public/fj/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 fj wiki list # List wiki pages for a specific repo fj wiki list -R owner/repo # Output as JSON fj wiki list --json`, RunE: runWikiList, } var wikiViewCmd = &cobra.Command{ Use: "view ", Short: "View a wiki page", Long: "Display the content of a wiki page.", Example: ` # View a wiki page fj wiki view Home # Open in browser fj wiki view Home --web # View a wiki page as JSON (includes content) fj wiki view Home --json # View a wiki page from a specific repo fj 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 fj wiki create "Getting Started" -b "# Welcome\nThis is the getting started guide." # Create a wiki page from a file fj wiki create "Setup Guide" --body-file setup.md # Create a wiki page from stdin echo "# FAQ" | fj wiki create FAQ --body-file - # Output as JSON fj 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 fj wiki edit Home -b "# Updated Home\nNew content here." # Edit a wiki page from a file fj wiki edit "Setup Guide" --body-file updated-setup.md # Edit a wiki page from stdin cat new-content.md | fj wiki edit Home --body-file - # Output as JSON fj 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 fj wiki delete "Old Page" # Delete without confirmation fj wiki delete "Old Page" -y # Delete a wiki page from a specific repo fj 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 }