dsc-rs 0.10.30

Discourse CLI tool for managing multiple Discourse forums: track installs, run upgrades over SSH, manage emojis, sync topics and categories as Markdown, and more.
Documentation
# dsc category

List, pull, push, and copy categories, plus sync category *definitions*.

Two distinct kinds of sync live under `dsc category`:

- **Topic content**`pull`/`push` move the Markdown *topics inside* a category.
- **Category definitions**`def pull`/`def push` and `show`/`get`/`set` manage the category objects themselves (name, colour, permissions, description, topic template, tag rules, ordering). This is the config-as-code counterpart to `dsc tag pull/push` and `dsc setting pull/push`.

## dsc category list

```
dsc category list <discourse> [--format text|json|yaml] [--tree]
```

Lists all categories with their IDs and names.

Flags:

- `--tree` — print categories in a hierarchy, with subcategories indented under parents.

## dsc category pull

```
dsc category pull <discourse> <category-id-or-slug> [<local-path>]
```

Pulls the category into a directory of Markdown files. If `<local-path>` is omitted, writes to a new folder in the current directory (named from the category slug/name). Files are named from topic titles.

Each file gets YAML front matter binding it to its remote topic:

```markdown
---
title: Dependency management
topic_id: 412
url: https://forum.example.com/t/dependency-management/412
pulled_at: 2026-06-22T09:19:00Z
---

[the topic's first post follows here as Markdown]
```

`category push` reads `topic_id` from this block to route updates by ID, so renaming the file or editing the title no longer risks creating a duplicate topic. The front matter is local-only metadata: `dsc` strips it before sending content to Discourse, so the `---` block never reaches the published post. Files without front matter (e.g. ones you author by hand) still work - they fall back to slug/title matching.

## dsc category push

```text
dsc category push [OPTIONS] <discourse> <category-id-or-slug> <local-path>
```

Pushes local Markdown files up to the category, updating existing topics and (by default) creating new ones for files with no remote match. Files are matched by `topic_id` front matter first, falling back to slug/title matching.

The push prints a plan with a sigil per file: `~` (update), `+` (create), `=` (unchanged, skipped). Bodies byte-identical to the remote post (ignoring trailing whitespace) are reported `=` and not re-sent.

Flags:

- `-n`, `--dry-run` — print the plan without writing anything. Run this first and review before pushing for real.
- `--updates-only` — only update existing topics; error with a hint instead of silently creating a new topic when a file has no remote match. Use for curated categories where accidental topic creation must be impossible.
- `--no-bump` — update posts without bumping their topics to the top of the category activity feed (sends `post[no_bump]=true`). Use for silent bulk maintenance edits.
- `--skip-revision` — update posts without recording an edit-history revision (sends `post[skip_revision]=true`). Suppresses the online audit trail; use sparingly.

### Recommended workflow

```text
dsc category pull forum 34 forum-export/      # snapshot (front matter embedded)
# edit files in forum-export/, commit to git for an offline audit trail
dsc category push -n forum 34 forum-export/   # review the plan
dsc category push forum 34 forum-export/      # apply
```

## dsc category copy

```
dsc category copy <source-discourse> <category-id-or-slug> [--target <target-discourse>]
```

Copies the specified category. If `--target` is omitted, copies within the same Discourse.

- The copied category name is set to `Copy of <original category name>`.
- The copied category slug is suffixed with `-copy` (e.g., `staff` -> `staff-copy`).
- All other fields match the source, except the ID which is assigned by Discourse.

`<category-id-or-slug>` can be found using `dsc category list`.

## Category definitions

Version-control a forum's category structure the way `tag pull/push` handles the tag taxonomy.

```
dsc category def pull <discourse> [categories.yaml]   # snapshot every definition to a file
dsc category def push <discourse> <categories.yaml>   # apply the file (upsert; never deletes)
```

- `def pull` writes one `categories.yaml` (or `.json` by extension) holding every category's definition - name, slug, colour, position, parent, `read_restricted`, description, topic template, permissions, tag rules, and display knobs. Usage counts and other volatile fields are dropped so re-pulls diff cleanly.
- `def push` reconciles the server toward the file: it **creates** missing categories and **updates** changed ones, matching by `id` (stable), then `slug`, then `name`. It never deletes. `--dry-run` prints the plan with `+` (create), `~` (update), `=` (unchanged) sigils. A file entry with no `id` that matches nothing is flagged loudly - it would create a new category, so if you meant to rename an existing one, keep its `id` in the file to preserve its topics.
- The push is idempotent: a pull followed by a push with no edits reports every category `= unchanged`.

### Single-field access

For a quick one-field read or edit without rewriting the whole file - mirrors `dsc setting get/set` and `dsc theme show`:

```
dsc category show <discourse> <category>            # all definition fields
dsc category get  <discourse> <category> <field>    # one field
dsc category set  <discourse> <category> <field> <value>
```

- `<category>` resolves by `id`, `slug`, or `name`.
- `<field>` is one of: `name`, `slug`, `color`, `text_color`, `position`, `parent`, `read_restricted`, `description`, `topic_template`, `permissions`, `allowed_tags`, `allowed_tag_groups`, `minimum_required_tags`, `sort_order`, `default_view`, `subcategory_list_style`, `num_featured_topics`, `show_subcategory_list`.
- List fields (`allowed_tags`, `allowed_tag_groups`) take a comma-separated value; an empty value clears the list.
- `permissions` takes `group:level,...` where level is `full`, `create_post`, or `readonly` (e.g. `staff:full`). Granting any group other than `everyone` also sets `read_restricted=true`, matching the admin UI.
- `show`/`get` honour `--format text|json|yaml`; `set` honours the global `--dry-run`.

Notes:

- `description` is read from the plain-text form; on write, Discourse re-cooks it as the category's "About" topic excerpt (settles a moment after a create).
- When `def push` creates a category whose `parent` is itself brand-new in the same file, run the push twice (or create the parent first) - a parent is resolved against categories that already exist on the server.