cmsctl and docs

This commit is contained in:
mats committed 2026-08-22 10:12:22 +02:00
1 parent fea6eb51b1
commit 93cfa123e4
5 files changed
+316

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
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 <id>\n cmsctl todos reopen <id>\n cmsctl todos get <id>\n cmsctl todos create --note '...' [--prio low|medium|high] [--location '...'] [--url '...']\n cmsctl todos delete <id> --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)
}