cmsctl and docs
This commit is contained in:
1 parent
fea6eb51b1
commit
93cfa123e4
5 files changed
+316
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user