Add missing resources to CLI, MCP, and n8n surfaces

Audit all three API surfaces against the console GraphQL schema and add
missing resources: asset, audit, datum, dpia, evidence upload, measure,
obligation, processing activity, rights request, snapshot, task, tia,
trust center (with references/files), and vendor management CLI
commands; MCP tools for deletes, rights requests, trust center, vendor
contacts/services, and compliance external URLs; n8n nodes for
obligation, finding, task, evidence, processing activity, dpia, tia,
rights request, snapshot, audit log, access review, organization
context, trust center, and additional control/measure/vendor operations.

Include MCP e2e test infrastructure (testutil MCP client with API key
auth and JSON-RPC session management) and tests covering all new MCP
tools.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-21 16:08:37 +02:00
parent f505e23cb0
commit 7be92defcc
220 changed files with 26395 additions and 7 deletions

View File

@@ -16,7 +16,12 @@ package asset
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/asset/create"
"go.probo.inc/probo/pkg/cmd/asset/delete"
"go.probo.inc/probo/pkg/cmd/asset/list"
"go.probo.inc/probo/pkg/cmd/asset/publish"
"go.probo.inc/probo/pkg/cmd/asset/update"
"go.probo.inc/probo/pkg/cmd/asset/view"
"go.probo.inc/probo/pkg/cmd/cmdutil"
)
@@ -26,6 +31,11 @@ func NewCmdAsset(f *cmdutil.Factory) *cobra.Command {
Short: "Manage assets",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
cmd.AddCommand(publish.NewCmdPublish(f))
return cmd

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateAssetInput!) {
createAsset(input: $input) {
assetEdge {
node {
id
name
assetType
amount
}
}
}
}
`
type createResponse struct {
CreateAsset struct {
AssetEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
AssetType string `json:"assetType"`
Amount int `json:"amount"`
} `json:"node"`
} `json:"assetEdge"`
} `json:"createAsset"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagAssetType string
flagAmount int
flagOwner string
flagDataTypesStored string
flagVendorIDs []string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new asset",
Example: ` # Create an asset interactively
prb asset create
# Create an asset non-interactively
prb asset create --name "Production Database" --asset-type VIRTUAL --amount 50000`,
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 == "" {
err := huh.NewInput().
Title("Asset name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagAssetType == "" {
err := huh.NewSelect[string]().
Title("Asset type").
Options(
huh.NewOption("Physical", "PHYSICAL"),
huh.NewOption("Virtual", "VIRTUAL"),
).
Value(&flagAssetType).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
if flagAssetType == "" {
return fmt.Errorf("asset type is required; pass --asset-type or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
"assetType": flagAssetType,
}
if cmd.Flags().Changed("amount") {
input["amount"] = flagAmount
}
if flagOwner != "" {
input["ownerId"] = flagOwner
}
if flagDataTypesStored != "" {
input["dataTypesStored"] = flagDataTypesStored
}
if len(flagVendorIDs) > 0 {
input["vendorIds"] = flagVendorIDs
}
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)
}
a := resp.CreateAsset.AssetEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created asset %s (%s)\n",
a.ID,
a.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Asset name (required)")
cmd.Flags().StringVar(&flagAssetType, "asset-type", "", "Asset type: PHYSICAL, VIRTUAL (required)")
cmd.Flags().IntVar(&flagAmount, "amount", 0, "Asset amount")
cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID")
cmd.Flags().StringVar(&flagDataTypesStored, "data-types-stored", "", "Data types stored")
cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteAssetInput!) {
deleteAsset(input: $input) {
deletedAssetId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete an asset",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete asset: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete asset %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"assetId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted asset %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

193
pkg/cmd/asset/list/list.go Normal file
View File

@@ -0,0 +1,193 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: AssetOrder, $filter: AssetFilter) {
node(id: $id) {
__typename
... on Organization {
assets(first: $first, after: $after, orderBy: $orderBy, filter: $filter) {
totalCount
edges {
node {
id
name
assetType
amount
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type asset struct {
ID string `json:"id"`
Name string `json:"name"`
AssetType string `json:"assetType"`
Amount int `json:"amount"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List assets in an organization",
Aliases: []string{"ls"},
Example: ` # List assets in the default organization
prb asset list
# List assets sorted by amount
prb asset ls --order-by AMOUNT --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "AMOUNT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
assets, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[asset], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Assets api.Connection[asset] `json:"assets"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Assets, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, assets)
}
if len(assets) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No assets found.")
return nil
}
rows := make([][]string, 0, len(assets))
for _, a := range assets {
rows = append(rows, []string{
a.ID,
a.Name,
a.AssetType,
fmt.Sprintf("%d", a.Amount),
})
}
t := cmdutil.NewTable("ID", "NAME", "TYPE", "AMOUNT").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(assets) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d assets\n",
len(assets),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of assets to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, AMOUNT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateAssetInput!) {
updateAsset(input: $input) {
asset {
id
name
assetType
amount
}
}
}
`
type updateResponse struct {
UpdateAsset struct {
Asset struct {
ID string `json:"id"`
Name string `json:"name"`
AssetType string `json:"assetType"`
Amount int `json:"amount"`
} `json:"asset"`
} `json:"updateAsset"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagAssetType string
flagAmount int
flagOwner string
flagDataTypesStored string
flagVendorIDs []string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update an asset",
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{
"id": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("asset-type") {
input["assetType"] = flagAssetType
}
if cmd.Flags().Changed("amount") {
input["amount"] = flagAmount
}
if cmd.Flags().Changed("owner") {
if flagOwner == "" {
input["ownerId"] = nil
} else {
input["ownerId"] = flagOwner
}
}
if cmd.Flags().Changed("data-types-stored") {
input["dataTypesStored"] = flagDataTypesStored
}
if cmd.Flags().Changed("vendor-ids") {
input["vendorIds"] = flagVendorIDs
}
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)
}
a := resp.UpdateAsset.Asset
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated asset %s (%s)\n",
a.ID,
a.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Asset name")
cmd.Flags().StringVar(&flagAssetType, "asset-type", "", "Asset type: PHYSICAL, VIRTUAL")
cmd.Flags().IntVar(&flagAmount, "amount", 0, "Asset amount")
cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID")
cmd.Flags().StringVar(&flagDataTypesStored, "data-types-stored", "", "Data types stored")
cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)")
return cmd
}

139
pkg/cmd/asset/view/view.go Normal file
View File

@@ -0,0 +1,139 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Asset {
id
name
assetType
amount
dataTypesStored
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
AssetType string `json:"assetType"`
Amount int `json:"amount"`
DataTypesStored string `json:"dataTypesStored"`
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 <id>",
Short: "View an asset",
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 {
return fmt.Errorf("asset %s not found", args[0])
}
if resp.Node.Typename != "Asset" {
return fmt.Errorf("expected Asset node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
a := 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(a.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), a.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), a.AssetType)
_, _ = fmt.Fprintf(out, "%s%d\n", label.Render("Amount:"), a.Amount)
if a.DataTypesStored != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Data Types Stored:"), a.DataTypesStored)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(a.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(a.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

40
pkg/cmd/audit/audit.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 audit
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/audit/create"
"go.probo.inc/probo/pkg/cmd/audit/delete"
"go.probo.inc/probo/pkg/cmd/audit/list"
"go.probo.inc/probo/pkg/cmd/audit/update"
"go.probo.inc/probo/pkg/cmd/audit/view"
"go.probo.inc/probo/pkg/cmd/cmdutil"
)
func NewCmdAudit(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "audit <command>",
Short: "Manage audits",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,191 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateAuditInput!) {
createAudit(input: $input) {
auditEdge {
node {
id
name
state
validFrom
validUntil
}
}
}
}
`
type createResponse struct {
CreateAudit struct {
AuditEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
ValidFrom *string `json:"validFrom"`
ValidUntil *string `json:"validUntil"`
} `json:"node"`
} `json:"auditEdge"`
} `json:"createAudit"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagFramework string
flagName string
flagState string
flagValidFrom string
flagValidUntil string
flagTrustCenterVisibility string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new audit",
Example: ` # Create an audit interactively
prb audit create
# Create an audit non-interactively
prb audit create --name "SOC 2 Type II 2026" --state IN_PROGRESS --valid-from 2026-01-01 --valid-until 2026-12-31`,
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 == "" {
err := huh.NewInput().
Title("Audit name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagState == "" {
err := huh.NewSelect[string]().
Title("Audit state").
Options(
huh.NewOption("Not Started", "NOT_STARTED"),
huh.NewOption("In Progress", "IN_PROGRESS"),
huh.NewOption("Completed", "COMPLETED"),
huh.NewOption("Rejected", "REJECTED"),
huh.NewOption("Outdated", "OUTDATED"),
).
Value(&flagState).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
}
if flagFramework != "" {
input["frameworkId"] = flagFramework
}
if flagState != "" {
input["state"] = flagState
}
if flagValidFrom != "" {
input["validFrom"] = flagValidFrom
}
if flagValidUntil != "" {
input["validUntil"] = flagValidUntil
}
if flagTrustCenterVisibility != "" {
input["trustCenterVisibility"] = flagTrustCenterVisibility
}
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)
}
a := resp.CreateAudit.AuditEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created audit %s (%s)\n",
a.ID,
a.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagFramework, "framework", "", "Framework ID")
cmd.Flags().StringVar(&flagName, "name", "", "Audit name (required)")
cmd.Flags().StringVar(&flagState, "state", "", "Audit state: NOT_STARTED, IN_PROGRESS, COMPLETED, REJECTED, OUTDATED")
cmd.Flags().StringVar(&flagValidFrom, "valid-from", "", "Valid from date (e.g. 2026-01-01)")
cmd.Flags().StringVar(&flagValidUntil, "valid-until", "", "Valid until date (e.g. 2026-12-31)")
cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteAuditInput!) {
deleteAudit(input: $input) {
deletedAuditId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete an audit",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete audit: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete audit %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"auditId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted audit %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

204
pkg/cmd/audit/list/list.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: AuditOrder) {
node(id: $id) {
__typename
... on Organization {
audits(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
state
validFrom
validUntil
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type audit struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
ValidFrom *string `json:"validFrom"`
ValidUntil *string `json:"validUntil"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List audits in an organization",
Aliases: []string{"ls"},
Example: ` # List audits in the default organization
prb audit list
# List audits sorted by state
prb audit ls --order-by STATE --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "VALID_FROM", "VALID_UNTIL", "STATE"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
audits, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[audit], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Audits api.Connection[audit] `json:"audits"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Audits, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, audits)
}
if len(audits) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No audits found.")
return nil
}
rows := make([][]string, 0, len(audits))
for _, a := range audits {
validFrom := ""
if a.ValidFrom != nil {
validFrom = *a.ValidFrom
}
validUntil := ""
if a.ValidUntil != nil {
validUntil = *a.ValidUntil
}
rows = append(rows, []string{
a.ID,
a.Name,
a.State,
validFrom,
validUntil,
})
}
t := cmdutil.NewTable("ID", "NAME", "STATE", "VALID FROM", "VALID UNTIL").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(audits) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d audits\n",
len(audits),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of audits to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, VALID_FROM, VALID_UNTIL, STATE)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateAuditInput!) {
updateAudit(input: $input) {
audit {
id
name
state
validFrom
validUntil
}
}
}
`
type updateResponse struct {
UpdateAudit struct {
Audit struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
ValidFrom *string `json:"validFrom"`
ValidUntil *string `json:"validUntil"`
} `json:"audit"`
} `json:"updateAudit"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagState string
flagValidFrom string
flagValidUntil string
flagTrustCenterVisibility string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update an audit",
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{
"id": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("state") {
input["state"] = flagState
}
if cmd.Flags().Changed("valid-from") {
input["validFrom"] = flagValidFrom
}
if cmd.Flags().Changed("valid-until") {
input["validUntil"] = flagValidUntil
}
if cmd.Flags().Changed("trust-center-visibility") {
input["trustCenterVisibility"] = flagTrustCenterVisibility
}
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)
}
a := resp.UpdateAudit.Audit
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated audit %s (%s)\n",
a.ID,
a.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Audit name")
cmd.Flags().StringVar(&flagState, "state", "", "Audit state: NOT_STARTED, IN_PROGRESS, COMPLETED, REJECTED, OUTDATED")
cmd.Flags().StringVar(&flagValidFrom, "valid-from", "", "Valid from date (e.g. 2026-01-01)")
cmd.Flags().StringVar(&flagValidUntil, "valid-until", "", "Valid until date (e.g. 2026-12-31)")
cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC")
return cmd
}

142
pkg/cmd/audit/view/view.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Audit {
id
name
state
validFrom
validUntil
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
ValidFrom *string `json:"validFrom"`
ValidUntil *string `json:"validUntil"`
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 <id>",
Short: "View an audit",
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 {
return fmt.Errorf("audit %s not found", args[0])
}
if resp.Node.Typename != "Audit" {
return fmt.Errorf("expected Audit node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
a := 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(a.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), a.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), a.State)
if a.ValidFrom != nil && *a.ValidFrom != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Valid From:"), *a.ValidFrom)
}
if a.ValidUntil != nil && *a.ValidUntil != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Valid Until:"), *a.ValidUntil)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(a.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(a.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateDatumInput!) {
createDatum(input: $input) {
datumEdge {
node {
id
name
dataClassification
}
}
}
}
`
type createResponse struct {
CreateDatum struct {
DatumEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
DataClassification string `json:"dataClassification"`
} `json:"node"`
} `json:"datumEdge"`
} `json:"createDatum"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagClassification string
flagOwner string
flagVendorIDs []string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new datum",
Example: ` # Create a datum interactively
prb datum create
# Create a datum non-interactively
prb datum create --name "Customer PII" --data-classification CONFIDENTIAL`,
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 == "" {
err := huh.NewInput().
Title("Datum name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagClassification == "" {
err := huh.NewSelect[string]().
Title("Data classification").
Options(
huh.NewOption("Public", "PUBLIC"),
huh.NewOption("Internal", "INTERNAL"),
huh.NewOption("Confidential", "CONFIDENTIAL"),
huh.NewOption("Secret", "SECRET"),
).
Value(&flagClassification).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
if flagClassification == "" {
return fmt.Errorf("data classification is required; pass --data-classification or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
"dataClassification": flagClassification,
}
if flagOwner != "" {
input["ownerId"] = flagOwner
}
if len(flagVendorIDs) > 0 {
input["vendorIds"] = flagVendorIDs
}
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)
}
d := resp.CreateDatum.DatumEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created datum %s (%s)\n",
d.ID,
d.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Datum name (required)")
cmd.Flags().StringVar(&flagClassification, "data-classification", "", "Data classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET (required)")
cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID")
cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)")
return cmd
}

View File

@@ -17,7 +17,12 @@ package datum
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/datum/create"
"go.probo.inc/probo/pkg/cmd/datum/delete"
"go.probo.inc/probo/pkg/cmd/datum/list"
"go.probo.inc/probo/pkg/cmd/datum/publish"
"go.probo.inc/probo/pkg/cmd/datum/update"
"go.probo.inc/probo/pkg/cmd/datum/view"
)
func NewCmdDatum(f *cmdutil.Factory) *cobra.Command {
@@ -26,6 +31,11 @@ func NewCmdDatum(f *cmdutil.Factory) *cobra.Command {
Short: "Manage data",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
cmd.AddCommand(publish.NewCmdPublish(f))
return cmd

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteDatumInput!) {
deleteDatum(input: $input) {
deletedDatumId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a datum",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete datum: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete datum %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"datumId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted datum %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

190
pkg/cmd/datum/list/list.go Normal file
View File

@@ -0,0 +1,190 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: DatumOrder) {
node(id: $id) {
__typename
... on Organization {
data(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
dataClassification
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type datum struct {
ID string `json:"id"`
Name string `json:"name"`
DataClassification string `json:"dataClassification"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List data in an organization",
Aliases: []string{"ls"},
Example: ` # List data in the default organization
prb datum list
# List data sorted by name
prb datum ls --order-by NAME --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME", "DATA_CLASSIFICATION"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
data, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(raw json.RawMessage) (*api.Connection[datum], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Data api.Connection[datum] `json:"data"`
} `json:"node"`
}
if err := json.Unmarshal(raw, &resp); err != nil {
return nil, err
}
if resp.Node == nil {
return nil, fmt.Errorf("organization %s not found", flagOrg)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Data, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, data)
}
if len(data) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No data found.")
return nil
}
rows := make([][]string, 0, len(data))
for _, d := range data {
rows = append(rows, []string{
d.ID,
d.Name,
d.DataClassification,
})
}
t := cmdutil.NewTable("ID", "NAME", "CLASSIFICATION").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(data) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d data\n",
len(data),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of data to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME, DATA_CLASSIFICATION)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateDatumInput!) {
updateDatum(input: $input) {
datum {
id
name
dataClassification
}
}
}
`
type updateResponse struct {
UpdateDatum struct {
Datum struct {
ID string `json:"id"`
Name string `json:"name"`
DataClassification string `json:"dataClassification"`
} `json:"datum"`
} `json:"updateDatum"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagClassification string
flagOwner string
flagVendorIDs []string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a datum",
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{
"id": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("data-classification") {
input["dataClassification"] = flagClassification
}
if cmd.Flags().Changed("owner") {
if flagOwner == "" {
input["ownerId"] = nil
} else {
input["ownerId"] = flagOwner
}
}
if cmd.Flags().Changed("vendor-ids") {
input["vendorIds"] = flagVendorIDs
}
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)
}
d := resp.UpdateDatum.Datum
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated datum %s (%s)\n",
d.ID,
d.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Datum name")
cmd.Flags().StringVar(&flagClassification, "data-classification", "", "Data classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET")
cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID")
cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)")
return cmd
}

130
pkg/cmd/datum/view/view.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Datum {
id
name
dataClassification
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
DataClassification string `json:"dataClassification"`
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 <id>",
Short: "View a datum",
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 {
return fmt.Errorf("datum %s not found", args[0])
}
if resp.Node.Typename != "Datum" {
return fmt.Errorf("expected Datum node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
d := 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(d.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), d.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Classification:"), d.DataClassification)
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(d.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(d.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateDataProtectionImpactAssessmentInput!) {
createDataProtectionImpactAssessment(input: $input) {
dataProtectionImpactAssessmentEdge {
node {
id
description
residualRisk
}
}
}
}
`
type createResponse struct {
CreateDataProtectionImpactAssessment struct {
DataProtectionImpactAssessmentEdge struct {
Node struct {
ID string `json:"id"`
Description string `json:"description"`
ResidualRisk string `json:"residualRisk"`
} `json:"node"`
} `json:"dataProtectionImpactAssessmentEdge"`
} `json:"createDataProtectionImpactAssessment"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagProcessingActivity string
flagDescription string
flagNecessityAndProportionality string
flagPotentialRisk string
flagMitigations string
flagResidualRisk string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new data protection impact assessment",
Example: ` # Create a DPIA
prb dpia create --processing-activity <id> --description "Assessment for HR processing"
# Create a DPIA with all fields
prb dpia create --processing-activity <id> --description "Assessment" --necessity "Required by law" --potential-risk "Data leak" --mitigations "Encryption" --residual-risk LOW`,
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 flagProcessingActivity == "" {
return fmt.Errorf("processing activity is required; pass --processing-activity")
}
if f.IOStreams.IsInteractive() && flagResidualRisk == "" {
err := huh.NewSelect[string]().
Title("Residual risk").
Options(
huh.NewOption("Low", "LOW"),
huh.NewOption("Medium", "MEDIUM"),
huh.NewOption("High", "HIGH"),
).
Value(&flagResidualRisk).
Run()
if err != nil {
return err
}
}
input := map[string]any{
"processingActivityId": flagProcessingActivity,
}
if flagDescription != "" {
input["description"] = flagDescription
}
if flagNecessityAndProportionality != "" {
input["necessityAndProportionality"] = flagNecessityAndProportionality
}
if flagPotentialRisk != "" {
input["potentialRisk"] = flagPotentialRisk
}
if flagMitigations != "" {
input["mitigations"] = flagMitigations
}
if flagResidualRisk != "" {
input["residualRisk"] = flagResidualRisk
}
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)
}
r := resp.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessmentEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created data protection impact assessment %s\n",
r.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagProcessingActivity, "processing-activity", "", "Processing activity ID (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Description")
cmd.Flags().StringVar(&flagNecessityAndProportionality, "necessity", "", "Necessity and proportionality")
cmd.Flags().StringVar(&flagPotentialRisk, "potential-risk", "", "Potential risk")
cmd.Flags().StringVar(&flagMitigations, "mitigations", "", "Mitigations")
cmd.Flags().StringVar(&flagResidualRisk, "residual-risk", "", "Residual risk: LOW, MEDIUM, HIGH")
_ = cmd.MarkFlagRequired("processing-activity")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteDataProtectionImpactAssessmentInput!) {
deleteDataProtectionImpactAssessment(input: $input) {
deletedDataProtectionImpactAssessmentId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a data protection impact assessment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete data protection impact assessment: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete data protection impact assessment %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"dataProtectionImpactAssessmentId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted data protection impact assessment %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

40
pkg/cmd/dpia/dpia.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 dpia
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/dpia/create"
"go.probo.inc/probo/pkg/cmd/dpia/delete"
"go.probo.inc/probo/pkg/cmd/dpia/list"
"go.probo.inc/probo/pkg/cmd/dpia/update"
"go.probo.inc/probo/pkg/cmd/dpia/view"
)
func NewCmdDPIA(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "dpia <command>",
Short: "Manage data protection impact assessments",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

195
pkg/cmd/dpia/list/list.go Normal file
View File

@@ -0,0 +1,195 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: DataProtectionImpactAssessmentOrder) {
node(id: $id) {
__typename
... on Organization {
dataProtectionImpactAssessments(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
description
residualRisk
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type dataProtectionImpactAssessment struct {
ID string `json:"id"`
Description string `json:"description"`
ResidualRisk string `json:"residualRisk"`
CreatedAt string `json:"createdAt"`
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max-3] + "..."
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List data protection impact assessments 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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
dpias, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[dataProtectionImpactAssessment], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
DataProtectionImpactAssessments api.Connection[dataProtectionImpactAssessment] `json:"dataProtectionImpactAssessments"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.DataProtectionImpactAssessments, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, dpias)
}
if len(dpias) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No data protection impact assessments found.")
return nil
}
rows := make([][]string, 0, len(dpias))
for _, d := range dpias {
rows = append(rows, []string{
d.ID,
truncate(d.Description, 50),
d.ResidualRisk,
cmdutil.FormatTime(d.CreatedAt),
})
}
t := cmdutil.NewTable("ID", "DESCRIPTION", "RESIDUAL RISK", "CREATED AT").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(dpias) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d data protection impact assessments\n",
len(dpias),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of data protection impact assessments to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateDataProtectionImpactAssessmentInput!) {
updateDataProtectionImpactAssessment(input: $input) {
dataProtectionImpactAssessment {
id
description
residualRisk
}
}
}
`
type updateResponse struct {
UpdateDataProtectionImpactAssessment struct {
DataProtectionImpactAssessment struct {
ID string `json:"id"`
Description string `json:"description"`
ResidualRisk string `json:"residualRisk"`
} `json:"dataProtectionImpactAssessment"`
} `json:"updateDataProtectionImpactAssessment"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagDescription string
flagNecessityAndProportionality string
flagPotentialRisk string
flagMitigations string
flagResidualRisk string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a data protection impact assessment",
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{
"id": args[0],
}
if cmd.Flags().Changed("description") {
input["description"] = flagDescription
}
if cmd.Flags().Changed("necessity") {
input["necessityAndProportionality"] = flagNecessityAndProportionality
}
if cmd.Flags().Changed("potential-risk") {
input["potentialRisk"] = flagPotentialRisk
}
if cmd.Flags().Changed("mitigations") {
input["mitigations"] = flagMitigations
}
if cmd.Flags().Changed("residual-risk") {
input["residualRisk"] = flagResidualRisk
}
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)
}
r := resp.UpdateDataProtectionImpactAssessment.DataProtectionImpactAssessment
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated data protection impact assessment %s\n",
r.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagDescription, "description", "", "Description")
cmd.Flags().StringVar(&flagNecessityAndProportionality, "necessity", "", "Necessity and proportionality")
cmd.Flags().StringVar(&flagPotentialRisk, "potential-risk", "", "Potential risk")
cmd.Flags().StringVar(&flagMitigations, "mitigations", "", "Mitigations")
cmd.Flags().StringVar(&flagResidualRisk, "residual-risk", "", "Residual risk: LOW, MEDIUM, HIGH")
return cmd
}

155
pkg/cmd/dpia/view/view.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 DataProtectionImpactAssessment {
id
description
necessityAndProportionality
potentialRisk
mitigations
residualRisk
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Description string `json:"description"`
NecessityAndProportionality string `json:"necessityAndProportionality"`
PotentialRisk string `json:"potentialRisk"`
Mitigations string `json:"mitigations"`
ResidualRisk string `json:"residualRisk"`
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 <id>",
Short: "View a data protection impact assessment",
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 {
return fmt.Errorf("data protection impact assessment %s not found", args[0])
}
if resp.Node.Typename != "DataProtectionImpactAssessment" {
return fmt.Errorf("expected DataProtectionImpactAssessment node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
r := resp.Node
out := f.IOStreams.Out
bold := lipgloss.NewStyle().Bold(true)
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(30)
_, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Data Protection Impact Assessment"))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID)
if r.Description != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), r.Description)
}
if r.NecessityAndProportionality != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Necessity & Proportionality:"), r.NecessityAndProportionality)
}
if r.PotentialRisk != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Potential Risk:"), r.PotentialRisk)
}
if r.Mitigations != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Mitigations:"), r.Mitigations)
}
if r.ResidualRisk != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Residual Risk:"), r.ResidualRisk)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -19,6 +19,7 @@ import (
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/evidence/delete"
"go.probo.inc/probo/pkg/cmd/evidence/list"
"go.probo.inc/probo/pkg/cmd/evidence/upload"
"go.probo.inc/probo/pkg/cmd/evidence/view"
)
@@ -31,6 +32,7 @@ func NewCmdEvidence(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(delete.NewCmdDelete(f))
cmd.AddCommand(upload.NewCmdUpload(f))
return cmd
}

View File

@@ -0,0 +1,119 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 upload
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cli/api"
"go.probo.inc/probo/pkg/cmd/cmdutil"
)
const uploadMutation = `
mutation($input: UploadMeasureEvidenceInput!) {
uploadMeasureEvidence(input: $input) {
evidence {
id
state
type
}
}
}
`
type uploadResponse struct {
UploadMeasureEvidence struct {
Evidence struct {
ID string `json:"id"`
State string `json:"state"`
Type string `json:"type"`
} `json:"evidence"`
} `json:"uploadMeasureEvidence"`
}
func NewCmdUpload(f *cmdutil.Factory) *cobra.Command {
var flagMeasure string
cmd := &cobra.Command{
Use: "upload <file>",
Short: "Upload evidence for a measure",
Example: ` # Upload a file as evidence for a measure
prb evidence upload ./report.pdf --measure <measure-id>`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
filePath := args[0]
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("cannot open file: %w", err)
}
defer func() { _ = file.Close() }()
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{
"input": map[string]any{
"measureId": flagMeasure,
"file": nil,
},
}
data, err := client.DoUpload(
uploadMutation,
variables,
"variables.input.file",
filepath.Base(filePath),
file,
)
if err != nil {
return err
}
var resp uploadResponse
if err := json.Unmarshal(data, &resp); err != nil {
return fmt.Errorf("cannot parse response: %w", err)
}
_, _ = fmt.Fprintf(f.IOStreams.Out, "Uploaded evidence %s\n", resp.UploadMeasureEvidence.Evidence.ID)
return nil
},
}
cmd.Flags().StringVar(&flagMeasure, "measure", "", "Measure ID (required)")
_ = cmd.MarkFlagRequired("measure")
return cmd
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge {
node {
id
name
category
state
}
}
}
}
`
type createResponse struct {
CreateMeasure struct {
MeasureEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
State string `json:"state"`
} `json:"node"`
} `json:"measureEdge"`
} `json:"createMeasure"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagCategory string
flagDescription string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new measure",
Example: ` # Create a measure interactively
prb measure create
# Create a measure non-interactively
prb measure create --name "Enable encryption at rest" --category "Security"`,
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 == "" {
err := huh.NewInput().
Title("Measure name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagCategory == "" {
err := huh.NewInput().
Title("Measure category").
Value(&flagCategory).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
if flagCategory == "" {
return fmt.Errorf("category is required; pass --category or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
"category": flagCategory,
}
if flagDescription != "" {
input["description"] = flagDescription
}
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)
}
m := resp.CreateMeasure.MeasureEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created measure %s (%s)\n",
m.ID,
m.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Measure name (required)")
cmd.Flags().StringVar(&flagCategory, "category", "", "Measure category (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Measure description")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a measure",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete measure: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete measure %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"measureId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted measure %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,204 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: MeasureOrder, $filter: MeasureFilter) {
node(id: $id) {
__typename
... on Organization {
measures(first: $first, after: $after, orderBy: $orderBy, filter: $filter) {
totalCount
edges {
node {
id
name
category
state
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type measure struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
State string `json:"state"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagFilter string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List measures in an organization",
Aliases: []string{"ls"},
Example: ` # List measures in the default organization
prb measure list
# Filter measures by name
prb measure list --filter "encryption"
# List measures sorted by name
prb measure ls --order-by NAME --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
if flagFilter != "" {
variables["filter"] = map[string]any{
"query": flagFilter,
}
}
measures, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[measure], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Measures api.Connection[measure] `json:"measures"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Measures, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, measures)
}
if len(measures) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No measures found.")
return nil
}
rows := make([][]string, 0, len(measures))
for _, m := range measures {
rows = append(rows, []string{
m.ID,
m.Name,
m.Category,
m.State,
})
}
t := cmdutil.NewTable("ID", "NAME", "CATEGORY", "STATE").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(measures) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d measures\n",
len(measures),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of measures to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
cmd.Flags().StringVarP(&flagFilter, "filter", "q", "", "Filter measures by search query")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 measure
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/measure/create"
"go.probo.inc/probo/pkg/cmd/measure/delete"
"go.probo.inc/probo/pkg/cmd/measure/list"
"go.probo.inc/probo/pkg/cmd/measure/update"
"go.probo.inc/probo/pkg/cmd/measure/view"
)
func NewCmdMeasure(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "measure <command>",
Short: "Manage measures",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure {
id
name
category
state
}
}
}
`
type updateResponse struct {
UpdateMeasure struct {
Measure struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
State string `json:"state"`
} `json:"measure"`
} `json:"updateMeasure"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagDescription string
flagCategory string
flagState string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a measure",
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{
"id": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("description") {
input["description"] = flagDescription
}
if cmd.Flags().Changed("category") {
input["category"] = flagCategory
}
if cmd.Flags().Changed("state") {
if err := cmdutil.ValidateEnum("state", flagState, []string{"NOT_STARTED", "IN_PROGRESS", "NOT_APPLICABLE", "IMPLEMENTED"}); err != nil {
return err
}
input["state"] = flagState
}
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)
}
m := resp.UpdateMeasure.Measure
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated measure %s (%s)\n",
m.ID,
m.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Measure name")
cmd.Flags().StringVar(&flagDescription, "description", "", "Measure description")
cmd.Flags().StringVar(&flagCategory, "category", "", "Measure category")
cmd.Flags().StringVar(&flagState, "state", "", "Measure state: NOT_STARTED, IN_PROGRESS, NOT_APPLICABLE, IMPLEMENTED")
return cmd
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Measure {
id
name
description
category
state
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Category string `json:"category"`
State string `json:"state"`
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 <id>",
Short: "View a measure",
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 {
return fmt.Errorf("measure %s not found", args[0])
}
if resp.Node.Typename != "Measure" {
return fmt.Errorf("expected Measure node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
m := 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(m.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), m.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Category:"), m.Category)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), m.State)
if m.Description != nil && *m.Description != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *m.Description)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(m.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(m.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,235 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateObligationInput!) {
createObligation(input: $input) {
obligationEdge {
node {
id
area
source
status
type
}
}
}
}
`
type createResponse struct {
CreateObligation struct {
ObligationEdge struct {
Node struct {
ID string `json:"id"`
Area string `json:"area"`
Source string `json:"source"`
Status string `json:"status"`
Type string `json:"type"`
} `json:"node"`
} `json:"obligationEdge"`
} `json:"createObligation"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagArea string
flagSource string
flagStatus string
flagType string
flagRequirement string
flagActionsToBeImplemented string
flagRegulator string
flagOwner string
flagLastReviewDate string
flagDueDate string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new obligation",
Example: ` # Create an obligation interactively
prb obligation create
# Create an obligation non-interactively
prb obligation create --area "Data Protection" --source "GDPR Article 5" --status NON_COMPLIANT --type LEGAL`,
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 flagArea == "" {
err := huh.NewInput().
Title("Obligation area").
Value(&flagArea).
Run()
if err != nil {
return err
}
}
if flagSource == "" {
err := huh.NewInput().
Title("Obligation source").
Value(&flagSource).
Run()
if err != nil {
return err
}
}
if flagStatus == "" {
err := huh.NewSelect[string]().
Title("Obligation status").
Options(
huh.NewOption("Non-Compliant", "NON_COMPLIANT"),
huh.NewOption("Partially Compliant", "PARTIALLY_COMPLIANT"),
huh.NewOption("Compliant", "COMPLIANT"),
).
Value(&flagStatus).
Run()
if err != nil {
return err
}
}
if flagType == "" {
err := huh.NewSelect[string]().
Title("Obligation type").
Options(
huh.NewOption("Legal", "LEGAL"),
huh.NewOption("Contractual", "CONTRACTUAL"),
).
Value(&flagType).
Run()
if err != nil {
return err
}
}
}
if flagArea == "" {
return fmt.Errorf("area is required; pass --area or run interactively")
}
if flagSource == "" {
return fmt.Errorf("source is required; pass --source or run interactively")
}
if flagStatus == "" {
return fmt.Errorf("status is required; pass --status or run interactively")
}
if flagType == "" {
return fmt.Errorf("type is required; pass --type or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"area": flagArea,
"source": flagSource,
"status": flagStatus,
"type": flagType,
}
if flagRequirement != "" {
input["requirement"] = flagRequirement
}
if flagActionsToBeImplemented != "" {
input["actionsToBeImplemented"] = flagActionsToBeImplemented
}
if flagRegulator != "" {
input["regulator"] = flagRegulator
}
if flagOwner != "" {
input["ownerId"] = flagOwner
}
if flagLastReviewDate != "" {
input["lastReviewDate"] = flagLastReviewDate
}
if flagDueDate != "" {
input["dueDate"] = flagDueDate
}
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)
}
o := resp.CreateObligation.ObligationEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created obligation %s\n",
o.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagArea, "area", "", "Obligation area (required)")
cmd.Flags().StringVar(&flagSource, "source", "", "Obligation source (required)")
cmd.Flags().StringVar(&flagStatus, "status", "", "Obligation status: NON_COMPLIANT, PARTIALLY_COMPLIANT, COMPLIANT (required)")
cmd.Flags().StringVar(&flagType, "type", "", "Obligation type: LEGAL, CONTRACTUAL (required)")
cmd.Flags().StringVar(&flagRequirement, "requirement", "", "Obligation requirement")
cmd.Flags().StringVar(&flagActionsToBeImplemented, "actions-to-be-implemented", "", "Actions to be implemented")
cmd.Flags().StringVar(&flagRegulator, "regulator", "", "Regulator")
cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID")
cmd.Flags().StringVar(&flagLastReviewDate, "last-review-date", "", "Last review date (ISO 8601)")
cmd.Flags().StringVar(&flagDueDate, "due-date", "", "Due date (ISO 8601)")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteObligationInput!) {
deleteObligation(input: $input) {
deletedObligationId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete an obligation",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete obligation: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete obligation %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"obligationId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted obligation %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,203 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: ObligationOrder, $filter: ObligationFilter) {
node(id: $id) {
__typename
... on Organization {
obligations(first: $first, after: $after, orderBy: $orderBy, filter: $filter) {
totalCount
edges {
node {
id
area
source
status
type
dueDate
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type obligation struct {
ID string `json:"id"`
Area string `json:"area"`
Source string `json:"source"`
Status string `json:"status"`
Type string `json:"type"`
DueDate *string `json:"dueDate"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List obligations in an organization",
Aliases: []string{"ls"},
Example: ` # List obligations in the default organization
prb obligation list
# List obligations sorted by due date
prb obligation ls --order-by DUE_DATE --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "LAST_REVIEW_DATE", "DUE_DATE", "STATUS"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
obligations, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[obligation], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Obligations api.Connection[obligation] `json:"obligations"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Obligations, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, obligations)
}
if len(obligations) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No obligations found.")
return nil
}
rows := make([][]string, 0, len(obligations))
for _, o := range obligations {
dueDate := ""
if o.DueDate != nil {
dueDate = *o.DueDate
}
rows = append(rows, []string{
o.ID,
o.Area,
o.Source,
o.Status,
o.Type,
dueDate,
})
}
t := cmdutil.NewTable("ID", "AREA", "SOURCE", "STATUS", "TYPE", "DUE DATE").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(obligations) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d obligations\n",
len(obligations),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of obligations to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, LAST_REVIEW_DATE, DUE_DATE, STATUS)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 obligation
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/obligation/create"
"go.probo.inc/probo/pkg/cmd/obligation/delete"
"go.probo.inc/probo/pkg/cmd/obligation/list"
"go.probo.inc/probo/pkg/cmd/obligation/update"
"go.probo.inc/probo/pkg/cmd/obligation/view"
)
func NewCmdObligation(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "obligation <command>",
Short: "Manage obligations",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateObligationInput!) {
updateObligation(input: $input) {
obligation {
id
area
source
status
type
}
}
}
`
type updateResponse struct {
UpdateObligation struct {
Obligation struct {
ID string `json:"id"`
Area string `json:"area"`
Source string `json:"source"`
Status string `json:"status"`
Type string `json:"type"`
} `json:"obligation"`
} `json:"updateObligation"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagArea string
flagSource string
flagStatus string
flagType string
flagRequirement string
flagActionsToBeImplemented string
flagRegulator string
flagOwner string
flagLastReviewDate string
flagDueDate string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update an obligation",
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{
"id": args[0],
}
if cmd.Flags().Changed("area") {
input["area"] = flagArea
}
if cmd.Flags().Changed("source") {
input["source"] = flagSource
}
if cmd.Flags().Changed("status") {
input["status"] = flagStatus
}
if cmd.Flags().Changed("type") {
input["type"] = flagType
}
if cmd.Flags().Changed("requirement") {
input["requirement"] = flagRequirement
}
if cmd.Flags().Changed("actions-to-be-implemented") {
input["actionsToBeImplemented"] = flagActionsToBeImplemented
}
if cmd.Flags().Changed("regulator") {
input["regulator"] = flagRegulator
}
if cmd.Flags().Changed("owner") {
if flagOwner == "" {
input["ownerId"] = nil
} else {
input["ownerId"] = flagOwner
}
}
if cmd.Flags().Changed("last-review-date") {
input["lastReviewDate"] = flagLastReviewDate
}
if cmd.Flags().Changed("due-date") {
input["dueDate"] = flagDueDate
}
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)
}
o := resp.UpdateObligation.Obligation
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated obligation %s\n",
o.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagArea, "area", "", "Obligation area")
cmd.Flags().StringVar(&flagSource, "source", "", "Obligation source")
cmd.Flags().StringVar(&flagStatus, "status", "", "Obligation status: NON_COMPLIANT, PARTIALLY_COMPLIANT, COMPLIANT")
cmd.Flags().StringVar(&flagType, "type", "", "Obligation type: LEGAL, CONTRACTUAL")
cmd.Flags().StringVar(&flagRequirement, "requirement", "", "Obligation requirement")
cmd.Flags().StringVar(&flagActionsToBeImplemented, "actions-to-be-implemented", "", "Actions to be implemented")
cmd.Flags().StringVar(&flagRegulator, "regulator", "", "Regulator")
cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID")
cmd.Flags().StringVar(&flagLastReviewDate, "last-review-date", "", "Last review date (ISO 8601)")
cmd.Flags().StringVar(&flagDueDate, "due-date", "", "Due date (ISO 8601)")
return cmd
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Obligation {
id
area
source
requirement
actionsToBeImplemented
regulator
lastReviewDate
dueDate
status
type
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Area string `json:"area"`
Source string `json:"source"`
Requirement *string `json:"requirement"`
ActionsToBeImplemented *string `json:"actionsToBeImplemented"`
Regulator *string `json:"regulator"`
LastReviewDate *string `json:"lastReviewDate"`
DueDate *string `json:"dueDate"`
Status string `json:"status"`
Type string `json:"type"`
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 <id>",
Short: "View an obligation",
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 {
return fmt.Errorf("obligation %s not found", args[0])
}
if resp.Node.Typename != "Obligation" {
return fmt.Errorf("expected Obligation node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
o := 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(o.Area))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), o.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Source:"), o.Source)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Status:"), o.Status)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), o.Type)
if o.Requirement != nil && *o.Requirement != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Requirement:"), *o.Requirement)
}
if o.ActionsToBeImplemented != nil && *o.ActionsToBeImplemented != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Actions:"), *o.ActionsToBeImplemented)
}
if o.Regulator != nil && *o.Regulator != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Regulator:"), *o.Regulator)
}
_, _ = fmt.Fprintln(out)
if o.LastReviewDate != nil && *o.LastReviewDate != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Last Review Date:"), *o.LastReviewDate)
}
if o.DueDate != nil && *o.DueDate != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Due Date:"), *o.DueDate)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(o.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(o.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,204 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateProcessingActivityInput!) {
createProcessingActivity(input: $input) {
processingActivityEdge {
node {
id
name
role
lawfulBasis
}
}
}
}
`
type createResponse struct {
CreateProcessingActivity struct {
ProcessingActivityEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
LawfulBasis string `json:"lawfulBasis"`
} `json:"node"`
} `json:"processingActivityEdge"`
} `json:"createProcessingActivity"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagPurpose string
flagRole string
flagLawfulBasis string
flagDataSubjectCategory string
flagPersonalDataCategory string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new processing activity",
Example: ` # Create a processing activity interactively
prb processing-activity create
# Create a processing activity non-interactively
prb pa create --name "Customer onboarding" --role CONTROLLER --lawful-basis CONSENT`,
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 == "" {
err := huh.NewInput().
Title("Processing activity name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagRole == "" {
err := huh.NewSelect[string]().
Title("Role").
Options(
huh.NewOption("Controller", "CONTROLLER"),
huh.NewOption("Processor", "PROCESSOR"),
).
Value(&flagRole).
Run()
if err != nil {
return err
}
}
if flagLawfulBasis == "" {
err := huh.NewSelect[string]().
Title("Lawful basis").
Options(
huh.NewOption("Legitimate interest", "LEGITIMATE_INTEREST"),
huh.NewOption("Consent", "CONSENT"),
huh.NewOption("Contractual necessity", "CONTRACTUAL_NECESSITY"),
huh.NewOption("Legal obligation", "LEGAL_OBLIGATION"),
huh.NewOption("Vital interests", "VITAL_INTERESTS"),
huh.NewOption("Public task", "PUBLIC_TASK"),
).
Value(&flagLawfulBasis).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
}
if flagPurpose != "" {
input["purpose"] = flagPurpose
}
if flagRole != "" {
input["role"] = flagRole
}
if flagLawfulBasis != "" {
input["lawfulBasis"] = flagLawfulBasis
}
if flagDataSubjectCategory != "" {
input["dataSubjectCategory"] = flagDataSubjectCategory
}
if flagPersonalDataCategory != "" {
input["personalDataCategory"] = flagPersonalDataCategory
}
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)
}
a := resp.CreateProcessingActivity.ProcessingActivityEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created processing activity %s (%s)\n",
a.ID,
a.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Processing activity name (required)")
cmd.Flags().StringVar(&flagPurpose, "purpose", "", "Purpose of processing")
cmd.Flags().StringVar(&flagRole, "role", "", "Role: CONTROLLER, PROCESSOR")
cmd.Flags().StringVar(&flagLawfulBasis, "lawful-basis", "", "Lawful basis: LEGITIMATE_INTEREST, CONSENT, CONTRACTUAL_NECESSITY, LEGAL_OBLIGATION, VITAL_INTERESTS, PUBLIC_TASK")
cmd.Flags().StringVar(&flagDataSubjectCategory, "data-subject-category", "", "Data subject category")
cmd.Flags().StringVar(&flagPersonalDataCategory, "personal-data-category", "", "Personal data category")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteProcessingActivityInput!) {
deleteProcessingActivity(input: $input) {
deletedProcessingActivityId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a processing activity",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete processing activity: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete processing activity %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"processingActivityId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted processing activity %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,193 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: ProcessingActivityOrder) {
node(id: $id) {
__typename
... on Organization {
processingActivities(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
role
lawfulBasis
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type processingActivity struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
LawfulBasis string `json:"lawfulBasis"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List processing activities in an organization",
Aliases: []string{"ls"},
Example: ` # List processing activities in the default organization
prb processing-activity list
# List processing activities sorted by name
prb pa ls --order-by NAME --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
activities, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[processingActivity], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
ProcessingActivities api.Connection[processingActivity] `json:"processingActivities"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.ProcessingActivities, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, activities)
}
if len(activities) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No processing activities found.")
return nil
}
rows := make([][]string, 0, len(activities))
for _, a := range activities {
rows = append(rows, []string{
a.ID,
a.Name,
a.Role,
a.LawfulBasis,
})
}
t := cmdutil.NewTable("ID", "NAME", "ROLE", "LAWFUL BASIS").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(activities) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d processing activities\n",
len(activities),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of processing activities to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 processingactivity
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/processing-activity/create"
"go.probo.inc/probo/pkg/cmd/processing-activity/delete"
"go.probo.inc/probo/pkg/cmd/processing-activity/list"
"go.probo.inc/probo/pkg/cmd/processing-activity/update"
"go.probo.inc/probo/pkg/cmd/processing-activity/view"
)
func NewCmdProcessingActivity(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "processing-activity <command>",
Short: "Manage processing activities",
Aliases: []string{"pa"},
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateProcessingActivityInput!) {
updateProcessingActivity(input: $input) {
processingActivity {
id
name
role
lawfulBasis
}
}
}
`
type updateResponse struct {
UpdateProcessingActivity struct {
ProcessingActivity struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
LawfulBasis string `json:"lawfulBasis"`
} `json:"processingActivity"`
} `json:"updateProcessingActivity"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagPurpose string
flagRole string
flagLawfulBasis string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a processing activity",
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{
"id": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("purpose") {
input["purpose"] = flagPurpose
}
if cmd.Flags().Changed("role") {
input["role"] = flagRole
}
if cmd.Flags().Changed("lawful-basis") {
input["lawfulBasis"] = flagLawfulBasis
}
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)
}
a := resp.UpdateProcessingActivity.ProcessingActivity
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated processing activity %s (%s)\n",
a.ID,
a.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Processing activity name")
cmd.Flags().StringVar(&flagPurpose, "purpose", "", "Purpose of processing")
cmd.Flags().StringVar(&flagRole, "role", "", "Role: CONTROLLER, PROCESSOR")
cmd.Flags().StringVar(&flagLawfulBasis, "lawful-basis", "", "Lawful basis: LEGITIMATE_INTEREST, CONSENT, CONTRACTUAL_NECESSITY, LEGAL_OBLIGATION, VITAL_INTERESTS, PUBLIC_TASK")
return cmd
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 ProcessingActivity {
id
name
purpose
role
lawfulBasis
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
Purpose *string `json:"purpose"`
Role string `json:"role"`
LawfulBasis string `json:"lawfulBasis"`
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 <id>",
Short: "View a processing activity",
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 {
return fmt.Errorf("processing activity %s not found", args[0])
}
if resp.Node.Typename != "ProcessingActivity" {
return fmt.Errorf("expected ProcessingActivity node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
a := 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(a.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), a.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Role:"), a.Role)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Lawful Basis:"), a.LawfulBasis)
if a.Purpose != nil && *a.Purpose != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Purpose:"), *a.Purpose)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(a.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(a.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,208 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateRightsRequestInput!) {
createRightsRequest(input: $input) {
rightsRequestEdge {
node {
id
requestType
requestState
dataSubject
}
}
}
}
`
type createResponse struct {
CreateRightsRequest struct {
RightsRequestEdge struct {
Node struct {
ID string `json:"id"`
RequestType string `json:"requestType"`
RequestState string `json:"requestState"`
DataSubject string `json:"dataSubject"`
} `json:"node"`
} `json:"rightsRequestEdge"`
} `json:"createRightsRequest"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagDataSubject string
flagType string
flagState string
flagContact string
flagDetails string
flagDeadline string
flagActionTaken string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new rights request",
Example: ` # Create a rights request interactively
prb rights-request create
# Create a rights request non-interactively
prb rights-request create --data-subject "John Doe" --type ACCESS --state TODO`,
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 flagDataSubject == "" {
err := huh.NewInput().
Title("Data subject").
Value(&flagDataSubject).
Run()
if err != nil {
return err
}
}
if flagType == "" {
err := huh.NewSelect[string]().
Title("Request type").
Options(
huh.NewOption("Access", "ACCESS"),
huh.NewOption("Deletion", "DELETION"),
huh.NewOption("Portability", "PORTABILITY"),
).
Value(&flagType).
Run()
if err != nil {
return err
}
}
if flagState == "" {
err := huh.NewSelect[string]().
Title("Request state").
Options(
huh.NewOption("To Do", "TODO"),
huh.NewOption("In Progress", "IN_PROGRESS"),
huh.NewOption("Done", "DONE"),
).
Value(&flagState).
Run()
if err != nil {
return err
}
}
}
if flagDataSubject == "" {
return fmt.Errorf("data subject is required; pass --data-subject or run interactively")
}
if flagType == "" {
return fmt.Errorf("request type is required; pass --type or run interactively")
}
if flagState == "" {
return fmt.Errorf("request state is required; pass --state or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"requestType": flagType,
"requestState": flagState,
"dataSubject": flagDataSubject,
}
if flagContact != "" {
input["contact"] = flagContact
}
if flagDetails != "" {
input["details"] = flagDetails
}
if flagDeadline != "" {
input["deadline"] = flagDeadline
}
if flagActionTaken != "" {
input["actionTaken"] = flagActionTaken
}
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)
}
r := resp.CreateRightsRequest.RightsRequestEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created rights request %s\n",
r.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagDataSubject, "data-subject", "", "Data subject name (required)")
cmd.Flags().StringVar(&flagType, "type", "", "Request type: ACCESS, DELETION, PORTABILITY (required)")
cmd.Flags().StringVar(&flagState, "state", "", "Request state: TODO, IN_PROGRESS, DONE (required)")
cmd.Flags().StringVar(&flagContact, "contact", "", "Contact information")
cmd.Flags().StringVar(&flagDetails, "details", "", "Request details")
cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline")
cmd.Flags().StringVar(&flagActionTaken, "action-taken", "", "Action taken")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteRightsRequestInput!) {
deleteRightsRequest(input: $input) {
deletedRightsRequestId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a rights request",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete rights request: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete rights request %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"rightsRequestId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted rights request %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,200 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: RightsRequestOrder) {
node(id: $id) {
__typename
... on Organization {
rightsRequests(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
dataSubject
requestType
requestState
deadline
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type rightsRequest struct {
ID string `json:"id"`
DataSubject string `json:"dataSubject"`
RequestType string `json:"requestType"`
RequestState string `json:"requestState"`
Deadline *string `json:"deadline"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List rights requests in an organization",
Aliases: []string{"ls"},
Example: ` # List rights requests in the default organization
prb rights-request list
# List rights requests sorted by deadline
prb rights-request ls --order-by DEADLINE --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "DEADLINE", "STATE", "TYPE"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
rightsRequests, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[rightsRequest], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
RightsRequests api.Connection[rightsRequest] `json:"rightsRequests"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.RightsRequests, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, rightsRequests)
}
if len(rightsRequests) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No rights requests found.")
return nil
}
rows := make([][]string, 0, len(rightsRequests))
for _, r := range rightsRequests {
deadline := ""
if r.Deadline != nil {
deadline = *r.Deadline
}
rows = append(rows, []string{
r.ID,
r.DataSubject,
r.RequestType,
r.RequestState,
deadline,
})
}
t := cmdutil.NewTable("ID", "DATA SUBJECT", "TYPE", "STATE", "DEADLINE").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(rightsRequests) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d rights requests\n",
len(rightsRequests),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of rights requests to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, DEADLINE, STATE, TYPE)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 rightsrequest
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/rights-request/create"
"go.probo.inc/probo/pkg/cmd/rights-request/delete"
"go.probo.inc/probo/pkg/cmd/rights-request/list"
"go.probo.inc/probo/pkg/cmd/rights-request/update"
"go.probo.inc/probo/pkg/cmd/rights-request/view"
)
func NewCmdRightsRequest(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "rights-request <command>",
Short: "Manage rights requests",
Aliases: []string{"rr"},
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateRightsRequestInput!) {
updateRightsRequest(input: $input) {
rightsRequest {
id
requestType
requestState
dataSubject
}
}
}
`
type updateResponse struct {
UpdateRightsRequest struct {
RightsRequest struct {
ID string `json:"id"`
RequestType string `json:"requestType"`
RequestState string `json:"requestState"`
DataSubject string `json:"dataSubject"`
} `json:"rightsRequest"`
} `json:"updateRightsRequest"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagType string
flagState string
flagDataSubject string
flagContact string
flagDetails string
flagDeadline string
flagActionTaken string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a rights request",
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{
"id": args[0],
}
if cmd.Flags().Changed("type") {
input["requestType"] = flagType
}
if cmd.Flags().Changed("state") {
input["requestState"] = flagState
}
if cmd.Flags().Changed("data-subject") {
input["dataSubject"] = flagDataSubject
}
if cmd.Flags().Changed("contact") {
input["contact"] = flagContact
}
if cmd.Flags().Changed("details") {
input["details"] = flagDetails
}
if cmd.Flags().Changed("deadline") {
input["deadline"] = flagDeadline
}
if cmd.Flags().Changed("action-taken") {
input["actionTaken"] = flagActionTaken
}
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)
}
r := resp.UpdateRightsRequest.RightsRequest
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated rights request %s\n",
r.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagType, "type", "", "Request type: ACCESS, DELETION, PORTABILITY")
cmd.Flags().StringVar(&flagState, "state", "", "Request state: TODO, IN_PROGRESS, DONE")
cmd.Flags().StringVar(&flagDataSubject, "data-subject", "", "Data subject name")
cmd.Flags().StringVar(&flagContact, "contact", "", "Contact information")
cmd.Flags().StringVar(&flagDetails, "details", "", "Request details")
cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline")
cmd.Flags().StringVar(&flagActionTaken, "action-taken", "", "Action taken")
return cmd
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 RightsRequest {
id
requestType
requestState
dataSubject
contact
details
deadline
actionTaken
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
RequestType string `json:"requestType"`
RequestState string `json:"requestState"`
DataSubject string `json:"dataSubject"`
Contact *string `json:"contact"`
Details *string `json:"details"`
Deadline *string `json:"deadline"`
ActionTaken *string `json:"actionTaken"`
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 <id>",
Short: "View a rights request",
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 {
return fmt.Errorf("rights request %s not found", args[0])
}
if resp.Node.Typename != "RightsRequest" {
return fmt.Errorf("expected RightsRequest node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
r := 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(r.DataSubject))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), r.RequestType)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), r.RequestState)
if r.Contact != nil && *r.Contact != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Contact:"), *r.Contact)
}
if r.Details != nil && *r.Details != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Details:"), *r.Details)
}
if r.Deadline != nil && *r.Deadline != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Deadline:"), *r.Deadline)
}
if r.ActionTaken != nil && *r.ActionTaken != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Action Taken:"), *r.ActionTaken)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -19,6 +19,7 @@ import (
accessreview "go.probo.inc/probo/pkg/cmd/access-review"
cmdapi "go.probo.inc/probo/pkg/cmd/api"
"go.probo.inc/probo/pkg/cmd/asset"
"go.probo.inc/probo/pkg/cmd/audit"
"go.probo.inc/probo/pkg/cmd/auditlog"
"go.probo.inc/probo/pkg/cmd/auth"
"go.probo.inc/probo/pkg/cmd/browse"
@@ -29,13 +30,23 @@ import (
"go.probo.inc/probo/pkg/cmd/control"
"go.probo.inc/probo/pkg/cmd/datum"
"go.probo.inc/probo/pkg/cmd/document"
"go.probo.inc/probo/pkg/cmd/dpia"
"go.probo.inc/probo/pkg/cmd/evidence"
"go.probo.inc/probo/pkg/cmd/finding"
"go.probo.inc/probo/pkg/cmd/framework"
"go.probo.inc/probo/pkg/cmd/measure"
"go.probo.inc/probo/pkg/cmd/obligation"
"go.probo.inc/probo/pkg/cmd/org"
processingactivity "go.probo.inc/probo/pkg/cmd/processing-activity"
rightsrequest "go.probo.inc/probo/pkg/cmd/rights-request"
"go.probo.inc/probo/pkg/cmd/risk"
"go.probo.inc/probo/pkg/cmd/snapshot"
"go.probo.inc/probo/pkg/cmd/soa"
"go.probo.inc/probo/pkg/cmd/task"
"go.probo.inc/probo/pkg/cmd/tia"
trustcenter "go.probo.inc/probo/pkg/cmd/trust-center"
"go.probo.inc/probo/pkg/cmd/user"
"go.probo.inc/probo/pkg/cmd/vendormgmt"
"go.probo.inc/probo/pkg/cmd/version"
"go.probo.inc/probo/pkg/cmd/webhook"
)
@@ -73,6 +84,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(accessreview.NewCmdAccessReview(f))
cmd.AddCommand(cmdapi.NewCmdAPI(f))
cmd.AddCommand(asset.NewCmdAsset(f))
cmd.AddCommand(audit.NewCmdAudit(f))
cmd.AddCommand(auditlog.NewCmdAuditLog(f))
cmd.AddCommand(auth.NewCmdAuth(f))
cmd.AddCommand(browse.NewCmdBrowse(f))
@@ -82,13 +94,23 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(control.NewCmdControl(f))
cmd.AddCommand(datum.NewCmdDatum(f))
cmd.AddCommand(document.NewCmdDocument(f))
cmd.AddCommand(dpia.NewCmdDPIA(f))
cmd.AddCommand(evidence.NewCmdEvidence(f))
cmd.AddCommand(finding.NewCmdFinding(f))
cmd.AddCommand(framework.NewCmdFramework(f))
cmd.AddCommand(measure.NewCmdMeasure(f))
cmd.AddCommand(obligation.NewCmdObligation(f))
cmd.AddCommand(org.NewCmdOrg(f))
cmd.AddCommand(processingactivity.NewCmdProcessingActivity(f))
cmd.AddCommand(rightsrequest.NewCmdRightsRequest(f))
cmd.AddCommand(risk.NewCmdRisk(f))
cmd.AddCommand(snapshot.NewCmdSnapshot(f))
cmd.AddCommand(soa.NewCmdSoa(f))
cmd.AddCommand(task.NewCmdTask(f))
cmd.AddCommand(tia.NewCmdTIA(f))
cmd.AddCommand(trustcenter.NewCmdTrustCenter(f))
cmd.AddCommand(user.NewCmdUser(f))
cmd.AddCommand(vendormgmt.NewCmdVendor(f))
cmd.AddCommand(version.NewCmdVersion(f))
cmd.AddCommand(webhook.NewCmdWebhook(f))

View File

@@ -0,0 +1,175 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateSnapshotInput!) {
createSnapshot(input: $input) {
snapshotEdge {
node {
id
name
type
}
}
}
}
`
type createResponse struct {
CreateSnapshot struct {
SnapshotEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
} `json:"node"`
} `json:"snapshotEdge"`
} `json:"createSnapshot"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagType string
flagDescription string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new snapshot",
Example: ` # Create a snapshot interactively
prb snapshot create
# Create a snapshot non-interactively
prb snapshot create --name "Q1 2026 Risks" --type RISKS`,
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 == "" {
err := huh.NewInput().
Title("Snapshot name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagType == "" {
err := huh.NewSelect[string]().
Title("Snapshot type").
Options(
huh.NewOption("Risks", "RISKS"),
huh.NewOption("Vendors", "VENDORS"),
huh.NewOption("Assets", "ASSETS"),
huh.NewOption("Findings", "FINDINGS"),
huh.NewOption("Obligations", "OBLIGATIONS"),
huh.NewOption("Processing Activities", "PROCESSING_ACTIVITIES"),
huh.NewOption("Statements of Applicability", "STATEMENTS_OF_APPLICABILITY"),
).
Value(&flagType).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
if flagType == "" {
return fmt.Errorf("type is required; pass --type or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
"type": flagType,
}
if flagDescription != "" {
input["description"] = flagDescription
}
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)
}
s := resp.CreateSnapshot.SnapshotEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created snapshot %s (%s)\n",
s.ID,
s.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Snapshot name (required)")
cmd.Flags().StringVar(&flagType, "type", "", "Snapshot type: RISKS, VENDORS, ASSETS, FINDINGS, OBLIGATIONS, PROCESSING_ACTIVITIES, STATEMENTS_OF_APPLICABILITY (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Snapshot description")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteSnapshotInput!) {
deleteSnapshot(input: $input) {
deletedSnapshotId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a snapshot",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete snapshot: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete snapshot %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"snapshotId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted snapshot %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,193 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: SnapshotOrder) {
node(id: $id) {
__typename
... on Organization {
snapshots(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
type
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type snapshot struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
CreatedAt string `json:"createdAt"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List snapshots in an organization",
Aliases: []string{"ls"},
Example: ` # List snapshots in the default organization
prb snapshot list
# List snapshots sorted by name
prb snapshot ls --order-by NAME --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME", "TYPE"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
snapshots, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[snapshot], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Snapshots api.Connection[snapshot] `json:"snapshots"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Snapshots, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, snapshots)
}
if len(snapshots) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No snapshots found.")
return nil
}
rows := make([][]string, 0, len(snapshots))
for _, s := range snapshots {
rows = append(rows, []string{
s.ID,
s.Name,
s.Type,
cmdutil.FormatTime(s.CreatedAt),
})
}
t := cmdutil.NewTable("ID", "NAME", "TYPE", "CREATED AT").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(snapshots) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d snapshots\n",
len(snapshots),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of snapshots to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME, TYPE)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 snapshot
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/snapshot/create"
"go.probo.inc/probo/pkg/cmd/snapshot/delete"
"go.probo.inc/probo/pkg/cmd/snapshot/list"
"go.probo.inc/probo/pkg/cmd/snapshot/view"
)
func NewCmdSnapshot(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "snapshot <command>",
Short: "Manage snapshots",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Snapshot {
id
name
description
type
createdAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Type string `json:"type"`
CreatedAt string `json:"createdAt"`
} `json:"node"`
}
func NewCmdView(f *cmdutil.Factory) *cobra.Command {
var flagOutput *string
cmd := &cobra.Command{
Use: "view <id>",
Short: "View a snapshot",
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 {
return fmt.Errorf("snapshot %s not found", args[0])
}
if resp.Node.Typename != "Snapshot" {
return fmt.Errorf("expected Snapshot node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
s := 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(s.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), s.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), s.Type)
if s.Description != nil && *s.Description != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *s.Description)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(s.CreatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,193 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateTaskInput!) {
createTask(input: $input) {
taskEdge {
node {
id
name
state
priority
}
}
}
}
`
type createResponse struct {
CreateTask struct {
TaskEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Priority string `json:"priority"`
} `json:"node"`
} `json:"taskEdge"`
} `json:"createTask"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagDescription string
flagPriority string
flagMeasure string
flagTimeEstimate string
flagAssignedTo string
flagDeadline string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new task",
Example: ` # Create a task interactively
prb task create
# Create a task non-interactively
prb task create --name "Review access controls" --priority HIGH`,
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 == "" {
err := huh.NewInput().
Title("Task name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagPriority == "" {
err := huh.NewSelect[string]().
Title("Task priority").
Options(
huh.NewOption("Urgent", "URGENT"),
huh.NewOption("High", "HIGH"),
huh.NewOption("Medium", "MEDIUM"),
huh.NewOption("Low", "LOW"),
).
Value(&flagPriority).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
}
if flagDescription != "" {
input["description"] = flagDescription
}
if flagPriority != "" {
input["priority"] = flagPriority
}
if flagMeasure != "" {
input["measureId"] = flagMeasure
}
if flagTimeEstimate != "" {
input["timeEstimate"] = flagTimeEstimate
}
if flagAssignedTo != "" {
input["assignedToId"] = flagAssignedTo
}
if flagDeadline != "" {
input["deadline"] = flagDeadline
}
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)
}
t := resp.CreateTask.TaskEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created task %s (%s)\n",
t.ID,
t.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Task name (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Task description")
cmd.Flags().StringVar(&flagPriority, "priority", "", "Task priority: URGENT, HIGH, MEDIUM, LOW")
cmd.Flags().StringVar(&flagMeasure, "measure", "", "Measure ID")
cmd.Flags().StringVar(&flagTimeEstimate, "time-estimate", "", "Time estimate")
cmd.Flags().StringVar(&flagAssignedTo, "assigned-to", "", "Assigned profile ID")
cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteTaskInput!) {
deleteTask(input: $input) {
deletedTaskId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a task",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete task: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete task %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"taskId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted task %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

200
pkg/cmd/task/list/list.go Normal file
View File

@@ -0,0 +1,200 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: TaskOrder) {
node(id: $id) {
__typename
... on Organization {
tasks(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
state
priority
deadline
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type task struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Priority string `json:"priority"`
Deadline *string `json:"deadline"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List tasks in an organization",
Aliases: []string{"ls"},
Example: ` # List tasks in the default organization
prb task list
# List tasks sorted by priority
prb task ls --order-by PRIORITY_RANK --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"PRIORITY_RANK", "CREATED_AT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
tasks, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[task], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Tasks api.Connection[task] `json:"tasks"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Tasks, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, tasks)
}
if len(tasks) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No tasks found.")
return nil
}
rows := make([][]string, 0, len(tasks))
for _, t := range tasks {
deadline := ""
if t.Deadline != nil {
deadline = *t.Deadline
}
rows = append(rows, []string{
t.ID,
t.Name,
t.State,
t.Priority,
deadline,
})
}
t := cmdutil.NewTable("ID", "NAME", "STATE", "PRIORITY", "DEADLINE").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(tasks) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d tasks\n",
len(tasks),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of tasks to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (PRIORITY_RANK, CREATED_AT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

40
pkg/cmd/task/task.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 task
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/task/create"
"go.probo.inc/probo/pkg/cmd/task/delete"
"go.probo.inc/probo/pkg/cmd/task/list"
"go.probo.inc/probo/pkg/cmd/task/update"
"go.probo.inc/probo/pkg/cmd/task/view"
)
func NewCmdTask(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "task <command>",
Short: "Manage tasks",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateTaskInput!) {
updateTask(input: $input) {
task {
id
name
state
priority
}
}
}
`
type updateResponse struct {
UpdateTask struct {
Task struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Priority string `json:"priority"`
} `json:"task"`
} `json:"updateTask"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagDescription string
flagState string
flagPriority string
flagTimeEstimate string
flagDeadline string
flagAssignedTo string
flagMeasure string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a task",
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{
"taskId": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("description") {
input["description"] = flagDescription
}
if cmd.Flags().Changed("state") {
input["state"] = flagState
}
if cmd.Flags().Changed("priority") {
input["priority"] = flagPriority
}
if cmd.Flags().Changed("time-estimate") {
input["timeEstimate"] = flagTimeEstimate
}
if cmd.Flags().Changed("deadline") {
input["deadline"] = flagDeadline
}
if cmd.Flags().Changed("assigned-to") {
if flagAssignedTo == "" {
input["assignedToId"] = nil
} else {
input["assignedToId"] = flagAssignedTo
}
}
if cmd.Flags().Changed("measure") {
if flagMeasure == "" {
input["measureId"] = nil
} else {
input["measureId"] = flagMeasure
}
}
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)
}
t := resp.UpdateTask.Task
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated task %s (%s)\n",
t.ID,
t.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Task name")
cmd.Flags().StringVar(&flagDescription, "description", "", "Task description")
cmd.Flags().StringVar(&flagState, "state", "", "Task state: TODO, IN_PROGRESS, DONE")
cmd.Flags().StringVar(&flagPriority, "priority", "", "Task priority: URGENT, HIGH, MEDIUM, LOW")
cmd.Flags().StringVar(&flagTimeEstimate, "time-estimate", "", "Time estimate")
cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline")
cmd.Flags().StringVar(&flagAssignedTo, "assigned-to", "", "Assigned profile ID")
cmd.Flags().StringVar(&flagMeasure, "measure", "", "Measure ID")
return cmd
}

151
pkg/cmd/task/view/view.go Normal file
View File

@@ -0,0 +1,151 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Task {
id
name
description
state
priority
timeEstimate
deadline
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
State string `json:"state"`
Priority string `json:"priority"`
TimeEstimate *string `json:"timeEstimate"`
Deadline *string `json:"deadline"`
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 <id>",
Short: "View a task",
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 {
return fmt.Errorf("task %s not found", args[0])
}
if resp.Node.Typename != "Task" {
return fmt.Errorf("expected Task node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
t := 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(t.Name))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), t.ID)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), t.State)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Priority:"), t.Priority)
if t.Description != nil && *t.Description != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *t.Description)
}
if t.TimeEstimate != nil && *t.TimeEstimate != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Time Estimate:"), *t.TimeEstimate)
}
if t.Deadline != nil && *t.Deadline != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Deadline:"), *t.Deadline)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(t.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(t.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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/spf13/cobra"
"go.probo.inc/probo/pkg/cli/api"
"go.probo.inc/probo/pkg/cmd/cmdutil"
)
const createMutation = `
mutation($input: CreateTransferImpactAssessmentInput!) {
createTransferImpactAssessment(input: $input) {
transferImpactAssessmentEdge {
node {
id
dataSubjects
legalMechanism
}
}
}
}
`
type createResponse struct {
CreateTransferImpactAssessment struct {
TransferImpactAssessmentEdge struct {
Node struct {
ID string `json:"id"`
DataSubjects string `json:"dataSubjects"`
LegalMechanism string `json:"legalMechanism"`
} `json:"node"`
} `json:"transferImpactAssessmentEdge"`
} `json:"createTransferImpactAssessment"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagProcessingActivity string
flagDataSubjects string
flagLegalMechanism string
flagTransfer string
flagLocalLawRisk string
flagSupplementaryMeasures string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new transfer impact assessment",
Example: ` # Create a TIA
prb tia create --processing-activity <id> --data-subjects "EU residents"
# Create a TIA with all fields
prb tia create --processing-activity <id> --data-subjects "EU residents" --legal-mechanism "SCCs" --transfer "US" --local-law-risk "FISA 702" --supplementary-measures "Encryption"`,
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 flagProcessingActivity == "" {
return fmt.Errorf("processing activity is required; pass --processing-activity")
}
input := map[string]any{
"processingActivityId": flagProcessingActivity,
}
if flagDataSubjects != "" {
input["dataSubjects"] = flagDataSubjects
}
if flagLegalMechanism != "" {
input["legalMechanism"] = flagLegalMechanism
}
if flagTransfer != "" {
input["transfer"] = flagTransfer
}
if flagLocalLawRisk != "" {
input["localLawRisk"] = flagLocalLawRisk
}
if flagSupplementaryMeasures != "" {
input["supplementaryMeasures"] = flagSupplementaryMeasures
}
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)
}
r := resp.CreateTransferImpactAssessment.TransferImpactAssessmentEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created transfer impact assessment %s\n",
r.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagProcessingActivity, "processing-activity", "", "Processing activity ID (required)")
cmd.Flags().StringVar(&flagDataSubjects, "data-subjects", "", "Data subjects")
cmd.Flags().StringVar(&flagLegalMechanism, "legal-mechanism", "", "Legal mechanism")
cmd.Flags().StringVar(&flagTransfer, "transfer", "", "Transfer")
cmd.Flags().StringVar(&flagLocalLawRisk, "local-law-risk", "", "Local law risk")
cmd.Flags().StringVar(&flagSupplementaryMeasures, "supplementary-measures", "", "Supplementary measures")
_ = cmd.MarkFlagRequired("processing-activity")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteTransferImpactAssessmentInput!) {
deleteTransferImpactAssessment(input: $input) {
deletedTransferImpactAssessmentId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a transfer impact assessment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete transfer impact assessment: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete transfer impact assessment %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"transferImpactAssessmentId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted transfer impact assessment %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

195
pkg/cmd/tia/list/list.go Normal file
View File

@@ -0,0 +1,195 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: TransferImpactAssessmentOrder) {
node(id: $id) {
__typename
... on Organization {
transferImpactAssessments(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
dataSubjects
legalMechanism
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type transferImpactAssessment struct {
ID string `json:"id"`
DataSubjects string `json:"dataSubjects"`
LegalMechanism string `json:"legalMechanism"`
CreatedAt string `json:"createdAt"`
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max-3] + "..."
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List transfer impact assessments 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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
tias, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[transferImpactAssessment], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
TransferImpactAssessments api.Connection[transferImpactAssessment] `json:"transferImpactAssessments"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.TransferImpactAssessments, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, tias)
}
if len(tias) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No transfer impact assessments found.")
return nil
}
rows := make([][]string, 0, len(tias))
for _, t := range tias {
rows = append(rows, []string{
t.ID,
truncate(t.DataSubjects, 50),
truncate(t.LegalMechanism, 50),
cmdutil.FormatTime(t.CreatedAt),
})
}
tbl := cmdutil.NewTable("ID", "DATA SUBJECTS", "LEGAL MECHANISM", "CREATED AT").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, tbl)
if totalCount > len(tias) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d transfer impact assessments\n",
len(tias),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of transfer impact assessments to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

40
pkg/cmd/tia/tia.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 tia
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/tia/create"
"go.probo.inc/probo/pkg/cmd/tia/delete"
"go.probo.inc/probo/pkg/cmd/tia/list"
"go.probo.inc/probo/pkg/cmd/tia/update"
"go.probo.inc/probo/pkg/cmd/tia/view"
)
func NewCmdTIA(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "tia <command>",
Short: "Manage transfer impact assessments",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateTransferImpactAssessmentInput!) {
updateTransferImpactAssessment(input: $input) {
transferImpactAssessment {
id
dataSubjects
legalMechanism
}
}
}
`
type updateResponse struct {
UpdateTransferImpactAssessment struct {
TransferImpactAssessment struct {
ID string `json:"id"`
DataSubjects string `json:"dataSubjects"`
LegalMechanism string `json:"legalMechanism"`
} `json:"transferImpactAssessment"`
} `json:"updateTransferImpactAssessment"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagDataSubjects string
flagLegalMechanism string
flagTransfer string
flagLocalLawRisk string
flagSupplementaryMeasures string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a transfer impact assessment",
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{
"id": args[0],
}
if cmd.Flags().Changed("data-subjects") {
input["dataSubjects"] = flagDataSubjects
}
if cmd.Flags().Changed("legal-mechanism") {
input["legalMechanism"] = flagLegalMechanism
}
if cmd.Flags().Changed("transfer") {
input["transfer"] = flagTransfer
}
if cmd.Flags().Changed("local-law-risk") {
input["localLawRisk"] = flagLocalLawRisk
}
if cmd.Flags().Changed("supplementary-measures") {
input["supplementaryMeasures"] = flagSupplementaryMeasures
}
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)
}
r := resp.UpdateTransferImpactAssessment.TransferImpactAssessment
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated transfer impact assessment %s\n",
r.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagDataSubjects, "data-subjects", "", "Data subjects")
cmd.Flags().StringVar(&flagLegalMechanism, "legal-mechanism", "", "Legal mechanism")
cmd.Flags().StringVar(&flagTransfer, "transfer", "", "Transfer")
cmd.Flags().StringVar(&flagLocalLawRisk, "local-law-risk", "", "Local law risk")
cmd.Flags().StringVar(&flagSupplementaryMeasures, "supplementary-measures", "", "Supplementary measures")
return cmd
}

155
pkg/cmd/tia/view/view.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 TransferImpactAssessment {
id
dataSubjects
legalMechanism
transfer
localLawRisk
supplementaryMeasures
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
DataSubjects string `json:"dataSubjects"`
LegalMechanism string `json:"legalMechanism"`
Transfer string `json:"transfer"`
LocalLawRisk string `json:"localLawRisk"`
SupplementaryMeasures string `json:"supplementaryMeasures"`
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 <id>",
Short: "View a transfer impact assessment",
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 {
return fmt.Errorf("transfer impact assessment %s not found", args[0])
}
if resp.Node.Typename != "TransferImpactAssessment" {
return fmt.Errorf("expected TransferImpactAssessment node, got %s", resp.Node.Typename)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
}
r := resp.Node
out := f.IOStreams.Out
bold := lipgloss.NewStyle().Bold(true)
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(26)
_, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Transfer Impact Assessment"))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID)
if r.DataSubjects != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Data Subjects:"), r.DataSubjects)
}
if r.LegalMechanism != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Legal Mechanism:"), r.LegalMechanism)
}
if r.Transfer != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Transfer:"), r.Transfer)
}
if r.LocalLawRisk != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Local Law Risk:"), r.LocalLawRisk)
}
if r.SupplementaryMeasures != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Supplementary Measures:"), r.SupplementaryMeasures)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt))
return nil
},
}
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteTrustCenterFileInput!) {
deleteTrustCenterFile(input: $input) {
deletedTrustCenterFileId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a trust center file",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete file: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete trust center file %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"id": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted trust center file %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 file
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/trust-center/file/delete"
"go.probo.inc/probo/pkg/cmd/trust-center/file/list"
)
func NewCmdFile(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "file <command>",
Short: "Manage trust center files",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,195 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: TrustCenterFileOrder) {
node(id: $id) {
__typename
... on Organization {
trustCenterFiles(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
category
trustCenterVisibility
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type trustCenterFile struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
TrustCenterVisibility string `json:"trustCenterVisibility"`
CreatedAt string `json:"createdAt"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List trust center files",
Aliases: []string{"ls"},
Example: ` # List files in the default organization
prb trust-center file list
# List files sorted by name
prb trust-center file ls --order-by NAME`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
files, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[trustCenterFile], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
TrustCenterFiles api.Connection[trustCenterFile] `json:"trustCenterFiles"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.TrustCenterFiles, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, files)
}
if len(files) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No files found.")
return nil
}
rows := make([][]string, 0, len(files))
for _, file := range files {
rows = append(rows, []string{
file.ID,
file.Name,
file.Category,
file.TrustCenterVisibility,
})
}
t := cmdutil.NewTable("ID", "NAME", "CATEGORY", "VISIBILITY").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(files) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d files\n",
len(files),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of files to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (NAME, CREATED_AT, UPDATED_AT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,227 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 trustCenterQuery = `
query($id: ID!) {
node(id: $id) {
__typename
... on Organization {
trustCenter {
id
}
}
}
}
`
const createMutation = `
mutation($input: CreateTrustCenterReferenceInput!) {
createTrustCenterReference(input: $input) {
trustCenterReferenceEdge {
node {
id
name
description
websiteUrl
rank
}
}
}
}
`
type trustCenterQueryResponse struct {
Node *struct {
Typename string `json:"__typename"`
TrustCenter *struct {
ID string `json:"id"`
} `json:"trustCenter"`
} `json:"node"`
}
type createResponse struct {
CreateTrustCenterReference struct {
TrustCenterReferenceEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
WebsiteUrl *string `json:"websiteUrl"`
Rank int `json:"rank"`
} `json:"node"`
} `json:"trustCenterReferenceEdge"`
} `json:"createTrustCenterReference"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagDescription string
flagWebsite string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a trust center reference",
Example: ` # Create a reference interactively
prb trust-center reference create
# Create a reference non-interactively
prb trust-center ref create --name "Acme Corp" --description "Enterprise customer" --website "https://acme.com"`,
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'")
}
// Fetch trust center ID from organization.
data, err := client.Do(
trustCenterQuery,
map[string]any{"id": flagOrg},
)
if err != nil {
return err
}
var tcResp trustCenterQueryResponse
if err := json.Unmarshal(data, &tcResp); err != nil {
return fmt.Errorf("cannot parse response: %w", err)
}
if tcResp.Node == nil {
return fmt.Errorf("organization %s not found", flagOrg)
}
if tcResp.Node.Typename != "Organization" {
return fmt.Errorf("expected Organization node, got %s", tcResp.Node.Typename)
}
if tcResp.Node.TrustCenter == nil {
return fmt.Errorf("trust center not found for organization %s", flagOrg)
}
if f.IOStreams.IsInteractive() {
if flagName == "" {
err := huh.NewInput().
Title("Reference name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagDescription == "" {
err := huh.NewInput().
Title("Description (optional)").
Value(&flagDescription).
Run()
if err != nil {
return err
}
}
if flagWebsite == "" {
err := huh.NewInput().
Title("Website URL (optional)").
Value(&flagWebsite).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
input := map[string]any{
"trustCenterId": tcResp.Node.TrustCenter.ID,
"name": flagName,
}
if flagDescription != "" {
input["description"] = flagDescription
}
if flagWebsite != "" {
input["websiteUrl"] = flagWebsite
}
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)
}
r := resp.CreateTrustCenterReference.TrustCenterReferenceEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created reference %s (%s)\n",
r.ID,
r.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Reference name (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Reference description")
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteTrustCenterReferenceInput!) {
deleteTrustCenterReference(input: $input) {
deletedTrustCenterReferenceId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a trust center reference",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete reference: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete reference %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"id": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted reference %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,208 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: TrustCenterReferenceOrder) {
node(id: $id) {
__typename
... on Organization {
trustCenter {
references(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
description
websiteUrl
rank
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
}
`
type trustCenterReference struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
WebsiteUrl *string `json:"websiteUrl"`
Rank int `json:"rank"`
CreatedAt string `json:"createdAt"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List trust center references",
Aliases: []string{"ls"},
Example: ` # List references in the default organization
prb trust-center reference list
# List references sorted by rank
prb trust-center ref ls --order-by RANK`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"RANK", "NAME", "CREATED_AT", "UPDATED_AT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
refs, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[trustCenterReference], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
TrustCenter *struct {
References api.Connection[trustCenterReference] `json:"references"`
} `json:"trustCenter"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
if resp.Node.TrustCenter == nil {
return nil, fmt.Errorf("trust center not found for organization %s", flagOrg)
}
return &resp.Node.TrustCenter.References, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, refs)
}
if len(refs) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No references found.")
return nil
}
rows := make([][]string, 0, len(refs))
for _, r := range refs {
website := ""
if r.WebsiteUrl != nil {
website = *r.WebsiteUrl
}
rows = append(rows, []string{
r.ID,
r.Name,
website,
fmt.Sprintf("%d", r.Rank),
})
}
t := cmdutil.NewTable("ID", "NAME", "WEBSITE", "RANK").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(refs) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d references\n",
len(refs),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of references to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (RANK, NAME, CREATED_AT, UPDATED_AT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 reference
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/trust-center/reference/create"
"go.probo.inc/probo/pkg/cmd/trust-center/reference/delete"
"go.probo.inc/probo/pkg/cmd/trust-center/reference/list"
)
func NewCmdReference(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "reference <command>",
Short: "Manage trust center references",
Aliases: []string{"ref"},
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 trustcenter
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/trust-center/file"
"go.probo.inc/probo/pkg/cmd/trust-center/reference"
"go.probo.inc/probo/pkg/cmd/trust-center/update"
"go.probo.inc/probo/pkg/cmd/trust-center/view"
)
func NewCmdTrustCenter(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "trust-center <command>",
Short: "Manage trust center",
Aliases: []string{"tc"},
}
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(reference.NewCmdReference(f))
cmd.AddCommand(file.NewCmdFile(f))
return cmd
}

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 trustCenterQuery = `
query($id: ID!) {
node(id: $id) {
__typename
... on Organization {
trustCenter {
id
}
}
}
}
`
const updateMutation = `
mutation($input: UpdateTrustCenterInput!) {
updateTrustCenter(input: $input) {
trustCenter {
id
active
searchEngineIndexing
}
}
}
`
type trustCenterQueryResponse struct {
Node *struct {
Typename string `json:"__typename"`
TrustCenter *struct {
ID string `json:"id"`
} `json:"trustCenter"`
} `json:"node"`
}
type updateResponse struct {
UpdateTrustCenter struct {
TrustCenter struct {
ID string `json:"id"`
Active bool `json:"active"`
SearchEngineIndexing string `json:"searchEngineIndexing"`
} `json:"trustCenter"`
} `json:"updateTrustCenter"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagActive bool
flagSearchEngineIndexing string
)
cmd := &cobra.Command{
Use: "update",
Short: "Update trust center settings",
Example: ` # Enable the trust center
prb trust-center update --active
# Disable search engine indexing
prb trust-center update --search-engine-indexing NOT_INDEXABLE`,
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 flagOrg == "" {
flagOrg = hc.Organization
}
if flagOrg == "" {
return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'")
}
// Fetch trust center ID from organization.
data, err := client.Do(
trustCenterQuery,
map[string]any{"id": flagOrg},
)
if err != nil {
return err
}
var tcResp trustCenterQueryResponse
if err := json.Unmarshal(data, &tcResp); err != nil {
return fmt.Errorf("cannot parse response: %w", err)
}
if tcResp.Node == nil {
return fmt.Errorf("organization %s not found", flagOrg)
}
if tcResp.Node.Typename != "Organization" {
return fmt.Errorf("expected Organization node, got %s", tcResp.Node.Typename)
}
if tcResp.Node.TrustCenter == nil {
return fmt.Errorf("trust center not found for organization %s", flagOrg)
}
input := map[string]any{
"trustCenterId": tcResp.Node.TrustCenter.ID,
}
if cmd.Flags().Changed("active") {
input["active"] = flagActive
}
if cmd.Flags().Changed("search-engine-indexing") {
if err := cmdutil.ValidateEnum("search-engine-indexing", flagSearchEngineIndexing, []string{"INDEXABLE", "NOT_INDEXABLE"}); err != nil {
return err
}
input["searchEngineIndexing"] = flagSearchEngineIndexing
}
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)
}
tc := resp.UpdateTrustCenter.TrustCenter
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated trust center %s\n",
tc.ID,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().BoolVar(&flagActive, "active", false, "Enable or disable the trust center")
cmd.Flags().StringVar(&flagSearchEngineIndexing, "search-engine-indexing", "", "Search engine indexing: INDEXABLE, NOT_INDEXABLE")
return cmd
}

View File

@@ -0,0 +1,172 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Organization {
trustCenter {
id
active
searchEngineIndexing
logoFileUrl
darkLogoFileUrl
ndaFileName
ndaFileUrl
createdAt
updatedAt
}
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
TrustCenter *struct {
ID string `json:"id"`
Active bool `json:"active"`
SearchEngineIndexing string `json:"searchEngineIndexing"`
LogoFileUrl *string `json:"logoFileUrl"`
DarkLogoFileUrl *string `json:"darkLogoFileUrl"`
NdaFileName *string `json:"ndaFileName"`
NdaFileUrl *string `json:"ndaFileUrl"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
} `json:"trustCenter"`
} `json:"node"`
}
func NewCmdView(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagOutput *string
)
cmd := &cobra.Command{
Use: "view",
Short: "View trust center settings",
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'")
}
data, err := client.Do(
viewQuery,
map[string]any{"id": flagOrg},
)
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 {
return fmt.Errorf("organization %s not found", flagOrg)
}
if resp.Node.Typename != "Organization" {
return fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
if resp.Node.TrustCenter == nil {
return fmt.Errorf("trust center not found for organization %s", flagOrg)
}
tc := resp.Node.TrustCenter
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, tc)
}
out := f.IOStreams.Out
bold := lipgloss.NewStyle().Bold(true)
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(28)
_, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Trust Center"))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), tc.ID)
_, _ = fmt.Fprintf(out, "%s%v\n", label.Render("Active:"), tc.Active)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Search Engine Indexing:"), tc.SearchEngineIndexing)
if tc.NdaFileName != nil && *tc.NdaFileName != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("NDA File:"), *tc.NdaFileName)
}
if tc.LogoFileUrl != nil && *tc.LogoFileUrl != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Logo URL:"), *tc.LogoFileUrl)
}
if tc.DarkLogoFileUrl != nil && *tc.DarkLogoFileUrl != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Dark Logo URL:"), *tc.DarkLogoFileUrl)
}
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(tc.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(tc.UpdatedAt))
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: CreateVendorInput!) {
createVendor(input: $input) {
vendorEdge {
node {
id
name
category
}
}
}
}
`
type createResponse struct {
CreateVendor struct {
VendorEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
} `json:"node"`
} `json:"vendorEdge"`
} `json:"createVendor"`
}
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagName string
flagCategory string
flagDescription string
flagLegalName string
flagAddress string
flagWebsite string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a new vendor",
Example: ` # Create a vendor interactively
prb vendor create
# Create a vendor non-interactively
prb vendor create --name "Acme Corp" --category CLOUD_PROVIDER`,
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 == "" {
err := huh.NewInput().
Title("Vendor name").
Value(&flagName).
Run()
if err != nil {
return err
}
}
if flagCategory == "" {
err := huh.NewSelect[string]().
Title("Vendor category").
Options(
huh.NewOption("Analytics", "ANALYTICS"),
huh.NewOption("Cloud Monitoring", "CLOUD_MONITORING"),
huh.NewOption("Cloud Provider", "CLOUD_PROVIDER"),
huh.NewOption("Collaboration", "COLLABORATION"),
huh.NewOption("Customer Support", "CUSTOMER_SUPPORT"),
huh.NewOption("Data Storage and Processing", "DATA_STORAGE_AND_PROCESSING"),
huh.NewOption("Document Management", "DOCUMENT_MANAGEMENT"),
huh.NewOption("Employee Management", "EMPLOYEE_MANAGEMENT"),
huh.NewOption("Engineering", "ENGINEERING"),
huh.NewOption("Finance", "FINANCE"),
huh.NewOption("Identity Provider", "IDENTITY_PROVIDER"),
huh.NewOption("IT", "IT"),
huh.NewOption("Marketing", "MARKETING"),
huh.NewOption("Office Operations", "OFFICE_OPERATIONS"),
huh.NewOption("Other", "OTHER"),
huh.NewOption("Password Management", "PASSWORD_MANAGEMENT"),
huh.NewOption("Product and Design", "PRODUCT_AND_DESIGN"),
huh.NewOption("Professional Services", "PROFESSIONAL_SERVICES"),
huh.NewOption("Recruiting", "RECRUITING"),
huh.NewOption("Sales", "SALES"),
huh.NewOption("Security", "SECURITY"),
huh.NewOption("Version Control", "VERSION_CONTROL"),
).
Value(&flagCategory).
Run()
if err != nil {
return err
}
}
}
if flagName == "" {
return fmt.Errorf("name is required; pass --name or run interactively")
}
if flagCategory == "" {
return fmt.Errorf("category is required; pass --category or run interactively")
}
input := map[string]any{
"organizationId": flagOrg,
"name": flagName,
"category": flagCategory,
}
if flagDescription != "" {
input["description"] = flagDescription
}
if flagLegalName != "" {
input["legalName"] = flagLegalName
}
if flagAddress != "" {
input["headquarterAddress"] = flagAddress
}
if flagWebsite != "" {
input["websiteUrl"] = flagWebsite
}
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)
}
v := resp.CreateVendor.VendorEdge.Node
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Created vendor %s (%s)\n",
v.ID,
v.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagName, "name", "", "Vendor name (required)")
cmd.Flags().StringVar(&flagCategory, "category", "", "Vendor category (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Vendor description")
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address")
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
return cmd
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: DeleteVendorInput!) {
deleteVendor(input: $input) {
deletedVendorId
}
}
`
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var flagYes bool
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete a vendor",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !flagYes {
if !f.IOStreams.IsInteractive() {
return fmt.Errorf("cannot delete vendor: confirmation required, use --yes to confirm")
}
var confirmed bool
err := huh.NewConfirm().
Title(fmt.Sprintf("Delete vendor %s?", args[0])).
Value(&confirmed).
Run()
if 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{
"vendorId": args[0],
},
},
)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Deleted vendor %s\n",
args[0],
)
return nil
},
}
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
return cmd
}

View File

@@ -0,0 +1,190 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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, $orderBy: VendorOrder) {
node(id: $id) {
__typename
... on Organization {
vendors(first: $first, after: $after, orderBy: $orderBy) {
totalCount
edges {
node {
id
name
category
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`
type vendor struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagLimit int
flagOrderBy string
flagOrderDir string
flagOutput *string
)
cmd := &cobra.Command{
Use: "list",
Short: "List vendors in an organization",
Aliases: []string{"ls"},
Example: ` # List vendors in the default organization
prb vendor list
# List vendors sorted by name
prb vendor ls --order-by NAME --json`,
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,
}
if flagOrderBy != "" {
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil {
return err
}
variables["orderBy"] = map[string]any{
"field": flagOrderBy,
"direction": flagOrderDir,
}
}
vendors, totalCount, err := api.Paginate(
client,
listQuery,
variables,
flagLimit,
func(data json.RawMessage) (*api.Connection[vendor], error) {
var resp struct {
Node *struct {
Typename string `json:"__typename"`
Vendors api.Connection[vendor] `json:"vendors"`
} `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)
}
if resp.Node.Typename != "Organization" {
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
}
return &resp.Node.Vendors, nil
},
)
if err != nil {
return err
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, vendors)
}
if len(vendors) == 0 {
_, _ = fmt.Fprintln(f.IOStreams.Out, "No vendors found.")
return nil
}
rows := make([][]string, 0, len(vendors))
for _, v := range vendors {
rows = append(rows, []string{
v.ID,
v.Name,
v.Category,
})
}
t := cmdutil.NewTable("ID", "NAME", "CATEGORY").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(vendors) {
_, _ = fmt.Fprintf(
f.IOStreams.ErrOut,
"\nShowing %d of %d vendors\n",
len(vendors),
totalCount,
)
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of vendors to list")
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (NAME, CREATED_AT, UPDATED_AT)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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: UpdateVendorInput!) {
updateVendor(input: $input) {
vendor {
id
name
category
}
}
}
`
type updateResponse struct {
UpdateVendor struct {
Vendor struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
} `json:"vendor"`
} `json:"updateVendor"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagName string
flagDescription string
flagCategory string
flagLegalName string
flagAddress string
flagWebsite string
)
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a vendor",
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{
"id": args[0],
}
if cmd.Flags().Changed("name") {
input["name"] = flagName
}
if cmd.Flags().Changed("description") {
input["description"] = flagDescription
}
if cmd.Flags().Changed("category") {
input["category"] = flagCategory
}
if cmd.Flags().Changed("legal-name") {
input["legalName"] = flagLegalName
}
if cmd.Flags().Changed("address") {
input["headquarterAddress"] = flagAddress
}
if cmd.Flags().Changed("website") {
input["websiteUrl"] = flagWebsite
}
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)
}
v := resp.UpdateVendor.Vendor
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated vendor %s (%s)\n",
v.ID,
v.Name,
)
return nil
},
}
cmd.Flags().StringVar(&flagName, "name", "", "Vendor name")
cmd.Flags().StringVar(&flagDescription, "description", "", "Vendor description")
cmd.Flags().StringVar(&flagCategory, "category", "", "Vendor category")
cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name")
cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address")
cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL")
return cmd
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 vendormgmt
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/vendormgmt/create"
"go.probo.inc/probo/pkg/cmd/vendormgmt/delete"
"go.probo.inc/probo/pkg/cmd/vendormgmt/list"
"go.probo.inc/probo/pkg/cmd/vendormgmt/update"
"go.probo.inc/probo/pkg/cmd/vendormgmt/view"
)
func NewCmdVendor(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "vendor <command>",
Short: "Manage vendors",
}
cmd.AddCommand(list.NewCmdList(f))
cmd.AddCommand(create.NewCmdCreate(f))
cmd.AddCommand(view.NewCmdView(f))
cmd.AddCommand(update.NewCmdUpdate(f))
cmd.AddCommand(delete.NewCmdDelete(f))
return cmd
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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 Vendor {
id
name
description
category
legalName
headquarterAddress
websiteUrl
createdAt
updatedAt
}
}
}
`
type viewResponse struct {
Node *struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Category string `json:"category"`
LegalName *string `json:"legalName"`
HeadquarterAddress *string `json:"headquarterAddress"`
WebsiteUrl *string `json:"websiteUrl"`
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 <id>",
Short: "View a vendor",
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 {
return fmt.Errorf("vendor %s not found", args[0])
}
if resp.Node.Typename != "Vendor" {
return fmt.Errorf("expected Vendor node, got %s", resp.Node.Typename)
}
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("Category:"), v.Category)
if v.Description != nil && *v.Description != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *v.Description)
}
if v.LegalName != nil && *v.LegalName != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Legal Name:"), *v.LegalName)
}
if v.HeadquarterAddress != nil && *v.HeadquarterAddress != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Address:"), *v.HeadquarterAddress)
}
if v.WebsiteUrl != nil && *v.WebsiteUrl != "" {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Website:"), *v.WebsiteUrl)
}
_, _ = 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
}