package main import ( "encoding/json" "fmt" "os" "github.com/spf13/cobra" "go.bigb.es/confluence-md-utilities/api" "go.bigb.es/confluence-md-utilities/converter" ) var ( pullOutput string pullRaw bool pullWithComments bool ) var pullCmd = &cobra.Command{ Use: "pull ", Short: "Pull a page from Confluence and convert to Markdown", Long: `Fetch a Confluence page by URL, extract its storage format body, and convert it to Markdown. Supported URL formats: https://confluence.example.com/pages/viewpage.action?pageId=12345 https://confluence.example.com/display/SPACE/Page+Title Use --raw to get the Confluence storage XML without converting to Markdown. Authentication via --token flag or CONFLUENCE_TOKEN environment variable.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { token := resolveToken() if token == "" { return fmt.Errorf("Confluence token required: use --token flag or set CONFLUENCE_TOKEN env var") } if pullWithComments && pullOutput == "" { return fmt.Errorf("--with-comments requires --output") } ref, err := api.ParsePageURL(args[0]) if err != nil { return err } client := api.NewClient(ref.BaseURL, token) page, err := client.GetPage(ref) if err != nil { return err } xmlBody := page.Body.Storage.Value fmt.Fprintf(os.Stderr, "Pulled page: %s (id=%s, version=%d)\n", page.Title, page.ID, page.Version.Number) var result string if pullRaw { result = xmlBody } else { result, err = converter.ConfluenceToMarkdown(xmlBody) if err != nil { return fmt.Errorf("converting to markdown: %w", err) } } if pullOutput != "" { if err := os.WriteFile(pullOutput, []byte(result), 0644); err != nil { return err } fmt.Fprintf(os.Stderr, "Written to %s\n", pullOutput) if pullWithComments { if err := pullSaveComments(client, page, pullOutput); err != nil { return fmt.Errorf("fetching comments: %w", err) } } return nil } fmt.Print(result) return nil }, } type commentSidecar struct { Page sidecarPage `json:"page"` InlineComments []*api.Comment `json:"inline_comments"` PageComments []*api.Comment `json:"page_comments"` } type sidecarPage struct { ID string `json:"id"` Title string `json:"title"` Version int `json:"version"` } func pullSaveComments(client *api.Client, page *api.ContentResponse, mdPath string) error { comments, err := client.GetComments(page.ID) if err != nil { return err } inline, footer := splitComments(comments) sidecar := commentSidecar{ Page: sidecarPage{ ID: page.ID, Title: page.Title, Version: page.Version.Number, }, InlineComments: inline, PageComments: footer, } data, err := json.MarshalIndent(sidecar, "", " ") if err != nil { return err } sidecarPath := mdPath + ".comments.json" if err := os.WriteFile(sidecarPath, data, 0644); err != nil { return err } fmt.Fprintf(os.Stderr, "Saved %d inline + %d page comments to %s\n", len(inline), len(footer), sidecarPath) return nil } func splitComments(all []*api.Comment) (inline, footer []*api.Comment) { for _, c := range all { if c.Location == "inline" { inline = append(inline, c) } else { footer = append(footer, c) } } return } func init() { pullCmd.Flags().StringVarP(&pullOutput, "output", "o", "", "Output file (default: stdout)") pullCmd.Flags().BoolVar(&pullRaw, "raw", false, "Output raw Confluence storage XML instead of Markdown") pullCmd.Flags().BoolVar(&pullWithComments, "with-comments", false, "Also save inline + page comments to .comments.json (requires --output)") rootCmd.AddCommand(pullCmd) }