package main import ( "context" "encoding/json" "errors" "flag" "fmt" "io" "net/url" "strconv" ) func commerceURL(c *Client, route string) string { return joinURL(effectiveBaseURL(c.cfg), route) } func runCommerceCarts(ctx context.Context, c *Client, args []string, stdin io.Reader, stdout io.Writer) error { if len(args) == 0 || args[0] == "help" { _, _ = io.WriteString(stdout, "Usage: carts get [id]\n carts add --sku SKU [--quantity N]\n carts set --input FILE|-\n carts remove --item-id ID\n") return nil } switch args[0] { case "get": if len(args) > 1 { body, err := c.get(ctx, commerceURL(c, "/cart/byid/"+escapePathSegment(args[1]))) if err != nil { return err } return writeRaw(stdout, body) } body, err := c.get(ctx, commerceURL(c, "/cart")) if err != nil { return err } return writeRaw(stdout, body) case "add": id, err := requireID(args[1:], "commerce carts add requires a cart id") if err != nil { return err } fs := flag.NewFlagSet("commerce carts add", flag.ContinueOnError) fs.SetOutput(io.Discard) sku := fs.String("sku", "", "SKU") qty := fs.Int("quantity", 1, "quantity") if err := fs.Parse(args[2:]); err != nil { return err } if *sku == "" { return errors.New("carts add requires --sku") } body, err := c.postJSON(ctx, commerceURL(c, "/cart/byid/"+escapePathSegment(id)), map[string]any{"sku": *sku, "quantity": *qty}) if err != nil { return err } return writeRaw(stdout, body) case "set": id, err := requireID(args[1:], "commerce carts set requires a cart id") if err != nil { return err } body, err := readRequiredBody(stdin, args[2:], "cart") if err != nil { return err } if !json.Valid(body) { return errors.New("cart input is not valid JSON") } response, err := c.postBody(ctx, commerceURL(c, "/cart/byid/"+escapePathSegment(id)+"/set"), body, "application/json") if err != nil { return err } return writeRaw(stdout, response) case "remove": id, err := requireID(args[1:], "commerce carts remove requires a cart id") if err != nil { return err } fs := flag.NewFlagSet("commerce carts remove", flag.ContinueOnError) fs.SetOutput(io.Discard) itemID := fs.Int64("item-id", -1, "cart line item id") if err := fs.Parse(args[2:]); err != nil { return err } if *itemID < 0 { return errors.New("carts remove requires --item-id") } body, err := c.delete(ctx, commerceURL(c, "/cart/byid/"+escapePathSegment(id)+"/"+strconv.FormatInt(*itemID, 10))) if err != nil { return err } return writeRaw(stdout, body) default: return fmt.Errorf("unknown commerce carts command %q", args[0]) } } func runCommerceCheckout(ctx context.Context, c *Client, args []string, stdin io.Reader, stdout io.Writer) error { if len(args) == 0 || args[0] == "help" { _, _ = io.WriteString(stdout, "Usage: checkout create --country SE --terms accepted\n") return nil } if args[0] != "create" { return fmt.Errorf("unknown commerce checkout command %q", args[0]) } id, err := requireID(args[1:], "commerce checkout create requires a cart id") if err != nil { return err } fs := flag.NewFlagSet("commerce checkout create", flag.ContinueOnError) fs.SetOutput(io.Discard) country := fs.String("country", "SE", "country code") terms := fs.String("terms", "accepted", "terms value") if err := fs.Parse(args[2:]); err != nil { return err } body, err := c.postJSON(ctx, commerceURL(c, "/cart/byid/"+escapePathSegment(id)+"/checkout-order"), map[string]string{"country": *country, "terms": *terms}) if err != nil { return err } return writeRaw(stdout, body) } func runCommerceOrders(ctx context.Context, c *Client, args []string, stdout io.Writer) error { if len(args) == 0 || args[0] == "help" { _, _ = io.WriteString(stdout, "Usage: orders get \n orders value-codes [--status STATUS --provider PROVIDER --limit N --offset N]\n") return nil } switch args[0] { case "get": id, err := requireID(args[1:], "commerce orders get requires an order id") if err != nil { return err } body, err := c.get(ctx, commerceURL(c, "/api/orders/"+escapePathSegment(id))) if err != nil { return err } return writeRaw(stdout, body) case "value-codes": fs := flag.NewFlagSet("commerce orders value-codes", flag.ContinueOnError) fs.SetOutput(io.Discard) status := fs.String("status", "", "status") provider := fs.String("provider", "", "provider") q := fs.String("q", "", "search") limit := fs.Int("limit", 100, "page size") offset := fs.Int("offset", 0, "offset") if err := fs.Parse(args[1:]); err != nil { return err } query := url.Values{"limit": {strconv.Itoa(*limit)}, "offset": {strconv.Itoa(*offset)}} if *status != "" { query.Set("status", *status) } if *provider != "" { query.Set("provider", *provider) } if *q != "" { query.Set("q", *q) } body, err := c.get(ctx, commerceURL(c, "/api/orders/value-codes")+"?"+query.Encode()) if err != nil { return err } return writeRaw(stdout, body) default: return fmt.Errorf("unknown commerce orders command %q", args[0]) } } func runCommerceValueCodes(ctx context.Context, c *Client, args []string, stdin io.Reader, stdout io.Writer) error { if len(args) == 0 || args[0] == "help" { _, _ = io.WriteString(stdout, "Usage: value-codes definitions get\n value-codes definitions replace --input FILE|-\n") return nil } if args[0] != "definitions" { return fmt.Errorf("unknown value-codes command %q", args[0]) } if len(args) < 2 || args[1] == "get" { body, err := c.get(ctx, commerceURL(c, "/commerce/valuecodes")) if err != nil { return err } return writeRaw(stdout, body) } if args[1] != "replace" { return fmt.Errorf("unknown definitions command %q", args[1]) } body, err := readRequiredBody(stdin, args[2:], "value-code definitions") if err != nil { return err } if !json.Valid(body) { return errors.New("value-code definitions input is not valid JSON") } response, err := c.postBody(ctx, commerceURL(c, "/commerce/valuecodes"), body, "application/json") if err != nil { return err } return writeRaw(stdout, response) } func runCommercePromotions(ctx context.Context, c *Client, args []string, stdin io.Reader, stdout io.Writer) error { if len(args) == 0 || args[0] == "help" { _, _ = io.WriteString(stdout, "Usage: promotions list [--status STATUS]\n promotions get \n promotions upsert --input FILE|-\n promotions status --status STATUS\n promotions delete --yes\n") return nil } switch args[0] { case "list": fs := flag.NewFlagSet("commerce promotions list", flag.ContinueOnError) fs.SetOutput(io.Discard) status := fs.String("status", "", "status") if err := fs.Parse(args[1:]); err != nil { return err } endpoint := commerceURL(c, "/mcp") if *status != "" { endpoint += "?status=" + url.QueryEscape(*status) } return mcpCall(ctx, c, endpoint, "list_promotions", map[string]any{"status": *status}, stdout) case "get": id, err := requireID(args[1:], "commerce promotions get requires an id") if err != nil { return err } return mcpCall(ctx, c, commerceURL(c, "/mcp"), "get_promotion", map[string]string{"id": id}, stdout) case "upsert": body, err := readRequiredBody(stdin, args[1:], "promotion") if err != nil { return err } if !json.Valid(body) { return errors.New("promotion input is not valid JSON") } return mcpCall(ctx, c, commerceURL(c, "/mcp"), "upsert_promotion", map[string]json.RawMessage{"promotion": body}, stdout) case "status": id, err := requireID(args[1:], "commerce promotions status requires an id") if err != nil { return err } fs := flag.NewFlagSet("commerce promotions status", flag.ContinueOnError) fs.SetOutput(io.Discard) status := fs.String("status", "", "status") if err := fs.Parse(args[2:]); err != nil { return err } if *status == "" { return errors.New("promotions status requires --status") } return mcpCall(ctx, c, commerceURL(c, "/mcp"), "set_promotion_status", map[string]string{"id": id, "status": *status}, stdout) case "delete": id, err := requireID(args[1:], "commerce promotions delete requires an id") if err != nil { return err } fs := flag.NewFlagSet("commerce promotions delete", flag.ContinueOnError) fs.SetOutput(io.Discard) yes := fs.Bool("yes", false, "confirm deletion") if err := fs.Parse(args[2:]); err != nil { return err } if !*yes { return errors.New("commerce promotions delete requires --yes") } return mcpCall(ctx, c, commerceURL(c, "/mcp"), "delete_promotion", map[string]string{"id": id}, stdout) default: return fmt.Errorf("unknown commerce promotions command %q", args[0]) } } func mcpCall(ctx context.Context, c *Client, endpoint, name string, arguments any, stdout io.Writer) error { initBody, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{"protocolVersion": "2025-06-18", "capabilities": map[string]any{}, "clientInfo": map[string]string{"name": "cmsctl", "version": "dev"}}}) if err != nil { return err } if _, err = c.postBody(ctx, endpoint, initBody, "application/json"); err != nil { return err } notification := []byte(`{"jsonrpc":"2.0","method":"notifications/initialized"}`) if _, err = c.postBody(ctx, endpoint, notification, "application/json"); err != nil { return err } body, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": "cmsctl", "method": "tools/call", "params": map[string]any{"name": name, "arguments": arguments}}) if err != nil { return err } response, err := c.postBody(ctx, endpoint, body, "application/json") if err != nil { return err } return writeRaw(stdout, response) }