commerce: add CLI and checkout session lifecycle\n\n🤖 Generated with Codebuff\nCo-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
1 parent
8627f0a26f
commit
cb7967e419
4 files changed
+378
No files matched your search
@@ -13,6 +13,27 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func runCommerce(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error {
|
||||||
|
if len(args) == 0 || args[0] == "help" || args[0] == "--help" {
|
||||||
|
_, _ = io.WriteString(stdout, "Usage: cmsctl commerce promotions {list|get|upsert|status|delete} ...\n cmsctl commerce value-codes definitions {get|replace} ...\n cmsctl commerce carts {get|add|set|remove} ...\n cmsctl commerce checkout create <cart-id> --country SE --terms accepted\n cmsctl commerce orders {get|value-codes} ...\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "carts":
|
||||||
|
return runCommerceCarts(ctx, client, args[1:], stdin, stdout)
|
||||||
|
case "checkout":
|
||||||
|
return runCommerceCheckout(ctx, client, args[1:], stdin, stdout)
|
||||||
|
case "orders":
|
||||||
|
return runCommerceOrders(ctx, client, args[1:], stdout)
|
||||||
|
case "value-codes":
|
||||||
|
return runCommerceValueCodes(ctx, client, args[1:], stdin, stdout)
|
||||||
|
case "promotions":
|
||||||
|
return runCommercePromotions(ctx, client, args[1:], stdin, stdout)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown commerce command %q", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runCMS(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error {
|
func runCMS(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error {
|
||||||
if len(args) == 0 || args[0] == "help" || args[0] == "--help" {
|
if len(args) == 0 || args[0] == "help" || args[0] == "--help" {
|
||||||
_, _ = io.WriteString(stdout, "Usage: cmsctl cms nodes {list|get|create|update|patch|delete|retype} ...\n cmsctl cms edges {get|create|delete|reorder} ...\n cmsctl cms schemas|templates|blueprints|tree|nav|graph|publish|regenerate|script ...\n cmsctl cms search --q text\n cmsctl cms types\n cmsctl cms import [file|-]\n")
|
_, _ = io.WriteString(stdout, "Usage: cmsctl cms nodes {list|get|create|update|patch|delete|retype} ...\n cmsctl cms edges {get|create|delete|reorder} ...\n cmsctl cms schemas|templates|blueprints|tree|nav|graph|publish|regenerate|script ...\n cmsctl cms search --q text\n cmsctl cms types\n cmsctl cms import [file|-]\n")
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
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 <id> --sku SKU [--quantity N]\n carts set <id> --input FILE|-\n carts remove <id> --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 <cart-id> --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 <id>\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 <id>\n promotions upsert --input FILE|-\n promotions status <id> --status STATUS\n promotions delete <id> --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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommerceCartAddUsesBaseURLAndPayload(t *testing.T) {
|
||||||
|
var gotMethod, gotPath, gotBody string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotMethod, gotPath = r.Method, r.URL.Path
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
gotBody = string(b)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = io.WriteString(w, `{ "id": "cart-1", "items": [] }`)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := newClient(Config{BaseURL: server.URL}, "token", "", 0)
|
||||||
|
var out strings.Builder
|
||||||
|
if err := runCommerceCarts(context.Background(), client, []string{"add", "cart-1", "--sku", "ND10200818HB0001", "--quantity", "2"}, strings.NewReader(""), &out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if gotMethod != http.MethodPost || gotPath != "/cart/byid/cart-1" {
|
||||||
|
t.Fatalf("request = %s %s, want POST /cart/byid/cart-1", gotMethod, gotPath)
|
||||||
|
}
|
||||||
|
if gotBody != `{"quantity":2,"sku":"ND10200818HB0001"}` {
|
||||||
|
t.Fatalf("body = %s", gotBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommercePromotionUpsertUsesMCPHandshakeAndToolCall(t *testing.T) {
|
||||||
|
var methods []string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
methods = append(methods, string(body))
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{}}`)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := newClient(Config{BaseURL: server.URL}, "token", "", 0)
|
||||||
|
promotion := `{"id":"promo-test","name":"Test","status":"active","conditions":[],"actions":[]}`
|
||||||
|
var out strings.Builder
|
||||||
|
if err := runCommercePromotions(context.Background(), client, []string{"upsert", "--input", "-"}, strings.NewReader(promotion), &out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(methods) != 3 {
|
||||||
|
t.Fatalf("MCP request count = %d, want 3", len(methods))
|
||||||
|
}
|
||||||
|
if !strings.Contains(methods[0], `"method":"initialize"`) || !strings.Contains(methods[1], `"method":"notifications/initialized"`) || !strings.Contains(methods[2], `"method":"tools/call"`) || !strings.Contains(methods[2], `"name":"upsert_promotion"`) {
|
||||||
|
t.Fatalf("unexpected MCP requests: %v", methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,12 @@ Usage:
|
|||||||
cmsctl finder save-trigger Ask reader to persist state
|
cmsctl finder save-trigger Ask reader to persist state
|
||||||
cmsctl finder import [file|-] Import inventory items (JSON/JSONL)
|
cmsctl finder import [file|-] Import inventory items (JSON/JSONL)
|
||||||
|
|
||||||
|
cmsctl commerce promotions {list|get|upsert|status|delete} ...
|
||||||
|
cmsctl commerce value-codes definitions {get|replace} ...
|
||||||
|
cmsctl commerce carts {get|add|set|remove} ...
|
||||||
|
cmsctl commerce checkout create <cart-id> ...
|
||||||
|
cmsctl commerce orders {get|value-codes} ...
|
||||||
|
|
||||||
All commands accept --config, --base-url, --finder-url, --api-key, --token,
|
All commands accept --config, --base-url, --finder-url, --api-key, --token,
|
||||||
and --timeout before the command. A missing --finder-url uses <base-url>/find
|
and --timeout before the command. A missing --finder-url uses <base-url>/find
|
||||||
for reads. Finder administration always uses <base-url>/catalog/admin.
|
for reads. Finder administration always uses <base-url>/catalog/admin.
|
||||||
@@ -153,6 +159,9 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) erro
|
|||||||
case "finder", "inventory":
|
case "finder", "inventory":
|
||||||
client := newClient(cfg, global.token, global.apiKey, global.timeout)
|
client := newClient(cfg, global.token, global.apiKey, global.timeout)
|
||||||
return runFinder(ctx, client, rest[1:], stdin, stdout)
|
return runFinder(ctx, client, rest[1:], stdin, stdout)
|
||||||
|
case "commerce":
|
||||||
|
client := newClient(cfg, global.token, global.apiKey, global.timeout)
|
||||||
|
return runCommerce(ctx, client, rest[1:], stdin, stdout)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown command %q (use --help)", rest[0])
|
return fmt.Errorf("unknown command %q (use --help)", rest[0])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user