# cmsctl `cmsctl` is a dependency-free Go CLI for the platform's backoffice APIs. It uses the existing RFC 8628 device-login flow and stores the resulting bearer token **together with the instance base URL** in: - `$CMSCTL_CONFIG`, when set - `$XDG_CONFIG_HOME/cmsctl/config.json`, when set - the platform config directory from Go's `os.UserConfigDir()` otherwise: - Unix/Linux: `~/.config/cmsctl/config.json` - macOS: `~/Library/Application Support/cmsctl/config.json` - Windows: `%AppData%\\cmsctl\\config.json` The persisted JSON uses `base_url` for the instance URL and `access_token` for the device token. The file and its parent directory are created with restrictive permissions (`0700`/`0600`). The token is never printed during login; login progress goes to stderr so stdout remains pipe-safe. ## Build and run From `platform/`: ```bash go build -o cmsctl ./cmd/cmsctl go run ./cmd/cmsctl --help ``` The default backoffice URL is `http://localhost:8080`. ## Authenticate ```bash cmsctl auth login --scope read,write # Open the displayed verification URL and approve the code as an admin. cmsctl auth status cmsctl auth token --raw | secret-tool store --label=cmsctl token cmsctl auth logout ``` The token can also be supplied per invocation with `--token` or `CMSCTL_TOKEN`; a minted API key can be supplied with `--api-key` or `CMSCTL_API_KEY`. Global connection flags must appear before the command: ```bash cmsctl --base-url https://shop.example cms nodes list --type page cmsctl --finder-url http://finder:8080 finder search --q drill cmsctl finder import inventory.json ``` When `--finder-url` is omitted, finder reads use `/find`, which is the backoffice proxy. Finder administration always uses `/catalog/admin`. The CLI intentionally has no separate finder-admin URL: the backoffice is the writeradmin boundary. ## Todos and suggestions List, filter, and complete improvement notes filed via the Suggest button or API: ```bash cmsctl todos list # all todos in triage sort (open first) cmsctl todos list --open # open / uncompleted only (--done=false) cmsctl todos list --done # completed only (--done=true) cmsctl todos list --prio high --q "checkout" # filter by priority and search text cmsctl todos done NODE_ID # mark completed (sets data.done = true) cmsctl todos reopen NODE_ID # reopen (sets data.done = false) cmsctl todos get NODE_ID # get todo node cmsctl todos create --note "Fix cart layout" --prio high --location "Commerce ยท Cart" cmsctl todos delete NODE_ID --yes ``` ## CMS ```bash cmsctl cms nodes list --type page --limit 100 cmsctl cms nodes get NODE_ID cmsctl cms search --q "summer sale" --type page cmsctl cms types cmsctl cms nodes create \ --type page \ --data '{"title":"From the CLI"}' \ --tags published,homepage cmsctl cms nodes update NODE_ID \ --data @replacement-data.json \ --tags published ``` `PUT /api/nodes/{id}` replaces the full `data` object. Use `cmsctl cms nodes get` first and merge fields before updating, or use `--input` with a complete `{"data":...,"tags":...}` request body. For a safe partial update, use the native merge endpoint: ```bash cmsctl cms nodes patch NODE_ID --data '{"data":{"title":"New"}}' cmsctl cms nodes delete NODE_ID --yes cmsctl cms nodes retype NODE_ID --type article ``` Graph and editorial operations are available directly: ```bash cmsctl cms edges get NODE_ID cmsctl cms edges create --from PAGE_ID --to BLOCK_ID --type child --sort 0 cmsctl cms edges reorder PAGE_ID --order EDGE_ID_1,EDGE_ID_2 cmsctl cms tree PAGE_ID --depth 3 cmsctl cms nav --data-type page cmsctl cms graph --root PAGE_ID --budget 250 --edges child,ref cmsctl cms publish PAGE_ID OTHER_PAGE_ID cmsctl cms regenerate --type page ``` Schemas and templates can be inspected or edited in development mode. Writes accept a file or `-` on stdin; destructive deletes require `--yes`: ```bash cmsctl cms schemas list cmsctl cms schemas get page > page.schema.json cmsctl cms schemas put page page.schema.json cmsctl cms templates get page.tmpl cat page.tmpl | cmsctl cms templates put page - cmsctl cms blueprints list cmsctl cms blueprints create --blueprint landing --name "New landing" ``` The script command exposes the server-side CMS scripting API for atomic or multi-step operations while retaining pipe-safe JSON output: ```bash cmsctl cms script --script 'result(cms.listNodes({type:"todo",limit:0}))' cat script-request.json | cmsctl cms script --input - ``` Bulk CMS import accepts the native `{nodes,edges}` request shape: ```bash cmsctl cms import data/import.json cat data/import.json | cmsctl cms import - ``` ## Finder and inventory Finder search emits the reader's JSONL response unchanged: product lines are followed by the final search metadata line. This makes it easy to use with `jq`, `awk`, or another streaming consumer. ```bash cmsctl finder search --q drill --limit 20 --sort price_asc | jq -c . cmsctl finder search --q window --filters '{"string":[{"id":"brand","value":["Dala"]}]}' cmsctl finder get 861201 cmsctl finder get ABC-123 # SKU lookup is supported by finder cmsctl finder config --stock in_stock cmsctl finder facets --q drill cmsctl finder facet-list cmsctl finder values brand cmsctl finder suggest --q dr cmsctl finder popular cmsctl finder related 861201 cmsctl finder compatible 861201 --limit 20 cmsctl finder compatible 861201 --cart-input cart-item-ids.json cmsctl finder similar 861201 cmsctl finder relation-groups cmsctl finder facet-groups ``` To materialize a bounded list of catalog items, pipe one numeric ID per line: ```bash printf '861201\\n861202\\n' | cmsctl finder stream-items - ``` `finder search` also exposes the reader's important request controls: `--filters`, `--stock`, `--sort`, `--mode`, `--disable-grouping`, `--skip-facets`, and `--skip-tracking`. `--filters` is the JSON object sent as the reader's `string`/`range` filter fields. Writer administration is explicitly routed through the backoffice writeradmin boundary (`/catalog/admin`), never directly through `--finder-url`: ```bash cmsctl finder admin settings get cmsctl finder admin settings put settings.json cmsctl finder admin facets list cmsctl finder admin facets create facet.json cmsctl finder admin facets update brand facet.json cmsctl finder admin facets delete obsolete --yes cmsctl finder admin update-by-filter update-request.json cmsctl finder admin delete-by-filter filter.json --yes ``` The filter update body follows the writer contract: `{"filter":{"string":[...] ,"range":[...]},"update":{...}}`. Filter operations reject empty filters server-side; deletion also requires `--yes` in the CLI. Inventory/catalog imports accept either the writer's JSON array or one partial item update per JSONL line. The CLI normalizes JSONL to the writer's batch request before sending it to `POST /catalog/admin/items/batch`: ```bash cat inventory.json | cmsctl finder import - cat inventory.jsonl | cmsctl inventory import - jq -c '.[]' products.json | cmsctl finder import - ``` Each finder import item follows the writer contract. For example: ```json {"id":861201,"title":"New title","values":{"2":"Intel"}} {"id":99000001,"buyable":true,"saleStatus":"ACT","values":{"4":50000}} ``` Updates are partial and merged by the finder writer; a `delete: true` item is a catalog deletion. The persisted device token needs write authority for CMS and finder mutations.