diff --git a/pkg/cmd/consent-record/consent_record.go b/pkg/cmd/consent-record/consent_record.go new file mode 100644 index 000000000..0ee8dde69 --- /dev/null +++ b/pkg/cmd/consent-record/consent_record.go @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package consentrecord + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/consent-record/list" + "go.probo.inc/probo/pkg/cmd/consent-record/view" +) + +func NewCmdConsentRecord(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "consent-record ", + Short: "Manage cookie consent records", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(view.NewCmdView(f)) + + return cmd +} diff --git a/pkg/cmd/consent-record/list/list.go b/pkg/cmd/consent-record/list/list.go new file mode 100644 index 000000000..1cf999b92 --- /dev/null +++ b/pkg/cmd/consent-record/list/list.go @@ -0,0 +1,173 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $filter: CookieConsentRecordFilter) { + node(id: $id) { + __typename + ... on CookieBanner { + consentRecords(first: $first, after: $after, filter: $filter) { + totalCount + edges { + node { + id + visitorId + action + sdkVersion + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type consentRecord struct { + ID string `json:"id"` + VisitorID string `json:"visitorId"` + Action string `json:"action"` + SDKVersion string `json:"sdkVersion"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagBannerID string + flagAction string + flagVisitorID string + flagVersion int + flagLimit int + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List cookie consent records for a banner", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + variables := map[string]any{"id": flagBannerID} + + filter := map[string]any{} + if cmd.Flags().Changed("action") { + filter["action"] = flagAction + } + if cmd.Flags().Changed("visitor-id") { + filter["visitorId"] = flagVisitorID + } + if cmd.Flags().Changed("version") { + filter["version"] = flagVersion + } + if len(filter) > 0 { + variables["filter"] = filter + } + + records, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[consentRecord], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + ConsentRecords api.Connection[consentRecord] `json:"consentRecords"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("cookie banner %s not found", flagBannerID) + } + return &resp.Node.ConsentRecords, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, records) + } + + if len(records) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No consent records found.") + return nil + } + + rows := make([][]string, 0, len(records)) + for _, r := range records { + rows = append(rows, []string{r.ID, r.VisitorID, r.Action, r.SDKVersion, r.CreatedAt}) + } + + t := cmdutil.NewTable("ID", "VISITOR ID", "ACTION", "SDK VERSION", "CREATED AT").Rows(rows...) + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(records) { + _, _ = fmt.Fprintf(f.IOStreams.ErrOut, "\nShowing %d of %d consent records\n", len(records), totalCount) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagBannerID, "banner-id", "", "Cookie banner ID (required)") + _ = cmd.MarkFlagRequired("banner-id") + cmd.Flags().StringVar(&flagAction, "action", "", "Filter by action") + cmd.Flags().StringVar(&flagVisitorID, "visitor-id", "", "Filter by visitor ID") + cmd.Flags().IntVar(&flagVersion, "version", 0, "Filter by version") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of items") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/consent-record/view/view.go b/pkg/cmd/consent-record/view/view.go new file mode 100644 index 000000000..b17517d75 --- /dev/null +++ b/pkg/cmd/consent-record/view/view.go @@ -0,0 +1,135 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on CookieConsentRecord { + id + visitorId + ipAddress + userAgent + consentData + action + sdkVersion + createdAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + VisitorID string `json:"visitorId"` + IPAddress *string `json:"ipAddress"` + UserAgent *string `json:"userAgent"` + ConsentData string `json:"consentData"` + Action string `json:"action"` + SdkVersion string `json:"sdkVersion"` + CreatedAt string `json:"createdAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a consent record", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(viewQuery, map[string]any{"id": args[0]}) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil || resp.Node.Typename != "CookieConsentRecord" { + return fmt.Errorf("consent record %s not found", args[0]) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Consent Record")) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Visitor ID:"), v.VisitorID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Action:"), v.Action) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("SDK Version:"), v.SdkVersion) + if v.IPAddress != nil && *v.IPAddress != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("IP Address:"), *v.IPAddress) + } + if v.UserAgent != nil && *v.UserAgent != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("User Agent:"), *v.UserAgent) + } + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Consent Data:"), v.ConsentData) + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/cookie-banner/activate/activate.go b/pkg/cmd/cookie-banner/activate/activate.go new file mode 100644 index 000000000..9949cccc4 --- /dev/null +++ b/pkg/cmd/cookie-banner/activate/activate.go @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package activate + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const activateMutation = ` +mutation($input: ActivateCookieBannerInput!) { + activateCookieBanner(input: $input) { + cookieBanner { + id + name + state + } + } +} +` + +func NewCmdActivate(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "activate ", + Short: "Activate a cookie banner", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"cookieBannerId": args[0]} + + data, err := client.Do(activateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp struct { + ActivateCookieBanner struct { + CookieBanner struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + } `json:"cookieBanner"` + } `json:"activateCookieBanner"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return err + } + + b := resp.ActivateCookieBanner.CookieBanner + _, _ = fmt.Fprintf(f.IOStreams.Out, "Activated cookie banner %s (%s)\n", b.Name, b.ID) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/cookie-banner/cookie_banner.go b/pkg/cmd/cookie-banner/cookie_banner.go new file mode 100644 index 000000000..e63f2cece --- /dev/null +++ b/pkg/cmd/cookie-banner/cookie_banner.go @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package cookiebanner + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/cookie-banner/activate" + "go.probo.inc/probo/pkg/cmd/cookie-banner/create" + "go.probo.inc/probo/pkg/cmd/cookie-banner/deactivate" + "go.probo.inc/probo/pkg/cmd/cookie-banner/delete" + "go.probo.inc/probo/pkg/cmd/cookie-banner/list" + "go.probo.inc/probo/pkg/cmd/cookie-banner/publish" + "go.probo.inc/probo/pkg/cmd/cookie-banner/translate" + "go.probo.inc/probo/pkg/cmd/cookie-banner/update" + "go.probo.inc/probo/pkg/cmd/cookie-banner/versions" + "go.probo.inc/probo/pkg/cmd/cookie-banner/view" +) + +func NewCmdCookieBanner(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "cookie-banner ", + Short: "Manage cookie banners", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(activate.NewCmdActivate(f)) + cmd.AddCommand(deactivate.NewCmdDeactivate(f)) + cmd.AddCommand(publish.NewCmdPublish(f)) + cmd.AddCommand(translate.NewCmdTranslate(f)) + cmd.AddCommand(versions.NewCmdVersions(f)) + + return cmd +} diff --git a/pkg/cmd/cookie-banner/create/create.go b/pkg/cmd/cookie-banner/create/create.go new file mode 100644 index 000000000..d51e65b26 --- /dev/null +++ b/pkg/cmd/cookie-banner/create/create.go @@ -0,0 +1,173 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateCookieBannerInput!) { + createCookieBanner(input: $input) { + cookieBannerEdge { + node { + id + name + origin + } + } + } +} +` + +type createResponse struct { + CreateCookieBanner struct { + CookieBannerEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Origin string `json:"origin"` + } `json:"node"` + } `json:"cookieBannerEdge"` + } `json:"createCookieBanner"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagOrigin string + flagCookiePolicyUrl string + flagPrivacyPolicyUrl string + flagConsentExpiry int + flagConsentMode string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new cookie banner", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + if err := huh.NewInput().Title("Banner name").Value(&flagName).Run(); err != nil { + return err + } + } + if flagOrigin == "" { + if err := huh.NewInput().Title("Website origin (e.g. https://example.com)").Value(&flagOrigin).Run(); err != nil { + return err + } + } + if flagCookiePolicyUrl == "" { + if err := huh.NewInput().Title("Cookie policy URL").Value(&flagCookiePolicyUrl).Run(); err != nil { + return err + } + } + if flagConsentMode == "" { + if err := huh.NewSelect[string](). + Title("Consent mode"). + Options( + huh.NewOption("Opt-In", "OPT_IN"), + huh.NewOption("Opt-Out", "OPT_OUT"), + ). + Value(&flagConsentMode).Run(); err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagOrigin == "" { + return fmt.Errorf("origin is required; pass --origin or run interactively") + } + if flagCookiePolicyUrl == "" { + return fmt.Errorf("cookie-policy-url is required; pass --cookie-policy-url or run interactively") + } + if flagConsentMode == "" { + return fmt.Errorf("consent-mode is required; pass --consent-mode or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + "origin": flagOrigin, + "cookiePolicyUrl": flagCookiePolicyUrl, + "consentExpiryDays": flagConsentExpiry, + "consentMode": flagConsentMode, + } + if flagPrivacyPolicyUrl != "" { + input["privacyPolicyUrl"] = flagPrivacyPolicyUrl + } + + data, err := client.Do(createMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + b := resp.CreateCookieBanner.CookieBannerEdge.Node + _, _ = fmt.Fprintf(f.IOStreams.Out, "Created cookie banner %s (%s)\n", b.ID, b.Name) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Banner name (required)") + cmd.Flags().StringVar(&flagOrigin, "origin", "", "Website origin (required)") + cmd.Flags().StringVar(&flagCookiePolicyUrl, "cookie-policy-url", "", "Cookie policy URL (required)") + cmd.Flags().StringVar(&flagPrivacyPolicyUrl, "privacy-policy-url", "", "Privacy policy URL") + cmd.Flags().IntVar(&flagConsentExpiry, "consent-expiry-days", 365, "Days until consent expires") + cmd.Flags().StringVar(&flagConsentMode, "consent-mode", "", "Consent mode: OPT_IN or OPT_OUT (required)") + + return cmd +} diff --git a/pkg/cmd/cookie-banner/deactivate/deactivate.go b/pkg/cmd/cookie-banner/deactivate/deactivate.go new file mode 100644 index 000000000..052e1eb23 --- /dev/null +++ b/pkg/cmd/cookie-banner/deactivate/deactivate.go @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package deactivate + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deactivateMutation = ` +mutation($input: DeactivateCookieBannerInput!) { + deactivateCookieBanner(input: $input) { + cookieBanner { + id + name + state + } + } +} +` + +func NewCmdDeactivate(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "deactivate ", + Short: "Deactivate a cookie banner", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"cookieBannerId": args[0]} + + data, err := client.Do(deactivateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp struct { + DeactivateCookieBanner struct { + CookieBanner struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + } `json:"cookieBanner"` + } `json:"deactivateCookieBanner"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return err + } + + b := resp.DeactivateCookieBanner.CookieBanner + _, _ = fmt.Fprintf(f.IOStreams.Out, "Deactivated cookie banner %s (%s)\n", b.Name, b.ID) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/cookie-banner/delete/delete.go b/pkg/cmd/cookie-banner/delete/delete.go new file mode 100644 index 000000000..0218d8cfd --- /dev/null +++ b/pkg/cmd/cookie-banner/delete/delete.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteCookieBannerInput!) { + deleteCookieBanner(input: $input) { + deletedCookieBannerId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a cookie banner", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete cookie banner: confirmation required, use --yes to confirm") + } + var confirmed bool + if err := huh.NewConfirm().Title(fmt.Sprintf("Delete cookie banner %s?", args[0])).Value(&confirmed).Run(); err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do(deleteMutation, map[string]any{ + "input": map[string]any{"cookieBannerId": args[0]}, + }) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Deleted cookie banner %s\n", args[0]) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/cookie-banner/list/list.go b/pkg/cmd/cookie-banner/list/list.go new file mode 100644 index 000000000..01d75a806 --- /dev/null +++ b/pkg/cmd/cookie-banner/list/list.go @@ -0,0 +1,159 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey) { + node(id: $id) { + __typename + ... on Organization { + cookieBanners(first: $first, after: $after) { + totalCount + edges { + node { + id + name + origin + state + consentMode + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type banner struct { + ID string `json:"id"` + Name string `json:"name"` + Origin string `json:"origin"` + State string `json:"state"` + ConsentMode string `json:"consentMode"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagLimit int + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List cookie banners in an organization", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + variables := map[string]any{"id": flagOrg} + + banners, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[banner], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + CookieBanners api.Connection[banner] `json:"cookieBanners"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("organization %s not found", flagOrg) + } + return &resp.Node.CookieBanners, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, banners) + } + + if len(banners) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No cookie banners found.") + return nil + } + + rows := make([][]string, 0, len(banners)) + for _, b := range banners { + rows = append(rows, []string{b.ID, b.Name, b.Origin, b.State, b.ConsentMode}) + } + + t := cmdutil.NewTable("ID", "NAME", "ORIGIN", "STATE", "CONSENT MODE").Rows(rows...) + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(banners) { + _, _ = fmt.Fprintf(f.IOStreams.ErrOut, "\nShowing %d of %d cookie banners\n", len(banners), totalCount) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of items") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/cookie-banner/publish/publish.go b/pkg/cmd/cookie-banner/publish/publish.go new file mode 100644 index 000000000..f88ce10d3 --- /dev/null +++ b/pkg/cmd/cookie-banner/publish/publish.go @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package publish + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const publishMutation = ` +mutation($input: PublishCookieBannerVersionInput!) { + publishCookieBannerVersion(input: $input) { + cookieBannerVersion { + id + version + state + } + } +} +` + +func NewCmdPublish(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "publish ", + Short: "Publish the current draft version", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"cookieBannerId": args[0]} + + data, err := client.Do(publishMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp struct { + PublishCookieBannerVersion struct { + CookieBannerVersion struct { + ID string `json:"id"` + Version int `json:"version"` + State string `json:"state"` + } `json:"cookieBannerVersion"` + } `json:"publishCookieBannerVersion"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return err + } + + v := resp.PublishCookieBannerVersion.CookieBannerVersion + _, _ = fmt.Fprintf(f.IOStreams.Out, "Published version %d for cookie banner %s\n", v.Version, args[0]) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/cookie-banner/translate/translate.go b/pkg/cmd/cookie-banner/translate/translate.go new file mode 100644 index 000000000..00cbcc3f5 --- /dev/null +++ b/pkg/cmd/cookie-banner/translate/translate.go @@ -0,0 +1,101 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package translate + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const translateMutation = ` +mutation($input: UpsertCookieBannerTranslationInput!) { + upsertCookieBannerTranslation(input: $input) { + cookieBannerTranslation { + id + language + } + } +} +` + +func NewCmdTranslate(f *cmdutil.Factory) *cobra.Command { + var ( + flagLanguage string + flagTranslations string + ) + + cmd := &cobra.Command{ + Use: "translate ", + Short: "Upsert a translation for a language", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if flagLanguage == "" { + return fmt.Errorf("--language is required") + } + if flagTranslations == "" { + return fmt.Errorf("--translations is required") + } + + var translations json.RawMessage + if err := json.Unmarshal([]byte(flagTranslations), &translations); err != nil { + return fmt.Errorf("invalid JSON for --translations: %w", err) + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "cookieBannerId": args[0], + "language": flagLanguage, + "translations": translations, + } + + _, err = client.Do(translateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Upserted translation for language %q on cookie banner %s\n", flagLanguage, args[0]) + + return nil + }, + } + + cmd.Flags().StringVar(&flagLanguage, "language", "", "Language code (e.g. fr, de, es)") + cmd.Flags().StringVar(&flagTranslations, "translations", "", "Translations JSON") + _ = cmd.MarkFlagRequired("language") + _ = cmd.MarkFlagRequired("translations") + + return cmd +} diff --git a/pkg/cmd/cookie-banner/update/update.go b/pkg/cmd/cookie-banner/update/update.go new file mode 100644 index 000000000..0d3d0212e --- /dev/null +++ b/pkg/cmd/cookie-banner/update/update.go @@ -0,0 +1,129 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateCookieBannerInput!) { + updateCookieBanner(input: $input) { + cookieBanner { + id + name + } + } +} +` + +type updateResponse struct { + UpdateCookieBanner struct { + CookieBanner struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"cookieBanner"` + } `json:"updateCookieBanner"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagCookiePolicyUrl string + flagPrivacyPolicyUrl string + flagConsentExpiry int + flagConsentMode string + flagDefaultLanguage string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a cookie banner", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"cookieBannerId": args[0]} + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("cookie-policy-url") { + input["cookiePolicyUrl"] = flagCookiePolicyUrl + } + if cmd.Flags().Changed("privacy-policy-url") { + input["privacyPolicyUrl"] = flagPrivacyPolicyUrl + } + if cmd.Flags().Changed("consent-expiry-days") { + input["consentExpiryDays"] = flagConsentExpiry + } + if cmd.Flags().Changed("consent-mode") { + input["consentMode"] = flagConsentMode + } + if cmd.Flags().Changed("default-language") { + input["defaultLanguage"] = flagDefaultLanguage + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do(updateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + b := resp.UpdateCookieBanner.CookieBanner + _, _ = fmt.Fprintf(f.IOStreams.Out, "Updated cookie banner %s (%s)\n", b.ID, b.Name) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Banner name") + cmd.Flags().StringVar(&flagCookiePolicyUrl, "cookie-policy-url", "", "Cookie policy URL") + cmd.Flags().StringVar(&flagPrivacyPolicyUrl, "privacy-policy-url", "", "Privacy policy URL") + cmd.Flags().IntVar(&flagConsentExpiry, "consent-expiry-days", 0, "Days until consent expires") + cmd.Flags().StringVar(&flagConsentMode, "consent-mode", "", "Consent mode: OPT_IN or OPT_OUT") + cmd.Flags().StringVar(&flagDefaultLanguage, "default-language", "", "Default language code") + + return cmd +} diff --git a/pkg/cmd/cookie-banner/versions/versions.go b/pkg/cmd/cookie-banner/versions/versions.go new file mode 100644 index 000000000..8dfe6e226 --- /dev/null +++ b/pkg/cmd/cookie-banner/versions/versions.go @@ -0,0 +1,120 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package versions + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const versionsQuery = ` +query($id: ID!) { + node(id: $id) { + ... on CookieBanner { + latestVersion { + id + version + state + createdAt + updatedAt + } + } + } +} +` + +type versionInfo struct { + ID string `json:"id"` + Version int `json:"version"` + State string `json:"state"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdVersions(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "versions ", + Short: "List versions for a cookie banner", + Aliases: []string{"ver"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(versionsQuery, map[string]any{"id": args[0]}) + if err != nil { + return err + } + + var resp struct { + Node *struct { + LatestVersion *versionInfo `json:"latestVersion"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return err + } + + if resp.Node == nil || resp.Node.LatestVersion == nil { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No versions found.") + return nil + } + + v := resp.Node.LatestVersion + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, v) + } + + rows := [][]string{ + {v.ID, strconv.Itoa(v.Version), v.State, cmdutil.FormatTime(v.CreatedAt)}, + } + t := cmdutil.NewTable("ID", "VERSION", "STATE", "CREATED").Rows(rows...) + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/cookie-banner/view/view.go b/pkg/cmd/cookie-banner/view/view.go new file mode 100644 index 000000000..4bf66c2c1 --- /dev/null +++ b/pkg/cmd/cookie-banner/view/view.go @@ -0,0 +1,143 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on CookieBanner { + id + name + origin + state + cookiePolicyUrl + privacyPolicyUrl + consentExpiryDays + consentMode + showBranding + defaultLanguage + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Origin string `json:"origin"` + State string `json:"state"` + CookiePolicyUrl string `json:"cookiePolicyUrl"` + PrivacyPolicyUrl *string `json:"privacyPolicyUrl"` + ConsentExpiryDays int `json:"consentExpiryDays"` + ConsentMode string `json:"consentMode"` + ShowBranding bool `json:"showBranding"` + DefaultLanguage string `json:"defaultLanguage"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a cookie banner", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(viewQuery, map[string]any{"id": args[0]}) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil || resp.Node.Typename != "CookieBanner" { + return fmt.Errorf("cookie banner %s not found", args[0]) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(v.Name)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Origin:"), v.Origin) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), v.State) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Consent Mode:"), v.ConsentMode) + _, _ = fmt.Fprintf(out, "%s%d days\n", label.Render("Consent Expiry:"), v.ConsentExpiryDays) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Default Language:"), v.DefaultLanguage) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Cookie Policy:"), v.CookiePolicyUrl) + if v.PrivacyPolicyUrl != nil && *v.PrivacyPolicyUrl != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Privacy Policy:"), *v.PrivacyPolicyUrl) + } + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/cookie-category/cookie_category.go b/pkg/cmd/cookie-category/cookie_category.go new file mode 100644 index 000000000..2b6849c91 --- /dev/null +++ b/pkg/cmd/cookie-category/cookie_category.go @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package cookiecategory + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/cookie-category/create" + "go.probo.inc/probo/pkg/cmd/cookie-category/delete" + "go.probo.inc/probo/pkg/cmd/cookie-category/list" + "go.probo.inc/probo/pkg/cmd/cookie-category/reorder" + "go.probo.inc/probo/pkg/cmd/cookie-category/update" + "go.probo.inc/probo/pkg/cmd/cookie-category/view" +) + +func NewCmdCookieCategory(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "cookie-category ", + Short: "Manage cookie categories", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(reorder.NewCmdReorder(f)) + + return cmd +} diff --git a/pkg/cmd/cookie-category/create/create.go b/pkg/cmd/cookie-category/create/create.go new file mode 100644 index 000000000..7e62edc83 --- /dev/null +++ b/pkg/cmd/cookie-category/create/create.go @@ -0,0 +1,150 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateCookieCategoryInput!) { + createCookieCategory(input: $input) { + cookieCategoryEdge { + node { + id + name + slug + } + } + cookieBanner { + id + } + } +} +` + +type createResponse struct { + CreateCookieCategory struct { + CookieCategoryEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + } `json:"node"` + } `json:"cookieCategoryEdge"` + CookieBanner struct { + ID string `json:"id"` + } `json:"cookieBanner"` + } `json:"createCookieCategory"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagBannerID string + flagName string + flagSlug string + flagDescription string + flagRank int + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new cookie category", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagName == "" { + if err := huh.NewInput().Title("Category name").Value(&flagName).Run(); err != nil { + return err + } + } + if flagSlug == "" { + if err := huh.NewInput().Title("Category slug").Value(&flagSlug).Run(); err != nil { + return err + } + } + if flagDescription == "" { + if err := huh.NewText().Title("Description").Value(&flagDescription).Run(); err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagSlug == "" { + return fmt.Errorf("slug is required; pass --slug or run interactively") + } + + input := map[string]any{ + "cookieBannerId": flagBannerID, + "name": flagName, + "slug": flagSlug, + "description": flagDescription, + "rank": flagRank, + } + + data, err := client.Do(createMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + c := resp.CreateCookieCategory.CookieCategoryEdge.Node + _, _ = fmt.Fprintf(f.IOStreams.Out, "Created cookie category %s (%s)\n", c.ID, c.Name) + + return nil + }, + } + + cmd.Flags().StringVar(&flagBannerID, "banner-id", "", "Cookie banner ID (required)") + cmd.Flags().StringVar(&flagName, "name", "", "Category name") + cmd.Flags().StringVar(&flagSlug, "slug", "", "Category slug") + cmd.Flags().StringVar(&flagDescription, "description", "", "Category description") + cmd.Flags().IntVar(&flagRank, "rank", 10, "Display rank") + + _ = cmd.MarkFlagRequired("banner-id") + + return cmd +} diff --git a/pkg/cmd/cookie-category/delete/delete.go b/pkg/cmd/cookie-category/delete/delete.go new file mode 100644 index 000000000..a6a41eab9 --- /dev/null +++ b/pkg/cmd/cookie-category/delete/delete.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteCookieCategoryInput!) { + deleteCookieCategory(input: $input) { + deletedCookieCategoryId + cookieBanner { + id + } + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a cookie category", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete cookie category: confirmation required, use --yes to confirm") + } + var confirmed bool + if err := huh.NewConfirm().Title(fmt.Sprintf("Delete cookie category %s?", args[0])).Value(&confirmed).Run(); err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do(deleteMutation, map[string]any{ + "input": map[string]any{"cookieCategoryId": args[0]}, + }) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Deleted cookie category %s\n", args[0]) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/cookie-category/list/list.go b/pkg/cmd/cookie-category/list/list.go new file mode 100644 index 000000000..712741906 --- /dev/null +++ b/pkg/cmd/cookie-category/list/list.go @@ -0,0 +1,158 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey) { + node(id: $id) { + __typename + ... on CookieBanner { + categories(first: $first, after: $after, orderBy: {field: RANK, direction: ASC}) { + totalCount + edges { + node { + id + name + slug + kind + rank + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type category struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Kind string `json:"kind"` + Rank int `json:"rank"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagBannerID string + flagLimit int + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List cookie categories for a banner", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + if flagBannerID == "" { + return fmt.Errorf("banner-id is required; pass --banner-id") + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + variables := map[string]any{"id": flagBannerID} + + categories, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[category], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Categories api.Connection[category] `json:"categories"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("cookie banner %s not found", flagBannerID) + } + return &resp.Node.Categories, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, categories) + } + + if len(categories) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No cookie categories found.") + return nil + } + + rows := make([][]string, 0, len(categories)) + for _, c := range categories { + rows = append(rows, []string{c.ID, c.Name, c.Slug, c.Kind, fmt.Sprintf("%d", c.Rank)}) + } + + t := cmdutil.NewTable("ID", "NAME", "SLUG", "KIND", "RANK").Rows(rows...) + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(categories) { + _, _ = fmt.Fprintf(f.IOStreams.ErrOut, "\nShowing %d of %d cookie categories\n", len(categories), totalCount) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagBannerID, "banner-id", "", "Cookie banner ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of items") + flagOutput = cmdutil.AddOutputFlag(cmd) + + _ = cmd.MarkFlagRequired("banner-id") + + return cmd +} diff --git a/pkg/cmd/cookie-category/reorder/reorder.go b/pkg/cmd/cookie-category/reorder/reorder.go new file mode 100644 index 000000000..bdb889e63 --- /dev/null +++ b/pkg/cmd/cookie-category/reorder/reorder.go @@ -0,0 +1,81 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package reorder + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const reorderMutation = ` +mutation($input: ReorderCookieCategoryInput!) { + reorderCookieCategory(input: $input) { + cookieBanner { + id + } + } +} +` + +func NewCmdReorder(f *cmdutil.Factory) *cobra.Command { + var flagRank int + + cmd := &cobra.Command{ + Use: "reorder ", + Short: "Change the rank of a cookie category", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do(reorderMutation, map[string]any{ + "input": map[string]any{ + "cookieCategoryId": args[0], + "rank": flagRank, + }, + }) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Reordered cookie category %s to rank %d\n", args[0], flagRank) + + return nil + }, + } + + cmd.Flags().IntVar(&flagRank, "rank", 0, "New rank position (required)") + _ = cmd.MarkFlagRequired("rank") + + return cmd +} diff --git a/pkg/cmd/cookie-category/update/update.go b/pkg/cmd/cookie-category/update/update.go new file mode 100644 index 000000000..475d2dbdd --- /dev/null +++ b/pkg/cmd/cookie-category/update/update.go @@ -0,0 +1,120 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateCookieCategoryInput!) { + updateCookieCategory(input: $input) { + cookieCategory { + id + name + } + cookieBanner { + id + } + } +} +` + +type updateResponse struct { + UpdateCookieCategory struct { + CookieCategory struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"cookieCategory"` + CookieBanner struct { + ID string `json:"id"` + } `json:"cookieBanner"` + } `json:"updateCookieCategory"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagSlug string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a cookie category", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"cookieCategoryId": args[0]} + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("slug") { + input["slug"] = flagSlug + } + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do(updateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + c := resp.UpdateCookieCategory.CookieCategory + _, _ = fmt.Fprintf(f.IOStreams.Out, "Updated cookie category %s (%s)\n", c.ID, c.Name) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Category name") + cmd.Flags().StringVar(&flagSlug, "slug", "", "Category slug") + cmd.Flags().StringVar(&flagDescription, "description", "", "Category description") + + return cmd +} diff --git a/pkg/cmd/cookie-category/view/view.go b/pkg/cmd/cookie-category/view/view.go new file mode 100644 index 000000000..2bc64bcfe --- /dev/null +++ b/pkg/cmd/cookie-category/view/view.go @@ -0,0 +1,140 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on CookieCategory { + id + name + slug + description + kind + rank + gcmConsentTypes + posthogConsent + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Kind string `json:"kind"` + Rank int `json:"rank"` + GcmConsentTypes []string `json:"gcmConsentTypes"` + PosthogConsent string `json:"posthogConsent"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a cookie category", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(viewQuery, map[string]any{"id": args[0]}) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil || resp.Node.Typename != "CookieCategory" { + return fmt.Errorf("cookie category %s not found", args[0]) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(v.Name)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Slug:"), v.Slug) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), v.Description) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Kind:"), v.Kind) + _, _ = fmt.Fprintf(out, "%s%d\n", label.Render("Rank:"), v.Rank) + if len(v.GcmConsentTypes) > 0 { + _, _ = fmt.Fprintf(out, "%s%v\n", label.Render("GCM Consent Types:"), v.GcmConsentTypes) + } + if v.PosthogConsent != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("PostHog Consent:"), v.PosthogConsent) + } + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/cookie_pattern.go b/pkg/cmd/cookie-pattern/cookie_pattern.go new file mode 100644 index 000000000..8c145ca2b --- /dev/null +++ b/pkg/cmd/cookie-pattern/cookie_pattern.go @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package cookiepattern + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/cookie-pattern/create" + "go.probo.inc/probo/pkg/cmd/cookie-pattern/delete" + "go.probo.inc/probo/pkg/cmd/cookie-pattern/list" + "go.probo.inc/probo/pkg/cmd/cookie-pattern/move" + "go.probo.inc/probo/pkg/cmd/cookie-pattern/update" + "go.probo.inc/probo/pkg/cmd/cookie-pattern/view" +) + +func NewCmdCookiePattern(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "cookie-pattern ", + Short: "Manage cookie patterns", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(move.NewCmdMove(f)) + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/create/create.go b/pkg/cmd/cookie-pattern/create/create.go new file mode 100644 index 000000000..ca744e3e5 --- /dev/null +++ b/pkg/cmd/cookie-pattern/create/create.go @@ -0,0 +1,161 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateCookiePatternInput!) { + createCookiePattern(input: $input) { + cookiePatternEdge { + node { + id + pattern + displayName + } + } + cookieBanner { + id + } + } +} +` + +type createResponse struct { + CreateCookiePattern struct { + CookiePatternEdge struct { + Node struct { + ID string `json:"id"` + Pattern string `json:"pattern"` + DisplayName string `json:"displayName"` + } `json:"node"` + } `json:"cookiePatternEdge"` + } `json:"createCookiePattern"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagCategoryID string + flagPattern string + flagMatchType string + flagDisplayName string + flagDescription string + flagMaxAge int + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new cookie pattern", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagPattern == "" { + if err := huh.NewInput().Title("Cookie pattern").Value(&flagPattern).Run(); err != nil { + return err + } + } + if flagMatchType == "" { + if err := huh.NewSelect[string](). + Title("Match type"). + Options( + huh.NewOption("Exact", "EXACT"), + huh.NewOption("Prefix", "PREFIX"), + ). + Value(&flagMatchType).Run(); err != nil { + return err + } + } + if flagDisplayName == "" { + if err := huh.NewInput().Title("Display name").Value(&flagDisplayName).Run(); err != nil { + return err + } + } + } + + if flagPattern == "" { + return fmt.Errorf("pattern is required; pass --pattern or run interactively") + } + if flagMatchType == "" { + return fmt.Errorf("match-type is required; pass --match-type or run interactively") + } + if flagDisplayName == "" { + return fmt.Errorf("display-name is required; pass --display-name or run interactively") + } + + input := map[string]any{ + "cookieCategoryId": flagCategoryID, + "pattern": flagPattern, + "matchType": flagMatchType, + "displayName": flagDisplayName, + } + if flagDescription != "" { + input["description"] = flagDescription + } + if cmd.Flags().Changed("max-age-seconds") { + input["maxAgeSeconds"] = flagMaxAge + } + + data, err := client.Do(createMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + p := resp.CreateCookiePattern.CookiePatternEdge.Node + _, _ = fmt.Fprintf(f.IOStreams.Out, "Created cookie pattern %s (%s)\n", p.ID, p.DisplayName) + + return nil + }, + } + + cmd.Flags().StringVar(&flagCategoryID, "category-id", "", "Cookie category ID (required)") + _ = cmd.MarkFlagRequired("category-id") + cmd.Flags().StringVar(&flagPattern, "pattern", "", "Cookie pattern (required)") + cmd.Flags().StringVar(&flagMatchType, "match-type", "", "Match type: EXACT or PREFIX (required)") + cmd.Flags().StringVar(&flagDisplayName, "display-name", "", "Display name (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Description") + cmd.Flags().IntVar(&flagMaxAge, "max-age-seconds", 0, "Maximum age in seconds") + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/delete/delete.go b/pkg/cmd/cookie-pattern/delete/delete.go new file mode 100644 index 000000000..e0b456e82 --- /dev/null +++ b/pkg/cmd/cookie-pattern/delete/delete.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteCookiePatternInput!) { + deleteCookiePattern(input: $input) { + deletedCookiePatternId + cookieBanner { + id + } + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a cookie pattern", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete cookie pattern: confirmation required, use --yes to confirm") + } + var confirmed bool + if err := huh.NewConfirm().Title(fmt.Sprintf("Delete cookie pattern %s?", args[0])).Value(&confirmed).Run(); err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do(deleteMutation, map[string]any{ + "input": map[string]any{"cookiePatternId": args[0]}, + }) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Deleted cookie pattern %s\n", args[0]) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/list/list.go b/pkg/cmd/cookie-pattern/list/list.go new file mode 100644 index 000000000..ade861455 --- /dev/null +++ b/pkg/cmd/cookie-pattern/list/list.go @@ -0,0 +1,153 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey) { + node(id: $id) { + __typename + ... on CookieCategory { + cookiePatterns(first: $first, after: $after) { + totalCount + edges { + node { + id + pattern + matchType + displayName + source + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type cookiePattern struct { + ID string `json:"id"` + Pattern string `json:"pattern"` + MatchType string `json:"matchType"` + DisplayName string `json:"displayName"` + Source string `json:"source"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagCategoryID string + flagLimit int + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List cookie patterns in a category", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + variables := map[string]any{"id": flagCategoryID} + + patterns, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[cookiePattern], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + CookiePatterns api.Connection[cookiePattern] `json:"cookiePatterns"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("cookie category %s not found", flagCategoryID) + } + return &resp.Node.CookiePatterns, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, patterns) + } + + if len(patterns) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No cookie patterns found.") + return nil + } + + rows := make([][]string, 0, len(patterns)) + for _, p := range patterns { + rows = append(rows, []string{p.ID, p.Pattern, p.MatchType, p.DisplayName, p.Source}) + } + + t := cmdutil.NewTable("ID", "PATTERN", "MATCH TYPE", "DISPLAY NAME", "SOURCE").Rows(rows...) + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(patterns) { + _, _ = fmt.Fprintf(f.IOStreams.ErrOut, "\nShowing %d of %d cookie patterns\n", len(patterns), totalCount) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagCategoryID, "category-id", "", "Cookie category ID (required)") + _ = cmd.MarkFlagRequired("category-id") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of items") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/move/move.go b/pkg/cmd/cookie-pattern/move/move.go new file mode 100644 index 000000000..9c19c4ad1 --- /dev/null +++ b/pkg/cmd/cookie-pattern/move/move.go @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package move + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const moveMutation = ` +mutation($input: MoveCookiePatternToCategoryInput!) { + moveCookiePatternToCategory(input: $input) { + cookiePattern { + id + cookieCategory { + id + name + } + } + cookieBanner { + id + } + } +} +` + +type moveResponse struct { + MoveCookiePatternToCategory struct { + CookiePattern struct { + ID string `json:"id"` + CookieCategory struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"cookieCategory"` + } `json:"cookiePattern"` + } `json:"moveCookiePatternToCategory"` +} + +func NewCmdMove(f *cmdutil.Factory) *cobra.Command { + var flagTargetCategoryID string + + cmd := &cobra.Command{ + Use: "move ", + Short: "Move a cookie pattern to a different category", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(moveMutation, map[string]any{ + "input": map[string]any{ + "cookiePatternId": args[0], + "targetCookieCategoryId": flagTargetCategoryID, + }, + }) + if err != nil { + return err + } + + var resp moveResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + p := resp.MoveCookiePatternToCategory.CookiePattern + _, _ = fmt.Fprintf(f.IOStreams.Out, "Moved cookie pattern %s to category %s\n", p.ID, p.CookieCategory.Name) + + return nil + }, + } + + cmd.Flags().StringVar(&flagTargetCategoryID, "target-category-id", "", "Target cookie category ID (required)") + _ = cmd.MarkFlagRequired("target-category-id") + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/update/update.go b/pkg/cmd/cookie-pattern/update/update.go new file mode 100644 index 000000000..9d27315c1 --- /dev/null +++ b/pkg/cmd/cookie-pattern/update/update.go @@ -0,0 +1,117 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateCookiePatternInput!) { + updateCookiePattern(input: $input) { + cookiePattern { + id + displayName + } + cookieBanner { + id + } + } +} +` + +type updateResponse struct { + UpdateCookiePattern struct { + CookiePattern struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"cookiePattern"` + } `json:"updateCookiePattern"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagDisplayName string + flagDescription string + flagMaxAge int + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a cookie pattern", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{"cookiePatternId": args[0]} + + if cmd.Flags().Changed("display-name") { + input["displayName"] = flagDisplayName + } + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + if cmd.Flags().Changed("max-age-seconds") { + input["maxAgeSeconds"] = flagMaxAge + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do(updateMutation, map[string]any{"input": input}) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + p := resp.UpdateCookiePattern.CookiePattern + _, _ = fmt.Fprintf(f.IOStreams.Out, "Updated cookie pattern %s (%s)\n", p.ID, p.DisplayName) + + return nil + }, + } + + cmd.Flags().StringVar(&flagDisplayName, "display-name", "", "Display name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Description") + cmd.Flags().IntVar(&flagMaxAge, "max-age-seconds", 0, "Maximum age in seconds") + + return cmd +} diff --git a/pkg/cmd/cookie-pattern/view/view.go b/pkg/cmd/cookie-pattern/view/view.go new file mode 100644 index 000000000..8c7920ad1 --- /dev/null +++ b/pkg/cmd/cookie-pattern/view/view.go @@ -0,0 +1,137 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on CookiePattern { + id + pattern + matchType + displayName + maxAgeSeconds + description + source + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Pattern string `json:"pattern"` + MatchType string `json:"matchType"` + DisplayName string `json:"displayName"` + MaxAgeSeconds *int `json:"maxAgeSeconds"` + Description *string `json:"description"` + Source string `json:"source"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a cookie pattern", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do(viewQuery, map[string]any{"id": args[0]}) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil || resp.Node.Typename != "CookiePattern" { + return fmt.Errorf("cookie pattern %s not found", args[0]) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(v.DisplayName)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Pattern:"), v.Pattern) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Match Type:"), v.MatchType) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Source:"), v.Source) + if v.MaxAgeSeconds != nil { + _, _ = fmt.Fprintf(out, "%s%d\n", label.Render("Max Age (seconds):"), *v.MaxAgeSeconds) + } + if v.Description != nil && *v.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *v.Description) + } + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 3cc7f0e35..d9cacd140 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -26,8 +26,12 @@ import ( "go.probo.inc/probo/pkg/cmd/cmdutil" "go.probo.inc/probo/pkg/cmd/completion" cmdconfig "go.probo.inc/probo/pkg/cmd/config" + consentrecord "go.probo.inc/probo/pkg/cmd/consent-record" cmdcontext "go.probo.inc/probo/pkg/cmd/context" "go.probo.inc/probo/pkg/cmd/control" + cookiebanner "go.probo.inc/probo/pkg/cmd/cookie-banner" + cookiecategory "go.probo.inc/probo/pkg/cmd/cookie-category" + cookiepattern "go.probo.inc/probo/pkg/cmd/cookie-pattern" "go.probo.inc/probo/pkg/cmd/datum" "go.probo.inc/probo/pkg/cmd/document" "go.probo.inc/probo/pkg/cmd/dpia" @@ -90,8 +94,12 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(browse.NewCmdBrowse(f)) cmd.AddCommand(completion.NewCmdCompletion(f)) cmd.AddCommand(cmdconfig.NewCmdConfig(f)) + cmd.AddCommand(consentrecord.NewCmdConsentRecord(f)) cmd.AddCommand(cmdcontext.NewCmdContext(f)) cmd.AddCommand(control.NewCmdControl(f)) + cmd.AddCommand(cookiebanner.NewCmdCookieBanner(f)) + cmd.AddCommand(cookiecategory.NewCmdCookieCategory(f)) + cmd.AddCommand(cookiepattern.NewCmdCookiePattern(f)) cmd.AddCommand(datum.NewCmdDatum(f)) cmd.AddCommand(document.NewCmdDocument(f)) cmd.AddCommand(dpia.NewCmdDPIA(f))