Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ fopost knowledge list · add · sync · delete · search
fopost analytics overview · top-posts · time-series
fopost automations list · get · toggle · trigger · runs
fopost webhooks list · create · test · delete
fopost ads tree · pause · resume · insights · leads
fopost ads tree · pause · resume · insights · leads · catalogs · library
fopost completion bash · zsh · fish · powershell
fopost version
```
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/fopost/fopost-cli
go 1.22

require (
github.com/fopost/fopost-go v0.2.1-0.20260920141700-633d72b705ca
github.com/fopost/fopost-go v0.2.1-0.20260920143205-156d1c2d360c
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.27.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ github.com/fopost/fopost-go v0.2.1-0.20260920130053-a8ac95df638d h1:XSxGXq2PUdF+
github.com/fopost/fopost-go v0.2.1-0.20260920130053-a8ac95df638d/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/fopost/fopost-go v0.2.1-0.20260920141700-633d72b705ca h1:1a7XwNMmGCUh22hW6FOs8R5F5xJVTsbgCoJK+VdgEMc=
github.com/fopost/fopost-go v0.2.1-0.20260920141700-633d72b705ca/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/fopost/fopost-go v0.2.1-0.20260920143205-156d1c2d360c h1:Md2W/Uu8pXDMqJkZ0gRuSb8iNz3/H4zn5FS++Yt+Lf8=
github.com/fopost/fopost-go v0.2.1-0.20260920143205-156d1c2d360c/go.mod h1:Qz+7UCYTBnDEeZqB3VMrePxmYLVR9rFvx3+QWcKND8Q=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/ads.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ func init() { register(newAdsCmd) }
func newAdsCmd(state *State) *cobra.Command {
cmd := &cobra.Command{
Use: "ads",
Short: "Inspect Meta campaigns, change their status, and read insights and leads",
Short: "Inspect Meta campaigns and catalogs, change status, and read insights, leads and the ad archive",
}
cmd.AddCommand(
newAdsTreeCmd(state),
newAdsStatusCmd(state, "pause", fopost.AdStatusPaused),
newAdsStatusCmd(state, "resume", fopost.AdStatusActive),
newAdsInsightsCmd(state),
newAdsLeadsCmd(state),
newAdsCatalogsCmd(state),
newAdsLibraryCmd(state),
)
return cmd
}
Expand Down
157 changes: 157 additions & 0 deletions internal/cmd/ads_extras.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package cmd

import (
"strconv"
"strings"

fopost "github.com/fopost/fopost-go"
"github.com/spf13/cobra"

"github.com/fopost/fopost-cli/internal/output"
)

func count(n *int) string {
if n == nil {
return "-"
}
return strconv.Itoa(*n)
}

// newAdsCatalogsCmd lists the product catalogs a connection reaches, and the
// product sets inside one. A catalog ad runs from a set, not the whole catalog.
func newAdsCatalogsCmd(state *State) *cobra.Command {
var connection, catalog string
cmd := &cobra.Command{
Use: "catalogs",
Short: "List product catalogs, or the product sets in one",
Example: strings.Join([]string{
" fopost ads catalogs --connection conn_1",
" fopost ads catalogs --connection conn_1 --catalog 500123456789",
}, "\n"),
RunE: func(cmd *cobra.Command, args []string) error {
if connection == "" {
return usageErrorf("--connection is required")
}
client, err := state.Client()
if err != nil {
return err
}
resolved, err := state.Resolved()
if err != nil {
return err
}
params := &fopost.AdObjectParams{
WorkspaceID: resolved.Workspace,
ConnectionID: connection,
}
printer := state.Printer()

if catalog != "" {
sets, err := client.Ads.ProductSets(cmd.Context(), catalog, params)
if err != nil {
return err
}
return printer.Value(sets, func() {
rows := [][]string{}
for _, set := range sets {
rows = append(rows, []string{
set.ID,
output.Truncate(set.Name, 40),
count(set.ProductCount),
})
}
printer.Table([]string{"id", "name", "products"}, rows)
})
}

result, err := client.Ads.Catalogs(cmd.Context(), params)
if err != nil {
return err
}
return printer.Value(result.Catalogs, func() {
rows := [][]string{}
for _, c := range result.Catalogs {
rows = append(rows, []string{
c.ID,
output.Truncate(c.Name, 40),
c.Vertical,
count(c.ProductCount),
})
}
printer.Table([]string{"id", "name", "vertical", "products"}, rows)
})
},
}
cmd.Flags().StringVar(&connection, "connection", "", "Meta Ads connection id (required)")
cmd.Flags().StringVar(&catalog, "catalog", "", "Show the product sets in this catalog instead")
return cmd
}

// newAdsLibraryCmd searches the public ad archive. Results are read live on
// every call and stored nowhere, so an ad that stops running is simply absent
// from the next search.
func newAdsLibraryCmd(state *State) *cobra.Command {
var connection, country, page string
var limit int
cmd := &cobra.Command{
Use: "library [keyword]",
Short: "Search the public ad archive for ads anyone is running",
Args: cobra.MaximumNArgs(1),
Example: strings.Join([]string{
" fopost ads library --connection conn_1 --country US \"running shoes\"",
" fopost ads library --connection conn_1 --country GB --page 1234567890",
}, "\n"),
RunE: func(cmd *cobra.Command, args []string) error {
if connection == "" {
return usageErrorf("--connection is required")
}
keyword := ""
if len(args) == 1 {
keyword = args[0]
}
if keyword == "" && page == "" {
return usageErrorf("search by keyword or by --page")
}
client, err := state.Client()
if err != nil {
return err
}
resolved, err := state.Resolved()
if err != nil {
return err
}
params := &fopost.LibraryParams{
WorkspaceID: resolved.Workspace,
ConnectionID: connection,
Countries: []string{country},
Q: keyword,
Limit: limit,
}
if page != "" {
params.PageIDs = []string{page}
}
result, err := client.Ads.Library(cmd.Context(), params)
if err != nil {
return err
}
printer := state.Printer()
return printer.Value(result.Entries, func() {
rows := [][]string{}
for _, entry := range result.Entries {
rows = append(rows, []string{
entry.ID,
output.Truncate(entry.PageName, 28),
output.Truncate(strings.Join(entry.Bodies, " · "), 48),
strings.Join(entry.PublisherPlatforms, ","),
})
}
printer.Table([]string{"id", "page", "copy", "platforms"}, rows)
})
},
}
cmd.Flags().StringVar(&connection, "connection", "", "Meta Ads connection id (required)")
cmd.Flags().StringVar(&country, "country", "US", "Two-letter code the ad reached")
cmd.Flags().StringVar(&page, "page", "", "Search one Page instead of a keyword")
cmd.Flags().IntVar(&limit, "limit", 25, "How many entries to return, 1-100")
return cmd
}
Loading