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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions cmd/plan/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package plan

import (
"encoding/json"
"fmt"
"strings"

"github.com/mitchellh/go-wordwrap"
"github.com/spf13/cobra"
"github.com/storacha/go-ucanto/did"

"github.com/storacha/guppy/internal/cmdutil"
"github.com/storacha/guppy/pkg/config"
"github.com/storacha/guppy/pkg/didmailto"
)

var getFlags struct {
jsonOutput bool
}

func init() {
getCmd.Flags().BoolVar(&getFlags.jsonOutput, "json", false, "Output in JSON format")
}

var getCmd = &cobra.Command{
Use: "get [account-did]",
Short: "Get the billing plan for an account",
Long: wordwrap.WrapString(
"Displays the billing plan the current account is subscribed to. "+
"If no account DID is provided, it defaults to the currently logged-in account.",
80),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

cfg, err := config.Load[config.Config]()
if err != nil {
return err
}
c := cmdutil.MustGetClient(cfg.Repo.Dir)

var accountDID did.DID

if len(args) > 0 {
accountDID, err = didmailto.FromInput(args[0])
if err != nil {
return fmt.Errorf("invalid account DID: %w", err)
}
} else {
accounts, err := c.Accounts()
if err != nil {
return fmt.Errorf("listing accounts: %w", err)
}
if len(accounts) == 0 {
return fmt.Errorf("no accounts found. Please login with 'guppy login'")
}
accountDID = accounts[0]
}

plan, err := c.PlanGet(ctx, accountDID)
if err != nil {
if strings.Contains(err.Error(), "billing profile not found") {
if getFlags.jsonOutput {
fmt.Println(`{"error": "No billing profile found"}`)
return nil
}
fmt.Printf(" No billing profile found for %s\n", accountDID)
fmt.Println(" Please visit the console to set up a plan: https://console.storacha.network")
return nil
}
return fmt.Errorf("getting plan: %w", err)
}

if getFlags.jsonOutput {
jsonBytes, err := json.MarshalIndent(plan, "", " ")
if err != nil {
return fmt.Errorf("marshaling output: %w", err)
}
fmt.Println(string(jsonBytes))
} else {
fmt.Printf("Plan Details for %s:\n", accountDID)
fmt.Printf(" Product: %s\n", plan.Product)
fmt.Printf(" Limit: %s\n", plan.Limit)
if plan.UpdatedAt != "" {
fmt.Printf(" Updated: %s\n", plan.UpdatedAt)
}
}

return nil
},
}
20 changes: 20 additions & 0 deletions cmd/plan/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package plan

import (
"github.com/mitchellh/go-wordwrap"
"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "plan",
Short: "Manage billing plans",
Long: wordwrap.WrapString(
"Inspect and manage the billing plan associated with your Storacha account.",
80),
}

func init() {
Cmd.AddCommand(
getCmd,
)
}
78 changes: 78 additions & 0 deletions cmd/plan/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package plan

import (
"fmt"
"strings"

"github.com/mitchellh/go-wordwrap"
"github.com/spf13/cobra"
"github.com/storacha/go-ucanto/did"

"github.com/storacha/guppy/internal/cmdutil"
"github.com/storacha/guppy/pkg/config"
"github.com/storacha/guppy/pkg/didmailto"
)

var setCmd = &cobra.Command{
Use: "set <product-did>",
Short: "Set the billing plan for an account",
Long: wordwrap.WrapString(
"Sets the billing plan for the account. You must provide the Product DID (e.g., did:web:starter.storacha.network). "+
"Optionally provide the account DID via --account, otherwise it defaults to the current account.",
80),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
productDIDStr := args[0]

productDID, err := did.Parse(productDIDStr)
if err != nil {
return fmt.Errorf("invalid product DID: %w", err)
}

cfg, err := config.Load[config.Config]()
if err != nil {
return err
}
c := cmdutil.MustGetClient(cfg.Repo.Dir)

var accountDID did.DID
if cmd.Flag("account").Changed {
val, _ := cmd.Flags().GetString("account")
accountDID, err = didmailto.FromInput(val)
if err != nil {
return fmt.Errorf("invalid account DID: %w", err)
}
} else {
accounts, err := c.Accounts()
if err != nil {
return err
}
if len(accounts) == 0 {
return fmt.Errorf("no accounts found")
}
accountDID = accounts[0]
}

fmt.Printf("Setting plan for %s to %s...\n", accountDID, productDID)
_, err = c.PlanSet(ctx, accountDID, productDID)

if err != nil {
if strings.Contains(err.Error(), "billing profile not found") {
fmt.Println("\n Update Failed: No Billing Profile Found")
fmt.Println(" To change plans, you must first set up a payment method.")
fmt.Println(" Please visit the console: https://console.storacha.network")
return nil
}
return err
}

fmt.Println("Success! Plan updated.")
return nil
},
}

func init() {
setCmd.Flags().String("account", "", "The account DID to update")
Cmd.AddCommand(setCmd)
}
48 changes: 48 additions & 0 deletions cmd/proof/ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package proof

import (
"fmt"

"github.com/spf13/cobra"

"github.com/storacha/guppy/internal/cmdutil"
"github.com/storacha/guppy/pkg/config"
)

var lsCmd = &cobra.Command{
Use: "ls",
Short: "List proofs of capabilities delegated to this agent.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load[config.Config]()
if err != nil {
return err
}

c := cmdutil.MustGetClient(cfg.Repo.Dir)

proofs, err := c.Proofs()
if err != nil {
return fmt.Errorf("getting proofs: %w", err)
}

if len(proofs) == 0 {
fmt.Println("No proofs found in the local agent store.")
return nil
}

fmt.Printf("Found %d proof(s):\n", len(proofs))

for _, p := range proofs {
fmt.Printf("CID: %s\n", p.Link().String())
fmt.Printf("Issuer: %s\n", p.Issuer().DID().String())
fmt.Println("Capabilities:")

for _, cap := range p.Capabilities() {
fmt.Printf(" - %v (with: %v)\n", cap.Can(), cap.With())
}
}

return nil
},
}
1 change: 1 addition & 0 deletions cmd/proof/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ var Cmd = &cobra.Command{

func init() {
Cmd.AddCommand(addCmd)
Cmd.AddCommand(lsCmd)
}
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import (
"github.com/storacha/guppy/cmd/account"
"github.com/storacha/guppy/cmd/delegation"
"github.com/storacha/guppy/cmd/gateway"
"github.com/storacha/guppy/cmd/plan"
"github.com/storacha/guppy/cmd/proof"
"github.com/storacha/guppy/cmd/space"
"github.com/storacha/guppy/cmd/upload"
"github.com/storacha/guppy/cmd/usage"
)

var (
Expand Down Expand Up @@ -103,6 +105,8 @@ func init() {
delegation.Cmd,
account.Cmd,
blob.Cmd,
plan.Cmd,
usage.Cmd,
)
}

Expand Down
73 changes: 73 additions & 0 deletions cmd/usage/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package usage

import (
"fmt"
"time"

"github.com/dustin/go-humanize"
"github.com/spf13/cobra"
"github.com/storacha/go-ucanto/did"

"github.com/storacha/guppy/internal/cmdutil"
"github.com/storacha/guppy/pkg/config"
)

func newReportCmd() *cobra.Command {
return &cobra.Command{
Use: "report [space-did]",
Short: "Show storage usage",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

cfg, err := config.Load[config.Config]()
if err != nil {
return err
}

c := cmdutil.MustGetClient(cfg.Repo.Dir)

var spaceDID did.DID

if len(args) > 0 {
parsedDID, err := did.Parse(args[0])
if err != nil {
return fmt.Errorf("invalid space DID: %w", err)
}
spaceDID = parsedDID
} else {
spaces, err := c.Spaces()
if err != nil {
return fmt.Errorf("listing spaces: %w", err)
}
if len(spaces) == 0 {
return fmt.Errorf("no spaces found")
}
spaceDID = spaces[0].DID()
}

to := time.Now().UTC()
from := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)

fmt.Printf("Fetching usage for space %s...\n", spaceDID)

reports, err := c.UsageReport(ctx, spaceDID, from, to)
if err != nil {
return err
}

fmt.Println("---------------------------------------------------")
if len(reports) == 0 {
fmt.Println("No usage data found for this period.")
} else {
for providerDID, report := range reports {
fmt.Printf("Provider: %s\n", providerDID)
fmt.Printf("Total Usage: %s\n", humanize.IBytes(report.Size))
fmt.Println("---------------------------------------------------")
}
}

return nil
},
}
}
15 changes: 15 additions & 0 deletions cmd/usage/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package usage

import (
"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "usage",
Short: "Manage usage reports",
Long: "View storage usage reports for your spaces.",
}

func init() {
Cmd.AddCommand(newReportCmd())
}
28 changes: 28 additions & 0 deletions pkg/client/nodevalue/nodevalue.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package nodevalue

import (
"bytes"
"encoding/json"
"fmt"
"strings"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/node/basicnode"
)

// NodeValue converts an arbitrary IPLD node to a Go value. This is useful as a
Expand Down Expand Up @@ -66,3 +71,26 @@ func NodeValue(node ipld.Node) (res any, err error) {
return nil, fmt.Errorf("unsupported node kind: %s", node.Kind())
}
}

// FromAny converts an arbitrary Go value (struct, map, etc) to an IPLD Node.
func FromAny(val any) (ipld.Node, error) {
if val == nil {
nb := basicnode.Prototype.Any.NewBuilder()
if err := dagjson.Decode(nb, strings.NewReader("null")); err != nil {
return nil, fmt.Errorf("failed to create null node: %w", err)
}
return nb.Build(), nil
}

b, err := json.Marshal(val)
if err != nil {
return nil, fmt.Errorf("marshaling to json: %w", err)
}

nb := basicnode.Prototype.Any.NewBuilder()
if err := dagjson.Decode(nb, bytes.NewReader(b)); err != nil {
return nil, fmt.Errorf("decoding json to ipld: %w", err)
}

return nb.Build(), nil
}
Loading