package main import ( "context" "encoding/json" "errors" "flag" "fmt" "io" "net/url" "strconv" "strings" ) func runTodos(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error { if len(args) == 0 { return runTodosList(ctx, client, nil, stdout) } if args[0] == "help" || args[0] == "--help" || args[0] == "-h" { _, _ = io.WriteString(stdout, "Usage: cmsctl todos [list] [--done=true|false|open|done|all] [--open] [--prio low|medium|high] [--q query] [--limit N] [--offset N]\n cmsctl todos done \n cmsctl todos reopen \n cmsctl todos get \n cmsctl todos create --note '...' [--prio low|medium|high] [--location '...'] [--url '...']\n cmsctl todos delete --yes\n") return nil } // If the first argument is a flag (e.g. `cmsctl todos --open`), treat as `list` if strings.HasPrefix(args[0], "-") { return runTodosList(ctx, client, args, stdout) } switch args[0] { case "list": return runTodosList(ctx, client, args[1:], stdout) case "done", "mark-done": return runTodosSetDone(ctx, client, args[1:], true, stdout) case "reopen", "undone", "open": return runTodosSetDone(ctx, client, args[1:], false, stdout) case "get": id, err := requireID(args[1:], "cmsctl todos get requires a todo id") if err != nil { return err } body, err := client.get(ctx, client.cmsURL("/nodes/"+escapePathSegment(id))) if err != nil { return err } return writeRaw(stdout, body) case "create": return runTodosCreate(ctx, client, args[1:], stdin, stdout) case "delete": return cmsDeleteNode(ctx, client, args[1:], stdout) default: return fmt.Errorf("unknown todos command %q (use --help)", args[0]) } } type doneFlag struct { set bool value string } func (d *doneFlag) String() string { return d.value } func (d *doneFlag) Set(s string) error { d.set = true d.value = strings.TrimSpace(strings.ToLower(s)) return nil } func (d *doneFlag) IsBoolFlag() bool { return true } func runTodosList(ctx context.Context, client *Client, args []string, stdout io.Writer) error { fs := flag.NewFlagSet("todos list", flag.ContinueOnError) var done doneFlag fs.Var(&done, "done", "filter by completion status: true|false, open|done, or all") openOnly := fs.Bool("open", false, "filter to open / uncompleted todos (shorthand for --done=false)") prio := fs.String("prio", "", "priority filter: low, medium, or high") q := fs.String("q", "", "search substring over note/location/url") limit := fs.Int("limit", 100, "page size; use 0 for all") offset := fs.Int("offset", 0, "page offset") fs.SetOutput(io.Discard) if err := fs.Parse(args); err != nil { return err } query := url.Values{ "limit": {strconv.Itoa(*limit)}, "offset": {strconv.Itoa(*offset)}, } if *prio != "" { query.Set("prio", strings.ToLower(*prio)) } if *q != "" { query.Set("q", *q) } if *openOnly { query.Set("done", "false") } else if done.set { switch done.value { case "true", "t", "1", "done", "yes": query.Set("done", "true") case "false", "f", "0", "open", "no": query.Set("done", "false") case "all", "": // omit parameter to fetch all default: return fmt.Errorf("invalid --done value %q: use true, false, open, done, or all", done.value) } } body, err := client.get(ctx, client.cmsURL("/todos")+"?"+query.Encode()) if err != nil { return err } return writeRaw(stdout, body) } func runTodosSetDone(ctx context.Context, client *Client, args []string, done bool, stdout io.Writer) error { id, err := requireID(args, "cmsctl todos done requires a todo id") if err != nil { return err } patch := map[string]any{ "data": map[string]any{ "done": done, }, } body, err := json.Marshal(patch) if err != nil { return err } response, err := client.patchBody(ctx, client.cmsURL("/nodes/"+escapePathSegment(id)), body, "application/json") if err != nil { return err } return writeRaw(stdout, response) } func runTodosCreate(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error { fs := flag.NewFlagSet("todos create", flag.ContinueOnError) note := fs.String("note", "", "improvement note / description") prio := fs.String("prio", "medium", "priority: low, medium, or high") done := fs.Bool("done", false, "initial completion status") location := fs.String("location", "", "admin view title where suggestion was filed") rawURL := fs.String("url", "", "page URL where suggestion was filed") tags := fs.String("tags", "", "comma-separated tags") input := fs.String("input", "", "complete request JSON file or - for stdin") fs.SetOutput(io.Discard) if err := fs.Parse(args); err != nil { return err } body, err := readInput(stdin, *input, fs.Args()) if err != nil { return fmt.Errorf("read todo input: %w", err) } if len(body) == 0 { if strings.TrimSpace(*note) == "" { return errors.New("todos create requires --note") } data := map[string]any{ "note": strings.TrimSpace(*note), "prio": *prio, "done": *done, } if *location != "" { data["location"] = *location } if *rawURL != "" { data["url"] = *rawURL } node := map[string]any{ "dataType": "todo", "data": data, "tags": splitCSV(*tags), } body, err = json.Marshal(node) if err != nil { return err } } if !json.Valid(body) { return errors.New("create todo input is not valid JSON") } response, err := client.postBody(ctx, client.cmsURL("/nodes"), body, "application/json") if err != nil { return err } return writeRaw(stdout, response) }