clickup-cli 1.7.0__tar.gz → 1.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/CHANGELOG.md +9 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/FEATURE-GAP-ANALYSIS.md +3 -3
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/INTEGRATION.md +7 -2
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/PKG-INFO +11 -7
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/README.md +10 -6
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/__init__.py +1 -1
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/client.py +19 -0
- clickup_cli-1.8.0/src/clickup_cli/commands/backup.py +162 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/folders.py +140 -1
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/lists.py +60 -1
- clickup_cli-1.8.0/src/clickup_cli/commands/tags.py +343 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/tasks.py +2 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/tasks_internal/parser.py +65 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/tasks_internal/read.py +13 -1
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/tasks_internal/write.py +154 -7
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_cli.py +126 -1
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_client.py +97 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_commands_misc.py +229 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_commands_spaces_lists_folders.py +257 -2
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_commands_tasks.py +233 -5
- clickup_cli-1.7.0/src/clickup_cli/commands/tags.py +0 -142
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/agents/api-compatibility-checker.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/agents/test-writer.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/settings.json +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/skills/add-command.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/skills/changelog/SKILL.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/skills/clickup-cli.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/skills/release/SKILL.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.claude/skills/validate-output/SKILL.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.github/workflows/ci.yml +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.gitignore +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/.mcp.json +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/CLAUDE.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/CODE_OF_CONDUCT.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/CONTRIBUTING.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/LICENSE +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/RELEASE.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/SECURITY.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/TROUBLESHOOTING.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/clickup-config.example.json +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/docs/superpowers/README.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/docs/superpowers/plans/2026-04-18-gsd-public-repo-autonomous-rollout.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/docs/superpowers/specs/2026-04-18-gsd-public-repo-autonomous-design.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/docs/superpowers/specs/2026-04-19-core-workflow-parity-design.md +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/pyproject.toml +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/scripts/validate-cli-output.sh +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/__main__.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/cli.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/__init__.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/comments.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/docs.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/fields.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/init.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/privacy.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/spaces.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/task_types.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/tasks_internal/__init__.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/tasks_internal/shared.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/commands/team.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/config.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/helpers.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/src/clickup_cli/runtime.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/command_fakes.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/conftest.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_command_manifest.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_commands_docs_comments.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_commands_metadata.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_config.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_helpers.py +0 -0
- {clickup_cli-1.7.0 → clickup_cli-1.8.0}/tests/test_tasks_facade.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 1.8.0 (2026-04-26)
|
|
6
|
+
|
|
7
|
+
- **Task output shaping and tagging workflows expanded.** `tasks get` now accepts `--fields` and `--full` alongside comment hydration flags, and `tasks create` supports repeatable `--tag` values applied after creation.
|
|
8
|
+
- **Migration and safety workflows are broader.** New `tasks bulk move` and `tasks bulk tags` commands provide dry-run-friendly bulk operations with resume-oriented failure output, while `lists backup`, `folders backup`, enriched delete dry-runs, and `folders purge-empty` add safer hierarchy cleanup paths.
|
|
9
|
+
- **Space tag management is now first-class.** `tags create`, `tags delete`, and `tags usage` cover Space-level tag lifecycle and usage auditing, including archived-list coverage fixes.
|
|
10
|
+
- **API resilience and destructive safety were tightened.** Safe GET requests retry transient 502/503/504 responses without changing 429 behavior, and bulk/tag/destructive validation now rejects unsafe or empty plans earlier.
|
|
11
|
+
|
|
3
12
|
## 1.7.0 (2026-04-20)
|
|
4
13
|
|
|
5
14
|
- **Metadata discovery and custom-field lookup are easier to drive from the CLI.** New discovery flows cover field and task-type metadata, and task search can now target custom fields directly instead of forcing clients to pre-resolve everything out of band.
|
|
@@ -22,11 +22,11 @@ This document identifies the largest feature gaps between the current `clickup-c
|
|
|
22
22
|
|
|
23
23
|
The repo already covers a solid core developer workflow surface:
|
|
24
24
|
|
|
25
|
-
- Tasks: `list`, `get`, `create`, `update`, `search`, `delete`, `move`, `merge`, `depend`, linked-task management, multi-list operations
|
|
25
|
+
- Tasks: `list`, `get`, `create`, `update`, `search`, `delete`, `move`, `merge`, `bulk`, `depend`, linked-task management, multi-list operations, output shaping, create-time tags
|
|
26
26
|
- Comments: task comments only via `list`, `add`, `update`, `delete`, `thread`, `reply`
|
|
27
27
|
- Docs/pages: doc list/get/create, page list/get/create/edit
|
|
28
|
-
- Hierarchy: spaces/folders/lists CRUD, statuses, privacy toggles
|
|
29
|
-
- Metadata/admin-lite: fields, task types,
|
|
28
|
+
- Hierarchy: spaces/folders/lists CRUD, statuses, privacy toggles, local backups, guarded empty-folder purge
|
|
29
|
+
- Metadata/admin-lite: fields, task types, Space tag lifecycle and usage audit, task tag add/remove, `whoami`, workspace members, bootstrap/init
|
|
30
30
|
|
|
31
31
|
Depth is strongest in tasks and docs/pages. The CLI already mixes v2 and v3 pragmatically, which is a good foundation for further expansion.
|
|
32
32
|
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
## Output Modes
|
|
13
13
|
|
|
14
14
|
- Default task list and search output is compact
|
|
15
|
-
- `--fields` returns only the requested fields
|
|
16
|
-
- `--full` returns full task objects with normalized status metadata
|
|
15
|
+
- `--fields` returns only the requested fields on task list/search/get flows
|
|
16
|
+
- `--full` returns full task objects with normalized status metadata where applicable
|
|
17
17
|
|
|
18
18
|
## Bounded Defaults
|
|
19
19
|
|
|
@@ -32,4 +32,9 @@
|
|
|
32
32
|
|
|
33
33
|
- Validate response metadata instead of assuming a default scan is exhaustive
|
|
34
34
|
- Use `--dry-run` before automating mutations in a new environment
|
|
35
|
+
- Use `tasks bulk ...` and backup commands for migration workflows that need resumable failure output or local JSON snapshots
|
|
35
36
|
- Treat doc IDs and page IDs as different values
|
|
37
|
+
|
|
38
|
+
## Reliability
|
|
39
|
+
|
|
40
|
+
- Safe GET requests retry transient 502/503/504 responses; 429 rate-limit handling remains explicit and separate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clickup-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: The missing ClickUp CLI. Built for developers and AI agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/efetoker/clickup-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/efetoker/clickup-cli
|
|
@@ -111,15 +111,15 @@ Output is JSON on stdout; errors go to stderr.
|
|
|
111
111
|
| Group | Subcommands | Description |
|
|
112
112
|
|-------|-------------|-------------|
|
|
113
113
|
| `init` | — | Interactive workspace setup |
|
|
114
|
-
| `tasks` | list, get, create, update, search, delete, move, merge, lists, add-to-list, remove-from-list, depend, link | Full task CRUD + memberships, links, and
|
|
114
|
+
| `tasks` | list, get, create, update, search, delete, move, merge, bulk, lists, add-to-list, remove-from-list, depend, link | Full task CRUD + memberships, links, dependencies, and migration-safe bulk operations |
|
|
115
115
|
| `comments` | list, add, update, delete, thread, reply | Full comment CRUD with threading |
|
|
116
116
|
| `docs` | list, get, create, pages, get-page, edit-page, create-page | Docs and page management |
|
|
117
117
|
| `fields` | list | Custom field discovery for list or space scope |
|
|
118
|
-
| `folders` | list, get, create, update, delete, privacy | Folder CRUD + privacy toggle |
|
|
119
|
-
| `lists` | list, get, create, update, delete, privacy | List CRUD + privacy toggle |
|
|
118
|
+
| `folders` | list, get, create, update, delete, backup, purge-empty, privacy | Folder CRUD + backups, guarded empty-folder purge, and privacy toggle |
|
|
119
|
+
| `lists` | list, get, create, update, delete, backup, privacy | List CRUD + backups and privacy toggle |
|
|
120
120
|
| `spaces` | list, get, create, update, delete, statuses, privacy | Full space CRUD + statuses + privacy toggle |
|
|
121
121
|
| `team` | whoami, members | Workspace and member info |
|
|
122
|
-
| `tags` | list, add, remove |
|
|
122
|
+
| `tags` | list, create, delete, usage, add, remove | Space tag lifecycle, usage audit, and task tag management |
|
|
123
123
|
| `task-types` | list | Workspace custom task type discovery |
|
|
124
124
|
|
|
125
125
|
Use `clickup <group> <command> --help` for detailed usage, examples, and return format.
|
|
@@ -144,7 +144,7 @@ clickup tasks list --space <name> --pretty
|
|
|
144
144
|
|
|
145
145
|
- **Flag aliases** — every positional argument also accepts a `--flag` form. `tasks get abc123` and `tasks get --task-id abc123` are equivalent. Same for `--query`, `--doc-id`, `--page-id`, `--folder-id`, `--list-id`, `--comment-id`, `--space`.
|
|
146
146
|
- **Raw numeric IDs** on `--space` and `--folder` flags are accepted transparently alongside config aliases. On tasks commands, list-bound commands may resolve a raw space ID to its first folderless list via one API call.
|
|
147
|
-
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided, and create also supports start/due dates, time estimates, points, repeatable `--custom-field` values, and `--task-type`.
|
|
147
|
+
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided, and create also supports start/due dates, time estimates, points, repeatable `--custom-field` values, repeatable `--tag` values, and `--task-type`.
|
|
148
148
|
- **`tasks update`** handles core fields, assignee diffs (`--add-assignee` / `--remove-assignee`), tag diffs (`--add-tag` / `--remove-tag`), and custom fields (`--custom-field FIELD_ID=VALUE`) in one call. All flags are repeatable; `--dry-run` returns a structured plan.
|
|
149
149
|
- **`tasks lists` / `tasks add-to-list` / `tasks remove-from-list`** let you inspect and manage multi-list task membership separately from home-list moves.
|
|
150
150
|
- **`tasks link`** manages non-blocking linked-task relationships; use `tasks depend` for blocking dependencies.
|
|
@@ -153,8 +153,12 @@ clickup tasks list --space <name> --pretty
|
|
|
153
153
|
- **`tasks list` / `tasks search --include-archived`** — second paginated call with `archived=true`, merged into the default results. Since ClickUp's `archived` param is a filter, this is the only way to see both in one command.
|
|
154
154
|
- **`tasks list` / `tasks search` bounded defaults** — by default, these commands fetch a bounded aggregate scan and return `pages_fetched`, `results_complete`, and `results_truncated`. Use `--all-pages` for an exhaustive scan.
|
|
155
155
|
- **`tasks list --full` / `tasks search --full`** return full task objects with a normalized `status` dict shape (`{status, color, type, orderindex}`), not just the compact projection.
|
|
156
|
-
- **`tasks get`** auto-fetches comments and appends them to the output. By default this is a bounded slice and the response includes `comment_count_returned`, `comments_complete`, and `comments_truncated`. Use `--all-comments` for exhaustive comment hydration or `--no-comments` to skip it.
|
|
156
|
+
- **`tasks get`** auto-fetches comments and appends them to the output. By default this is a bounded slice and the response includes `comment_count_returned`, `comments_complete`, and `comments_truncated`. Use `--fields` to select specific task fields, `--full` to request the full task payload explicitly, `--all-comments` for exhaustive comment hydration, or `--no-comments` to skip it.
|
|
157
157
|
- **`tasks search`** auto-detects task ID patterns like `PROJ-39` and applies prefix filtering.
|
|
158
|
+
- **`tasks bulk move` / `tasks bulk tags`** provide dry-run-friendly batch migration flows with resume-oriented failure details.
|
|
159
|
+
- **`lists backup` / `folders backup`** write local JSON backups with safety-first defaults before migration or deletion; `folders purge-empty` only deletes after exhaustive empty-folder proof.
|
|
160
|
+
- **Space tag lifecycle** — `tags create`, `tags delete`, and `tags usage` manage and audit Space-level tags; task-level `tags add` / `tags remove` remain available.
|
|
161
|
+
- **GET retries** — transient ClickUp 502/503/504 responses are retried only for safe GET requests; 429 rate-limit handling remains separate.
|
|
158
162
|
- **`docs edit-page --append`** reads the current page content, appends your new content, and sends one update.
|
|
159
163
|
- **Tag names** are auto-lowercased (ClickUp API stores them lowercase regardless of UI display).
|
|
160
164
|
- **Doc ID ≠ page ID.** Always use `docs pages <doc_id>` to discover page IDs before using `get-page` or `edit-page`.
|
|
@@ -79,15 +79,15 @@ Output is JSON on stdout; errors go to stderr.
|
|
|
79
79
|
| Group | Subcommands | Description |
|
|
80
80
|
|-------|-------------|-------------|
|
|
81
81
|
| `init` | — | Interactive workspace setup |
|
|
82
|
-
| `tasks` | list, get, create, update, search, delete, move, merge, lists, add-to-list, remove-from-list, depend, link | Full task CRUD + memberships, links, and
|
|
82
|
+
| `tasks` | list, get, create, update, search, delete, move, merge, bulk, lists, add-to-list, remove-from-list, depend, link | Full task CRUD + memberships, links, dependencies, and migration-safe bulk operations |
|
|
83
83
|
| `comments` | list, add, update, delete, thread, reply | Full comment CRUD with threading |
|
|
84
84
|
| `docs` | list, get, create, pages, get-page, edit-page, create-page | Docs and page management |
|
|
85
85
|
| `fields` | list | Custom field discovery for list or space scope |
|
|
86
|
-
| `folders` | list, get, create, update, delete, privacy | Folder CRUD + privacy toggle |
|
|
87
|
-
| `lists` | list, get, create, update, delete, privacy | List CRUD + privacy toggle |
|
|
86
|
+
| `folders` | list, get, create, update, delete, backup, purge-empty, privacy | Folder CRUD + backups, guarded empty-folder purge, and privacy toggle |
|
|
87
|
+
| `lists` | list, get, create, update, delete, backup, privacy | List CRUD + backups and privacy toggle |
|
|
88
88
|
| `spaces` | list, get, create, update, delete, statuses, privacy | Full space CRUD + statuses + privacy toggle |
|
|
89
89
|
| `team` | whoami, members | Workspace and member info |
|
|
90
|
-
| `tags` | list, add, remove |
|
|
90
|
+
| `tags` | list, create, delete, usage, add, remove | Space tag lifecycle, usage audit, and task tag management |
|
|
91
91
|
| `task-types` | list | Workspace custom task type discovery |
|
|
92
92
|
|
|
93
93
|
Use `clickup <group> <command> --help` for detailed usage, examples, and return format.
|
|
@@ -112,7 +112,7 @@ clickup tasks list --space <name> --pretty
|
|
|
112
112
|
|
|
113
113
|
- **Flag aliases** — every positional argument also accepts a `--flag` form. `tasks get abc123` and `tasks get --task-id abc123` are equivalent. Same for `--query`, `--doc-id`, `--page-id`, `--folder-id`, `--list-id`, `--comment-id`, `--space`.
|
|
114
114
|
- **Raw numeric IDs** on `--space` and `--folder` flags are accepted transparently alongside config aliases. On tasks commands, list-bound commands may resolve a raw space ID to its first folderless list via one API call.
|
|
115
|
-
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided, and create also supports start/due dates, time estimates, points, repeatable `--custom-field` values, and `--task-type`.
|
|
115
|
+
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided, and create also supports start/due dates, time estimates, points, repeatable `--custom-field` values, repeatable `--tag` values, and `--task-type`.
|
|
116
116
|
- **`tasks update`** handles core fields, assignee diffs (`--add-assignee` / `--remove-assignee`), tag diffs (`--add-tag` / `--remove-tag`), and custom fields (`--custom-field FIELD_ID=VALUE`) in one call. All flags are repeatable; `--dry-run` returns a structured plan.
|
|
117
117
|
- **`tasks lists` / `tasks add-to-list` / `tasks remove-from-list`** let you inspect and manage multi-list task membership separately from home-list moves.
|
|
118
118
|
- **`tasks link`** manages non-blocking linked-task relationships; use `tasks depend` for blocking dependencies.
|
|
@@ -121,8 +121,12 @@ clickup tasks list --space <name> --pretty
|
|
|
121
121
|
- **`tasks list` / `tasks search --include-archived`** — second paginated call with `archived=true`, merged into the default results. Since ClickUp's `archived` param is a filter, this is the only way to see both in one command.
|
|
122
122
|
- **`tasks list` / `tasks search` bounded defaults** — by default, these commands fetch a bounded aggregate scan and return `pages_fetched`, `results_complete`, and `results_truncated`. Use `--all-pages` for an exhaustive scan.
|
|
123
123
|
- **`tasks list --full` / `tasks search --full`** return full task objects with a normalized `status` dict shape (`{status, color, type, orderindex}`), not just the compact projection.
|
|
124
|
-
- **`tasks get`** auto-fetches comments and appends them to the output. By default this is a bounded slice and the response includes `comment_count_returned`, `comments_complete`, and `comments_truncated`. Use `--all-comments` for exhaustive comment hydration or `--no-comments` to skip it.
|
|
124
|
+
- **`tasks get`** auto-fetches comments and appends them to the output. By default this is a bounded slice and the response includes `comment_count_returned`, `comments_complete`, and `comments_truncated`. Use `--fields` to select specific task fields, `--full` to request the full task payload explicitly, `--all-comments` for exhaustive comment hydration, or `--no-comments` to skip it.
|
|
125
125
|
- **`tasks search`** auto-detects task ID patterns like `PROJ-39` and applies prefix filtering.
|
|
126
|
+
- **`tasks bulk move` / `tasks bulk tags`** provide dry-run-friendly batch migration flows with resume-oriented failure details.
|
|
127
|
+
- **`lists backup` / `folders backup`** write local JSON backups with safety-first defaults before migration or deletion; `folders purge-empty` only deletes after exhaustive empty-folder proof.
|
|
128
|
+
- **Space tag lifecycle** — `tags create`, `tags delete`, and `tags usage` manage and audit Space-level tags; task-level `tags add` / `tags remove` remain available.
|
|
129
|
+
- **GET retries** — transient ClickUp 502/503/504 responses are retried only for safe GET requests; 429 rate-limit handling remains separate.
|
|
126
130
|
- **`docs edit-page --append`** reads the current page content, appends your new content, and sends one update.
|
|
127
131
|
- **Tag names** are auto-lowercased (ClickUp API stores them lowercase regardless of UI display).
|
|
128
132
|
- **Doc ID ≠ page ID.** Always use `docs pages <doc_id>` to discover page IDs before using `get-page` or `edit-page`.
|
|
@@ -13,6 +13,10 @@ from .runtime import RuntimeContext
|
|
|
13
13
|
class ClickUpClient:
|
|
14
14
|
BASE_V2 = "https://api.clickup.com/api/v2"
|
|
15
15
|
BASE_V3 = "https://api.clickup.com/api/v3"
|
|
16
|
+
TRANSIENT_RETRY_STATUSES = {502, 503, 504}
|
|
17
|
+
TRANSIENT_RETRY_METHODS = {"GET"}
|
|
18
|
+
TRANSIENT_RETRY_ATTEMPTS = 3
|
|
19
|
+
TRANSIENT_RETRY_BACKOFF_SECONDS = 0.25
|
|
16
20
|
|
|
17
21
|
def __init__(self, token, dry_run=False, debug=False, runtime=None):
|
|
18
22
|
self.token = token
|
|
@@ -72,6 +76,21 @@ class ClickUpClient:
|
|
|
72
76
|
time.sleep(wait)
|
|
73
77
|
response = _do_request()
|
|
74
78
|
|
|
79
|
+
attempt = 1
|
|
80
|
+
while (
|
|
81
|
+
method in self.TRANSIENT_RETRY_METHODS
|
|
82
|
+
and response.status_code in self.TRANSIENT_RETRY_STATUSES
|
|
83
|
+
and attempt < self.TRANSIENT_RETRY_ATTEMPTS
|
|
84
|
+
):
|
|
85
|
+
attempt += 1
|
|
86
|
+
self._log(
|
|
87
|
+
f" retrying transient {response.status_code} "
|
|
88
|
+
f"(attempt {attempt}/{self.TRANSIENT_RETRY_ATTEMPTS})"
|
|
89
|
+
)
|
|
90
|
+
time.sleep(self.TRANSIENT_RETRY_BACKOFF_SECONDS)
|
|
91
|
+
response = _do_request()
|
|
92
|
+
self._log(f" → {response.status_code} ({len(response.text)} bytes)")
|
|
93
|
+
|
|
75
94
|
if response.status_code == 401:
|
|
76
95
|
error(
|
|
77
96
|
"Authentication failed — check your API token"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Shared backup and destructive-safety helpers for list/folder commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..helpers import fetch_all_comments
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def backup_options(args):
|
|
10
|
+
"""Return safety-first backup options from argparse flags."""
|
|
11
|
+
return {
|
|
12
|
+
"include_closed": not getattr(args, "no_closed", False),
|
|
13
|
+
"include_archived": not getattr(args, "no_archived", False),
|
|
14
|
+
"subtasks": not getattr(args, "no_subtasks", False),
|
|
15
|
+
"all_pages": not getattr(args, "first_page", False),
|
|
16
|
+
"comments": not getattr(args, "no_comments", False),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def scan_list_tasks(client, list_id, options):
|
|
21
|
+
"""Scan task summaries for a list using explicit completeness metadata."""
|
|
22
|
+
tasks = []
|
|
23
|
+
pages_fetched = 0
|
|
24
|
+
complete = True
|
|
25
|
+
passes = [{"archived": "false"}]
|
|
26
|
+
if options["include_archived"]:
|
|
27
|
+
passes.append({"archived": "true"})
|
|
28
|
+
|
|
29
|
+
for pass_params in passes:
|
|
30
|
+
page = 0
|
|
31
|
+
while True:
|
|
32
|
+
params = dict(pass_params)
|
|
33
|
+
params["page"] = str(page)
|
|
34
|
+
if options["include_closed"]:
|
|
35
|
+
params["include_closed"] = "true"
|
|
36
|
+
if options["subtasks"]:
|
|
37
|
+
params["subtasks"] = "true"
|
|
38
|
+
|
|
39
|
+
response = client.get_v2(
|
|
40
|
+
f"/list/{list_id}/task", params=params, allow_dry_run=True
|
|
41
|
+
)
|
|
42
|
+
pages_fetched += 1
|
|
43
|
+
tasks.extend(response.get("tasks", []))
|
|
44
|
+
|
|
45
|
+
if response.get("last_page", False):
|
|
46
|
+
break
|
|
47
|
+
if not options["all_pages"]:
|
|
48
|
+
complete = False
|
|
49
|
+
break
|
|
50
|
+
page += 1
|
|
51
|
+
|
|
52
|
+
deduped = list({task.get("id"): task for task in tasks if task.get("id")}.values())
|
|
53
|
+
return {
|
|
54
|
+
"tasks": deduped,
|
|
55
|
+
"task_ids": [task["id"] for task in deduped],
|
|
56
|
+
"count": len(deduped),
|
|
57
|
+
"pages_fetched": pages_fetched,
|
|
58
|
+
"complete": complete,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def count_list_tasks(client, list_id):
|
|
63
|
+
"""Count all active, closed, archived, and subtask items for safety checks."""
|
|
64
|
+
options = {
|
|
65
|
+
"include_closed": True,
|
|
66
|
+
"include_archived": True,
|
|
67
|
+
"subtasks": True,
|
|
68
|
+
"all_pages": True,
|
|
69
|
+
"comments": False,
|
|
70
|
+
}
|
|
71
|
+
scan = scan_list_tasks(client, list_id, options)
|
|
72
|
+
return {
|
|
73
|
+
"total": scan["count"],
|
|
74
|
+
"task_ids": scan["task_ids"],
|
|
75
|
+
"pages_fetched": scan["pages_fetched"],
|
|
76
|
+
"complete": scan["complete"],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write_json(path, payload):
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def backup_list(client, list_id, output_dir, options, *, prefix=""):
|
|
86
|
+
"""Write a deterministic backup for one list and return manifest details."""
|
|
87
|
+
root = Path(output_dir)
|
|
88
|
+
list_meta = client.get_v2(f"/list/{list_id}", allow_dry_run=True)
|
|
89
|
+
scan = scan_list_tasks(client, list_id, options)
|
|
90
|
+
|
|
91
|
+
files = []
|
|
92
|
+
list_file = root / "list.json"
|
|
93
|
+
tasks_file = root / "tasks.json"
|
|
94
|
+
write_json(list_file, list_meta)
|
|
95
|
+
write_json(tasks_file, scan["tasks"])
|
|
96
|
+
files.extend([f"{prefix}list.json", f"{prefix}tasks.json"])
|
|
97
|
+
|
|
98
|
+
comments_complete = True
|
|
99
|
+
for task_id in scan["task_ids"]:
|
|
100
|
+
task = client.get_v2(f"/task/{task_id}", allow_dry_run=True)
|
|
101
|
+
if options["comments"]:
|
|
102
|
+
comment_result = fetch_all_comments(client, task_id, all_pages=True)
|
|
103
|
+
task["comments"] = comment_result["comments"]
|
|
104
|
+
task["comments_complete"] = comment_result["complete"]
|
|
105
|
+
comments_complete = comments_complete and comment_result["complete"]
|
|
106
|
+
task_file = root / "tasks" / f"{task_id}.json"
|
|
107
|
+
write_json(task_file, task)
|
|
108
|
+
files.append(f"{prefix}tasks/{task_id}.json")
|
|
109
|
+
|
|
110
|
+
manifest = {
|
|
111
|
+
"type": "list_backup",
|
|
112
|
+
"list_id": list_id,
|
|
113
|
+
"task_ids": scan["task_ids"],
|
|
114
|
+
"task_count": scan["count"],
|
|
115
|
+
"options": options,
|
|
116
|
+
"complete": scan["complete"] and comments_complete,
|
|
117
|
+
"tasks_complete": scan["complete"],
|
|
118
|
+
"comments_complete": comments_complete,
|
|
119
|
+
"pages_fetched": scan["pages_fetched"],
|
|
120
|
+
"files": files,
|
|
121
|
+
}
|
|
122
|
+
write_json(root / "manifest.json", manifest)
|
|
123
|
+
return manifest
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def count_folder_tasks(client, folder):
|
|
127
|
+
"""Count all tasks in every child list included by folder metadata."""
|
|
128
|
+
list_counts = []
|
|
129
|
+
total = 0
|
|
130
|
+
task_ids = []
|
|
131
|
+
complete = True
|
|
132
|
+
pages_fetched = 0
|
|
133
|
+
for child_list in folder_child_lists(client, folder, include_archived=True):
|
|
134
|
+
count = count_list_tasks(client, child_list["id"])
|
|
135
|
+
list_counts.append({"list_id": child_list["id"], **count})
|
|
136
|
+
total += count["total"]
|
|
137
|
+
task_ids.extend(count["task_ids"])
|
|
138
|
+
complete = complete and count["complete"]
|
|
139
|
+
pages_fetched += count["pages_fetched"]
|
|
140
|
+
return {
|
|
141
|
+
"total": total,
|
|
142
|
+
"task_ids": task_ids,
|
|
143
|
+
"lists": list_counts,
|
|
144
|
+
"pages_fetched": pages_fetched,
|
|
145
|
+
"complete": complete,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def folder_child_lists(client, folder, include_archived=False):
|
|
150
|
+
"""Return folder child lists, optionally adding archived lists not in metadata."""
|
|
151
|
+
child_lists = list(folder.get("lists", []))
|
|
152
|
+
if include_archived and folder.get("id"):
|
|
153
|
+
archived_response = client.get_v2(
|
|
154
|
+
f"/folder/{folder['id']}/list",
|
|
155
|
+
params={"archived": "true"},
|
|
156
|
+
allow_dry_run=True,
|
|
157
|
+
)
|
|
158
|
+
child_lists.extend(archived_response.get("lists", []))
|
|
159
|
+
deduped = {}
|
|
160
|
+
for child_list in child_lists:
|
|
161
|
+
deduped.setdefault(child_list["id"], child_list)
|
|
162
|
+
return list(deduped.values())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Folder command handlers — list, get, create, update, delete, privacy."""
|
|
2
2
|
|
|
3
3
|
from ..helpers import error, resolve_space_id, add_id_argument
|
|
4
|
+
from .backup import backup_list, backup_options, count_folder_tasks, folder_child_lists, write_json
|
|
4
5
|
from .privacy import handle_privacy_request, register_privacy_subcommand
|
|
5
6
|
|
|
6
7
|
|
|
@@ -22,6 +23,8 @@ Subcommands:
|
|
|
22
23
|
create — create a new folder in a space (mutating)
|
|
23
24
|
update — update a folder's name (mutating)
|
|
24
25
|
delete — delete a folder (destructive)
|
|
26
|
+
backup — export folder/list metadata and tasks to local JSON files
|
|
27
|
+
purge-empty — delete only if exhaustive scans prove the folder is empty
|
|
25
28
|
privacy — make a folder private or public (mutating)
|
|
26
29
|
|
|
27
30
|
Does not cover: reordering folders or setting folder-level statuses
|
|
@@ -158,6 +161,54 @@ examples:
|
|
|
158
161
|
)
|
|
159
162
|
add_id_argument(fd, "folder_id", "ClickUp folder ID to delete")
|
|
160
163
|
|
|
164
|
+
fb = folders_sub.add_parser(
|
|
165
|
+
"backup",
|
|
166
|
+
formatter_class=F,
|
|
167
|
+
help="Back up a folder and child lists to local JSON files",
|
|
168
|
+
description="""\
|
|
169
|
+
Back up a folder before migration or deletion. Writes folder metadata,
|
|
170
|
+
per-list backups, per-task full JSON, and a deterministic manifest.json to
|
|
171
|
+
--output-dir.
|
|
172
|
+
|
|
173
|
+
Defaults are safety-first: include closed tasks, archived tasks, subtasks,
|
|
174
|
+
all task pages, and all comments unless explicitly disabled.""",
|
|
175
|
+
epilog="""\
|
|
176
|
+
returns:
|
|
177
|
+
{"status": "ok", "action": "backup_folder", "folder_id": "...", ...}
|
|
178
|
+
|
|
179
|
+
examples:
|
|
180
|
+
clickup folders backup 12345 --output-dir ./backup/folder-12345
|
|
181
|
+
clickup folders backup --folder-id 12345 --output-dir ./backup --no-comments
|
|
182
|
+
|
|
183
|
+
notes:
|
|
184
|
+
This command writes local files and does not mutate ClickUp.""",
|
|
185
|
+
)
|
|
186
|
+
add_id_argument(fb, "folder_id", "ClickUp folder ID to back up")
|
|
187
|
+
fb.add_argument("--output-dir", required=True, help="Directory for backup JSON files")
|
|
188
|
+
fb.add_argument("--no-closed", action="store_true", help="Do not include closed tasks")
|
|
189
|
+
fb.add_argument("--no-archived", action="store_true", help="Do not include archived tasks")
|
|
190
|
+
fb.add_argument("--no-subtasks", action="store_true", help="Do not include subtasks")
|
|
191
|
+
fb.add_argument("--first-page", action="store_true", help="Only fetch the first task page")
|
|
192
|
+
fb.add_argument("--no-comments", action="store_true", help="Do not hydrate task comments")
|
|
193
|
+
|
|
194
|
+
fpe = folders_sub.add_parser(
|
|
195
|
+
"purge-empty",
|
|
196
|
+
formatter_class=F,
|
|
197
|
+
help="Delete a folder only after proving every child list is empty",
|
|
198
|
+
description="""\
|
|
199
|
+
Exhaustively scans every child list for active, closed, archived, and subtask
|
|
200
|
+
items. The folder is deleted only when all scans are complete and zero tasks
|
|
201
|
+
are found. Use --dry-run to preview the proof without deleting.""",
|
|
202
|
+
epilog="""\
|
|
203
|
+
returns:
|
|
204
|
+
{"status": "ok", "action": "purged_empty_folder", "folder_id": "..."}
|
|
205
|
+
|
|
206
|
+
examples:
|
|
207
|
+
clickup --dry-run folders purge-empty 12345
|
|
208
|
+
clickup folders purge-empty 12345""",
|
|
209
|
+
)
|
|
210
|
+
add_id_argument(fpe, "folder_id", "ClickUp folder ID to purge if empty")
|
|
211
|
+
|
|
161
212
|
register_privacy_subcommand(
|
|
162
213
|
folders_sub,
|
|
163
214
|
F,
|
|
@@ -230,12 +281,98 @@ def cmd_folders_update(client, args):
|
|
|
230
281
|
def cmd_folders_delete(client, args):
|
|
231
282
|
"""Delete a folder by ID."""
|
|
232
283
|
if client.dry_run:
|
|
233
|
-
|
|
284
|
+
folder = client.get_v2(f"/folder/{args.folder_id}", allow_dry_run=True)
|
|
285
|
+
task_counts = count_folder_tasks(client, folder)
|
|
286
|
+
return {
|
|
287
|
+
"dry_run": True,
|
|
288
|
+
"action": "delete_folder",
|
|
289
|
+
"folder_id": args.folder_id,
|
|
290
|
+
"folder": folder,
|
|
291
|
+
"lists": folder.get("lists", []),
|
|
292
|
+
"task_counts": task_counts,
|
|
293
|
+
}
|
|
234
294
|
|
|
235
295
|
client.delete_v2(f"/folder/{args.folder_id}")
|
|
236
296
|
return {"status": "ok", "action": "deleted", "folder_id": args.folder_id}
|
|
237
297
|
|
|
238
298
|
|
|
299
|
+
def cmd_folders_backup(client, args):
|
|
300
|
+
"""Back up a folder and its child lists to local JSON files."""
|
|
301
|
+
from pathlib import Path
|
|
302
|
+
|
|
303
|
+
root = Path(args.output_dir)
|
|
304
|
+
options = backup_options(args)
|
|
305
|
+
folder = client.get_v2(f"/folder/{args.folder_id}", allow_dry_run=True)
|
|
306
|
+
write_json(root / "folder.json", folder)
|
|
307
|
+
|
|
308
|
+
files = ["folder.json"]
|
|
309
|
+
list_ids = []
|
|
310
|
+
task_ids = []
|
|
311
|
+
task_count = 0
|
|
312
|
+
complete = True
|
|
313
|
+
list_manifests = []
|
|
314
|
+
for child_list in folder_child_lists(client, folder, include_archived=options["include_archived"]):
|
|
315
|
+
list_id = child_list["id"]
|
|
316
|
+
list_ids.append(list_id)
|
|
317
|
+
list_manifest = backup_list(
|
|
318
|
+
client,
|
|
319
|
+
list_id,
|
|
320
|
+
root / "lists" / list_id,
|
|
321
|
+
options,
|
|
322
|
+
prefix=f"lists/{list_id}/",
|
|
323
|
+
)
|
|
324
|
+
list_manifests.append(list_manifest)
|
|
325
|
+
task_ids.extend(list_manifest["task_ids"])
|
|
326
|
+
task_count += list_manifest["task_count"]
|
|
327
|
+
complete = complete and list_manifest["complete"]
|
|
328
|
+
files.extend(list_manifest["files"])
|
|
329
|
+
|
|
330
|
+
manifest = {
|
|
331
|
+
"type": "folder_backup",
|
|
332
|
+
"folder_id": args.folder_id,
|
|
333
|
+
"list_ids": list_ids,
|
|
334
|
+
"task_ids": task_ids,
|
|
335
|
+
"task_count": task_count,
|
|
336
|
+
"options": options,
|
|
337
|
+
"complete": complete,
|
|
338
|
+
"lists": list_manifests,
|
|
339
|
+
"files": files,
|
|
340
|
+
}
|
|
341
|
+
write_json(root / "manifest.json", manifest)
|
|
342
|
+
return {
|
|
343
|
+
"status": "ok",
|
|
344
|
+
"action": "backup_folder",
|
|
345
|
+
"folder_id": args.folder_id,
|
|
346
|
+
"output_dir": args.output_dir,
|
|
347
|
+
"manifest": "manifest.json",
|
|
348
|
+
"task_count": task_count,
|
|
349
|
+
"complete": complete,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def cmd_folders_purge_empty(client, args):
|
|
354
|
+
"""Delete a folder only after exhaustive scans prove it is empty."""
|
|
355
|
+
folder = client.get_v2(f"/folder/{args.folder_id}", allow_dry_run=True)
|
|
356
|
+
task_counts = count_folder_tasks(client, folder)
|
|
357
|
+
if not task_counts["complete"]:
|
|
358
|
+
error("Cannot purge folder: exhaustive task scan did not complete")
|
|
359
|
+
if task_counts["total"] > 0:
|
|
360
|
+
error("Cannot purge folder: child lists contain tasks")
|
|
361
|
+
|
|
362
|
+
if client.dry_run:
|
|
363
|
+
return {
|
|
364
|
+
"dry_run": True,
|
|
365
|
+
"action": "purge_empty_folder",
|
|
366
|
+
"folder_id": args.folder_id,
|
|
367
|
+
"deletable": True,
|
|
368
|
+
"folder": folder,
|
|
369
|
+
"task_counts": task_counts,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
client.delete_v2(f"/folder/{args.folder_id}")
|
|
373
|
+
return {"status": "ok", "action": "purged_empty_folder", "folder_id": args.folder_id}
|
|
374
|
+
|
|
375
|
+
|
|
239
376
|
def cmd_folders_privacy(client, args):
|
|
240
377
|
"""Set a folder private or public via the v3 ACLs endpoint."""
|
|
241
378
|
return handle_privacy_request(
|
|
@@ -255,6 +392,8 @@ COMMAND_MANIFEST = {
|
|
|
255
392
|
"create": cmd_folders_create,
|
|
256
393
|
"update": cmd_folders_update,
|
|
257
394
|
"delete": cmd_folders_delete,
|
|
395
|
+
"backup": cmd_folders_backup,
|
|
396
|
+
"purge-empty": cmd_folders_purge_empty,
|
|
258
397
|
"privacy": cmd_folders_privacy,
|
|
259
398
|
},
|
|
260
399
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""List command handlers — list, get, create, update, delete, privacy."""
|
|
2
2
|
|
|
3
3
|
from ..helpers import read_content, error, resolve_space_id, add_id_argument
|
|
4
|
+
from .backup import backup_list, backup_options, count_list_tasks
|
|
4
5
|
from .privacy import handle_privacy_request, register_privacy_subcommand
|
|
5
6
|
|
|
6
7
|
|
|
@@ -22,6 +23,7 @@ Subcommands:
|
|
|
22
23
|
create — create a new list in a folder or space (mutating)
|
|
23
24
|
update — update a list's name, content, or status (mutating)
|
|
24
25
|
delete — delete a list (destructive)
|
|
26
|
+
backup — export list metadata, tasks, and comments to local JSON files
|
|
25
27
|
privacy — make a list private or public (mutating)""",
|
|
26
28
|
epilog="""\
|
|
27
29
|
examples:
|
|
@@ -199,6 +201,35 @@ examples:
|
|
|
199
201
|
)
|
|
200
202
|
add_id_argument(ld, "list_id", "ClickUp list ID to delete")
|
|
201
203
|
|
|
204
|
+
lb = lists_sub.add_parser(
|
|
205
|
+
"backup",
|
|
206
|
+
formatter_class=F,
|
|
207
|
+
help="Back up a list to local JSON files",
|
|
208
|
+
description="""\
|
|
209
|
+
Back up a list before migration or deletion. Writes list metadata, task-list
|
|
210
|
+
JSON, per-task full JSON, and a deterministic manifest.json to --output-dir.
|
|
211
|
+
|
|
212
|
+
Defaults are safety-first: include closed tasks, archived tasks, subtasks,
|
|
213
|
+
all task pages, and all comments unless explicitly disabled.""",
|
|
214
|
+
epilog="""\
|
|
215
|
+
returns:
|
|
216
|
+
{"status": "ok", "action": "backup_list", "list_id": "...", ...}
|
|
217
|
+
|
|
218
|
+
examples:
|
|
219
|
+
clickup lists backup 12345 --output-dir ./backup/list-12345
|
|
220
|
+
clickup lists backup --list-id 12345 --output-dir ./backup --no-comments
|
|
221
|
+
|
|
222
|
+
notes:
|
|
223
|
+
This command writes local files and does not mutate ClickUp.""",
|
|
224
|
+
)
|
|
225
|
+
add_id_argument(lb, "list_id", "ClickUp list ID to back up")
|
|
226
|
+
lb.add_argument("--output-dir", required=True, help="Directory for backup JSON files")
|
|
227
|
+
lb.add_argument("--no-closed", action="store_true", help="Do not include closed tasks")
|
|
228
|
+
lb.add_argument("--no-archived", action="store_true", help="Do not include archived tasks")
|
|
229
|
+
lb.add_argument("--no-subtasks", action="store_true", help="Do not include subtasks")
|
|
230
|
+
lb.add_argument("--first-page", action="store_true", help="Only fetch the first task page")
|
|
231
|
+
lb.add_argument("--no-comments", action="store_true", help="Do not hydrate task comments")
|
|
232
|
+
|
|
202
233
|
register_privacy_subcommand(
|
|
203
234
|
lists_sub,
|
|
204
235
|
F,
|
|
@@ -292,12 +323,39 @@ def cmd_lists_update(client, args):
|
|
|
292
323
|
def cmd_lists_delete(client, args):
|
|
293
324
|
"""Delete a list by ID."""
|
|
294
325
|
if client.dry_run:
|
|
295
|
-
|
|
326
|
+
list_meta = client.get_v2(f"/list/{args.list_id}", allow_dry_run=True)
|
|
327
|
+
task_counts = count_list_tasks(client, args.list_id)
|
|
328
|
+
return {
|
|
329
|
+
"dry_run": True,
|
|
330
|
+
"action": "delete_list",
|
|
331
|
+
"list_id": args.list_id,
|
|
332
|
+
"list": list_meta,
|
|
333
|
+
"task_counts": task_counts,
|
|
334
|
+
}
|
|
296
335
|
|
|
297
336
|
client.delete_v2(f"/list/{args.list_id}")
|
|
298
337
|
return {"status": "ok", "action": "deleted", "list_id": args.list_id}
|
|
299
338
|
|
|
300
339
|
|
|
340
|
+
def cmd_lists_backup(client, args):
|
|
341
|
+
"""Back up a list to local JSON files."""
|
|
342
|
+
manifest = backup_list(
|
|
343
|
+
client,
|
|
344
|
+
args.list_id,
|
|
345
|
+
args.output_dir,
|
|
346
|
+
backup_options(args),
|
|
347
|
+
)
|
|
348
|
+
return {
|
|
349
|
+
"status": "ok",
|
|
350
|
+
"action": "backup_list",
|
|
351
|
+
"list_id": args.list_id,
|
|
352
|
+
"output_dir": args.output_dir,
|
|
353
|
+
"manifest": "manifest.json",
|
|
354
|
+
"task_count": manifest["task_count"],
|
|
355
|
+
"complete": manifest["complete"],
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
301
359
|
def cmd_lists_privacy(client, args):
|
|
302
360
|
"""Set a list private or public via the v3 ACLs endpoint."""
|
|
303
361
|
return handle_privacy_request(
|
|
@@ -317,6 +375,7 @@ COMMAND_MANIFEST = {
|
|
|
317
375
|
"create": cmd_lists_create,
|
|
318
376
|
"update": cmd_lists_update,
|
|
319
377
|
"delete": cmd_lists_delete,
|
|
378
|
+
"backup": cmd_lists_backup,
|
|
320
379
|
"privacy": cmd_lists_privacy,
|
|
321
380
|
},
|
|
322
381
|
}
|