pingcode-cli-unofficial 1.7.1

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.
@@ -0,0 +1,385 @@
1
+ # 项目管理 (pjm) — `project`
2
+
3
+ > Part of the `pingcode` skill. Read [`../SKILL.md`](../SKILL.md) first — it carries the
4
+ > authentication gate, the stdout/stderr contract, the exit-code table and the rules that
5
+ > apply to every module. This file is only the pjm command surface and the ids it needs.
6
+
7
+ ### Projects — 项目管理
8
+
9
+ ```bash
10
+ pingcode project list --json
11
+ pingcode project list --keywords mobile --type scrum
12
+ pingcode project get "Mobile App" --json
13
+ pingcode project progress "Mobile App" --json
14
+
15
+ pingcode project create --name "Payments" --identifier PAY --type scrum --dry-run --json
16
+ pingcode project create --name "Payments" --identifier PAY --type scrum \
17
+ --assignee wangxiao --member wangxiao --member lina --json
18
+ pingcode project update "Payments" --description "Q4 rewrite" --json
19
+ ```
20
+
21
+ **A project cannot be deleted, and cannot even be archived.** There is no `DELETE
22
+ /v1/pjm/projects/{id}`, and `is_archived` is read-only — a patch containing it returns 200 and
23
+ changes nothing (verified live). So `project create` is **irreversible**: a project created by
24
+ mistake stays in every listing forever. Always `--dry-run` first.
25
+
26
+ Two more create-time facts an `--help` line cannot convey:
27
+
28
+ - **`--visibility` can only be set at create time.** Patching it later is accepted and silently
29
+ dropped, exactly like `is_archived`, `type` and `process_id`.
30
+ - **`--identifier` prefixes every work-item id** in the project (`PAY-1`, `PAY-2`). It must be
31
+ uppercase and short; anything else is rejected as a bad format rather than truncated. It is the
32
+ one required create field that *can* still be changed afterwards — and changing it renames every
33
+ work-item identifier in the project.
34
+ - **`--start-at` / `--end-at` are instants, stored verbatim** — `12:34:56` comes back as
35
+ `12:34:56`. This is the opposite of `project sprint` and `project version`, which snap to
36
+ `00:00:00` / `23:59:59` of the date. That is why this pair is spelled `--start-at` / `--end-at`
37
+ and theirs is `--start` / `--end`. The inconsistency is upstream's, not the CLI's.
38
+ - **`--assignee` (负责人) is unrelated to membership.** It may be someone who is not a member, and
39
+ setting it does not add them. `PATCH` is partial: fields you do not pass are untouched.
40
+
41
+ `project progress` returns **work-item counts only** — no sprint, release or workload figure — and
42
+ it is a single count block, not a list, despite the API docs implying otherwise. The three counts
43
+ group every state by its `type`, so a custom state counts towards whichever of
44
+ pending / in_progress / completed it is configured as.
45
+
46
+ ### 项目成员 Members — `project member`
47
+
48
+ ```bash
49
+ pingcode project member list --project "Mobile App" --json
50
+ pingcode project member get wangxiao --project "Mobile App" --json
51
+ pingcode project member add --project "Mobile App" --user lina --json
52
+ pingcode project member add --project "Mobile App" --group-id 68389e7f33ee52bc5c2584c1 --json
53
+ ```
54
+
55
+ - **A membership is addressed by the USER id.** The row's `id` *is* the user (or group) id, so
56
+ `member get` takes the same reference `member add --user` took. There is no separate membership
57
+ id.
58
+ - **There is no `member remove` leaf.** The endpoint exists; it is reachable through the generic
59
+ layer, and that call is verified working:
60
+ `pingcode api DELETE /v1/pjm/projects/<project_id>/members/<user_id> --yes`. It is not a refined
61
+ leaf because a membership is the cheapest thing in this API to recreate — one `member add`.
62
+ - `--role-id` is optional and defaults to 普通成员. The three roles are organisation-level:
63
+ `pingcode api GET /v1/directory/roles`.
64
+ - A user who is not in the project, **and** an id that does not exist, both answer
65
+ `成员不在项目中` → **exit 5**. The two are indistinguishable, so exit 5 here means "not a member
66
+ of this project", not necessarily "no such user".
67
+
68
+ ### `project meta` — mandatory before creating or updating a work item
69
+
70
+ `type_id`, `state_id` and `priority_id` are **project-scoped**: the same state name has a different id
71
+ in another project, and system work-item types use slugs (`task`, `story`, `bug`) while custom types
72
+ use hex ids. Never reuse an id across projects, and never guess one.
73
+
74
+ ```bash
75
+ pingcode project meta types --project "Mobile App" --json
76
+ pingcode project meta states --project "Mobile App" --type task --json
77
+ pingcode project meta priorities --project "Mobile App" --json
78
+ pingcode project meta sprints --project "Mobile App" --json
79
+ pingcode project meta relation-types --json
80
+ pingcode project meta tags --project "Mobile App" --json
81
+ ```
82
+
83
+ `pingcode project meta states` requires **both** a project and a type — that is an API constraint, not a CLI
84
+ choice.
85
+
86
+ **`--state <name>` therefore always requires `--type`, everywhere in this module.** States live in a
87
+ `(project, work item type)` pair and the API does **not** report a work item's type, so the CLI cannot
88
+ infer it — not on `list`, not on `create`, and not on `update` / `transition` either. Either pass
89
+ `--type <name|id>` alongside `--state <name>`, or skip the lookup entirely with `--state-id <id>`.
90
+ `--state` and `--state-id` are mutually exclusive.
91
+
92
+ **State changes are workflow-validated by the server**: the target state must belong to the type's
93
+ state scheme *and* a legal transition must exist from the current state. There is no local
94
+ pre-check — on rejection the CLI prints the server's message plus the states configured for that
95
+ type on stderr, but only if you passed `--type`, since listing the candidates needs it too.
96
+
97
+ Lookups are cached under `~/.pingcode/cache/` for 24 hours. Use `--no-cache` if a project was
98
+ reconfigured and an id looks stale.
99
+
100
+ `project meta relation-types` is **organisation-wide** and takes no project: nine system rows, and
101
+ the `CATEGORY` column (`relate`, `block`, `blocked_by`, `cause`, `caused_by`, `clone`, `cloned_by`,
102
+ `duplicate`, `mention`) is the stable key to script against — the ids are 24-hex and differ per
103
+ tenant. It serves `project work-item link add` and nothing else.
104
+
105
+ **`project meta tags` needs reading before you use an id from it.** `--project` is *required* by the
106
+ endpoint and then *ignored* by it — three different projects return byte-identical lists (verified
107
+ live) — so what you get is every tag in the **organisation**. Tags are nevertheless really
108
+ project-scoped where it counts: `work-item tag add` refuses a tag belonging to another project with
109
+ `'tag'资源不存在`, which reads as though the tag did not exist. It does; it is simply not this
110
+ project's. Concretely: of 23 org-wide tags, **all 23 were refused** for a work item in one project
111
+ and **8 were accepted** for a work item in another. There is no endpoint listing just one project's
112
+ tags, so the reliable way to find a usable id is to read the `tags[]` of a work item already in that
113
+ project. Names are not unique either (four `后端`, three `前端`), which is also why `pingcode resolve`
114
+ has **no** work-item-tag kind: a cached resolver would hand back ids the write refuses.
115
+
116
+ ### Work items — `project work-item`
117
+
118
+ ```bash
119
+ pingcode project work-item list --project "Mobile App" --json
120
+ pingcode project work-item list --project "Mobile App" --type task --state "In Progress" --json
121
+ pingcode project work-item list --project "Mobile App" --assignee wangxiao --page-size 20 --page 0
122
+ pingcode project work-item list --project "Mobile App" --all --limit 200 --json
123
+
124
+ pingcode project work-item get SCR-5 --json
125
+ pingcode project work-item get 1bAqLmTG --json
126
+ pingcode project work-item get https://example.pingcode.com/pjm/work_items/1bAqLmTG --json
127
+
128
+ pingcode project work-item create --project "Mobile App" --type task --title "Fix login retry" --dry-run --json
129
+ pingcode project work-item create --project "Mobile App" --type task --title "Fix login retry" \
130
+ --assignee wangxiao --priority High --end-at 2026-02-15 --json
131
+
132
+ pingcode project work-item update SCR-5 --title "Fix login retry (v2)" --json
133
+ pingcode project work-item update SCR-5 --type task --state "In Progress" --json
134
+ pingcode project work-item transition SCR-5 --type task --state Done --json
135
+ pingcode project work-item transition SCR-5 --state-id 5eb623f6a70571487ea47000 --json
136
+ ```
137
+
138
+ `project work-item get` accepts an id, a `short_id`, an identifier such as `SCR-5`, or a pasted work-item URL.
139
+ `update` and `transition` accept the same forms and resolve them to a real id first.
140
+
141
+ On `update` and `transition`, `--type` is **only** a lookup aid: it resolves `--state <name>` and lets
142
+ the CLI list the candidate states if the server rejects the change. It is never written to the work
143
+ item — there is no patchable type field.
144
+
145
+ **The assignee cannot be cleared via the API.** `PATCH`'s `assignee_id` is a plain string with no
146
+ `nullable`, and the server treats `null` as "field absent" (HTTP 200, no-op) and `""` as invalid
147
+ (HTTP 400). So `work-item update <id> --assignee ""` does **not** send a request — it fails fast with
148
+ exit 2 and a message naming the limitation. To unassign, use the PingCode web UI.
149
+
150
+ #### Filtering: two transports, one command
151
+
152
+ ```bash
153
+ pingcode project work-item list --project "Mobile App" --unassigned --json
154
+ pingcode project work-item list --project "Mobile App" --title-contains login --json
155
+ pingcode project work-item list --project "Mobile App" --created-after 2026-08-01 --all --json
156
+ ```
157
+
158
+ Six flags — `--unassigned`, `--title-contains`, `--created-after/-before`,
159
+ `--updated-after/-before`, marked `(search)` in `--help` — can only be expressed by
160
+ `POST /v1/pjm/work_items/search`, so passing any of them switches the transport. **Paging, `--all`
161
+ and the reported total behave identically on both**, so the switch is invisible except for which
162
+ filters exist. What genuinely differs is the filter vocabulary: search cannot filter by
163
+ `identifier`, `short_id` or bug type at all, and the simple list cannot filter by date, title text
164
+ or "unassigned".
165
+
166
+ #### `bulk-update` — one property, many work items
167
+
168
+ ```bash
169
+ pingcode project work-item bulk-update --id SCR-5 --id SCR-6 --assignee wangxiao --json
170
+ pingcode project work-item bulk-update --id SCR-5 --id SCR-6 --project "Mobile App" --type task --state Done --json
171
+ ```
172
+
173
+ **The endpoint carries ONE `property_name` per call**, so exactly one of `--assignee` / `--state` /
174
+ `--priority` / `--title` / `--description` / `--property` may be given; two properties need two
175
+ invocations. Four things about it that will bite an agent that assumes otherwise:
176
+
177
+ - **`--sprint` does not exist here, deliberately.** `property_name: sprint_id` is accepted with
178
+ HTTP 200 and `updated: 0`, and changes nothing (verified live). The same is true of `type_id`,
179
+ `tag_ids`, `version_ids`, `participant_ids`, `properties` and `bug_type_id`. Moving items into a
180
+ sprint or onto a release has to go one at a time through `work-item update --sprint` /
181
+ `--release` — see the next section. Only `assignee_id`, `state_id`, `priority_id`, `title` and
182
+ `description` actually apply; `--property` is the unvalidated escape hatch for anything else.
183
+ - **Always read the `updated` count.** It is the *only* signal the endpoint gives: a nonexistent id,
184
+ a silently-rejected value and an unsupported property all answer 200. The CLI warns when `updated`
185
+ is less than the number of ids sent — treat that warning as "verify by reading the items back".
186
+ - **It is best-effort, not atomic.** An unknown id is skipped silently and the rest still land. (The
187
+ `sprint bulk-create` and `version bulk-create` *are* atomic — do not generalise from one to the other.)
188
+ - **It leaves no audit trail.** The change appears in neither `work-item activity list` nor
189
+ `work-item history list`, while the equivalent single `update` does. It is invisible to an audit.
190
+
191
+ #### Moving an existing item into a sprint, or onto a release
192
+
193
+ `work-item update` carries both, and it is the only refined path: `bulk-update` answers
194
+ 200 / `updated: 0` for `sprint_id` and `version_ids` alike, however valid the id (verified live
195
+ 2026-08-05, and again after these flags landed).
196
+
197
+ ```bash
198
+ pingcode project work-item update SCR-5 --sprint "Sprint 12" --json
199
+ pingcode project work-item update SCR-5 --release "1.4.0" --json
200
+ pingcode project work-item update SCR-5 --release "1.4.0" --release "1.4.1-hotfix" --json
201
+ ```
202
+
203
+ - **The release flag is `--release`, NOT `--version`.** `--version` belongs to the CLI itself, and
204
+ because commander's root parses options across the whole argv it wins even after the subcommand:
205
+ `work-item update <id> --version 1.4.0` prints `0.1.0` and **exits 0 having sent nothing**. Do not
206
+ reach for it. These are the same 发布 that `project version list` prints.
207
+ - **The two fields have different shapes upstream, so the flags differ.** `sprint_id` is a
208
+ **scalar**: one `--sprint`, and moving between sprints is a single call. `version_ids` is an
209
+ **array that replaces**: `--release` is repeatable, and the releases you pass become the complete
210
+ list. Pass every release the item should end up on in one invocation; read the current ones off
211
+ `work-item get` first if you mean to add rather than set.
212
+ - **No `--project` is needed.** Both names resolve per project, and the project comes off the work
213
+ item itself. Repeating `--release` costs no extra round-trip: the second name hits the list the
214
+ first one cached.
215
+ - **Neither field can be EMPTIED — not by these flags and not by any other route.** Omitting
216
+ `--release` leaves the list alone, and the generic layer cannot clear it either (all live
217
+ 2026-08-05): `--body '{"version_ids":[]}'` is refused with
218
+ `100006 'version_ids'不是有效的数组(数组不能为空)`, `[""]` with `不是有效的字符串`, and
219
+ `{"version_ids":null}` answers **200 and changes nothing** — the accepted-and-ignored pattern this
220
+ API keeps repeating. `sprint_id` behaves identically (`null` → 200 no-op, `""` → `100003`). You can
221
+ move an item to a *different* sprint or release; you cannot take it off all of them. `version_ids`
222
+ is also one of the few fields the server really type-checks, so a stringified array via `--set` is
223
+ `100006`, exit 7, nothing written — use `--body` if you go around the flags.
224
+ - Both changes *are* audited, unlike `bulk-update`: they show up in `work-item activity list` as
225
+ `property_key: iteration` and `property_key: version`, with `origin`/`target` — which is also how
226
+ you verify them without trusting the 200.
227
+
228
+ #### `delete`
229
+
230
+ ```bash
231
+ pingcode project work-item delete SCR-5 --yes --dry-run --json
232
+ pingcode project work-item delete SCR-5 --yes
233
+ ```
234
+
235
+ `--yes` is mandatory and the refusal echoes the identifier and title, so a wrong reference is
236
+ visible before the write. Unlike a sprint (which can never be deleted) a work item **can** be, and
237
+ it leaves every list, sprint, board and link immediately. The web UI recycle bin can restore it;
238
+ this API cannot.
239
+
240
+ #### `link` vs `relation` — two unrelated families, and they must not be confused
241
+
242
+ ```bash
243
+ pingcode project meta relation-types --json
244
+ pingcode project work-item link add SCR-5 --target SCR-6 --relation block --json
245
+ pingcode project work-item link list SCR-5 --json
246
+ pingcode project work-item link get SCR-5 <link_id> --json
247
+ pingcode project work-item link delete SCR-5 <link_id> --yes
248
+ ```
249
+
250
+ - **`link` is work item ↔ work item, with a required type** (`/v1/pjm/work_items/{id}/relations`).
251
+ `--relation` accepts the category slug (`block`), the localized name (`阻塞`) or the id — all three
252
+ verified working.
253
+ - **`relation` is work item ↔ anything else, with no type at all** (`/v1/relations`) and it
254
+ **refuses** two work items outright (`不支持的'principal_type'`). Use it for a work item to an
255
+ idea, ticket, test case or wiki page.
256
+ - The server maintains the **inverse** edge: adding `block` on one item adds `blocked_by` on the
257
+ other, and deleting either side removes both. **The two sides have different link ids**, so delete
258
+ the id `link list` printed for the item you are on — passing the other end's id answers
259
+ `工作项或工作项关联不存在` → exit 5.
260
+ - Links may cross projects, and a work item may be linked to itself.
261
+
262
+ #### `tag` — and the list that does not exist
263
+
264
+ ```bash
265
+ pingcode project work-item tag add SCR-5 --tag-id 6a28fbe209dbd0bc097457ee --json
266
+ pingcode project work-item tag get SCR-5 <tag_id> --json
267
+ pingcode project work-item tag delete SCR-5 <tag_id> --yes
268
+ ```
269
+
270
+ **There is no `work-item tag list`, and there cannot be**: upstream has the add, the get-one and the
271
+ delete but no collection GET. Read the `tags[]` field of `work-item get` instead — that is the
272
+ complete answer for one work item. `project meta tags` is the only way to enumerate the vocabulary,
273
+ with the organisation-wide caveat documented above; `--tag <name>` is resolved live against the work
274
+ item's own project and errors with the candidates when a name is ambiguous, which the common names
275
+ are.
276
+
277
+ #### `history` — state changes only
278
+
279
+ ```bash
280
+ pingcode project work-item history list SCR-5 --json
281
+ pingcode project work-item history get SCR-5 <history_id> --json
282
+ ```
283
+
284
+ 流转记录 is **state changes only**. A title, assignee or sprint change is not here — that is
285
+ `work-item activity list`, the free-form audit feed. Every work item has one row from creation, with
286
+ `FROM` shown as `(new)`. A `bulk-update` appears in **neither** feed.
287
+
288
+ ### 迭代 Sprints — `project sprint`
289
+
290
+ ```bash
291
+ pingcode project meta sprints --project "Mobile App" --json # the LIST lives here
292
+ pingcode project sprint get "Sprint 5" --project "Mobile App" --json
293
+ pingcode project sprint create --project "Mobile App" --name "Sprint 5" \
294
+ --start 2026-09-01 --end 2026-09-14 --assignee wangxiao --json
295
+ pingcode project sprint update "Sprint 5" --project "Mobile App" --status in_progress --json
296
+ pingcode project sprint bulk-create --project "Mobile App" --assignee wangxiao --file sprints.json --json
297
+ ```
298
+
299
+ Four things about sprints that `--help` cannot make obvious enough:
300
+
301
+ 1. **There is no `sprint list` leaf.** The list is `project meta sprints`, because it doubles as
302
+ the lookup `--sprint <name>` resolves against. Do not look for `project sprint list`.
303
+ 2. **A sprint cannot be deleted. Ever.** The API exposes only `GET` and `PATCH` on a sprint, so
304
+ `pingcode api DELETE /v1/pjm/projects/<p>/sprints/<id>` is refused before any request too.
305
+ Treat `sprint create` and `sprint bulk-create` as irreversible.
306
+ 3. **Sprints exist only in scrum and hybrid projects.** In a kanban or waterfall project the list
307
+ is empty and a create fails with `'project'资源不存在` — *the project is fine*, sprints are not
308
+ available in it. That exits **7**, not 5, precisely because the code cannot be told apart from a
309
+ genuinely missing project; the CLI appends the explanation to the message. Plan releases with
310
+ `project version` instead, which works in every project type.
311
+ 4. **`--status` writes a field, it does not run the lifecycle.** Setting `in_progress` or
312
+ `completed` through the API leaves `started_at` and `completed_at` `null`, and neither is
313
+ writable. A sprint "completed" via the API is not the same as one completed in the web UI.
314
+
315
+ ### 发布 Releases — `project version`
316
+
317
+ ```bash
318
+ pingcode project version list --project "Mobile App" --json
319
+ pingcode project version list --project "Mobile App" --name 1.4 --status in_progress --json
320
+ pingcode project version get "1.4.0" --project "Mobile App" --json
321
+ pingcode project version create --project "Mobile App" --name 1.4.0 \
322
+ --start 2026-09-01 --end 2026-09-30 --assignee wangxiao --json
323
+ pingcode project version update "1.4.0" --project "Mobile App" --stage-id <id> --operate-at 2026-09-20 --json
324
+ pingcode project version delete "1.4.0" --project "Mobile App" --yes --json
325
+ pingcode project version bulk-create --project "Mobile App" --assignee wangxiao --file releases.json --json
326
+ ```
327
+
328
+ **"version" is the most overloaded word in this API. This one is a project release 发布.** It is
329
+ not a wiki page revision (`/v1/wiki/pages/{id}/versions`), not a work-item state or property
330
+ *scheme* (`work_item_state_plans`, `*_property_plans`), and not a test plan or a requirement
331
+ schedule. Four different resources, one English word. `project version` only ever means the first.
332
+
333
+ - **`--name` on the list is a SUBSTRING search**, case-insensitive — `--name 1.4` matches `1.4.0`
334
+ and `1.4.1`. This is unlike `scm platform list --name` and `release env list --name`, which are
335
+ exact matches. Do not use it as an existence check.
336
+ - **`--status` filters on the stage's *kind*** — `pending | in_progress | published` — not on a
337
+ field of the release, which has no `status` at all. Stage *names* (`未开始`) are rejected with
338
+ exit 2.
339
+ - **The API ignores `--project` on `get` and `update`.** A version id is effectively
340
+ organisation-wide: naming the wrong project still reads, and still *writes* — the change lands on
341
+ the release in its real project. Only `delete` refuses a mismatched pair. Never treat a
342
+ successful update as proof that the project was right.
343
+ - **`progress` and `changelog` are read-only.** No body field writes either; sending one is
344
+ accepted and silently dropped.
345
+ - **`--operate-at` requires `--stage-id`.** Sent alone the API answers 200, echoes the *previous*
346
+ value and stores nothing, so the CLI refuses it with exit 2. Moving to a stage the release has
347
+ never been in requires it; moving to one it has been in does not. Moving to the stage it is
348
+ already in is refused by the server (exit 7).
349
+ - **`version delete` detaches the release from every work item that references it.** The work items
350
+ survive; their version link disappears. It is gated behind `--yes` and the confirmation names the
351
+ release.
352
+ - **Stages and categories are ids, not names.** `--stage-id` and `--category-id` take ids because
353
+ the stage and category endpoints are outside this command group:
354
+ `pingcode api GET /v1/pjm/stages` and
355
+ `pingcode api GET /v1/pjm/projects/<project_id>/version_categories`. A release name *is*
356
+ resolvable: `pingcode resolve pjm-version "1.4.0" --parent <project_id>`.
357
+
358
+ ### Both families: dates, and the two `bulk-create` leaves
359
+
360
+ **`--start` and `--end` are dates, not instants.** The server stores `--start` at `00:00:00` and
361
+ `--end` at `23:59:59` of the date given, in both families, on create and on update. Pass
362
+ `2026-09-01`, not a timestamp, and do not try to express a partial day — it will be widened.
363
+ Unlike `release deploy`, both ends may travel in one update and are validated against each other.
364
+
365
+ Both `bulk-create` leaves take a JSON array (or the wire's own `{"sprints": […]}` / `{"versions": […]}`
366
+ wrapper) through `--file <path>`:
367
+
368
+ ```json
369
+ [
370
+ {"name": "Sprint 5", "start": "2026-09-01", "end": "2026-09-14"},
371
+ {"name": "Sprint 6", "start": "2026-09-15", "end": "2026-09-28", "assignee": "lina"}
372
+ ]
373
+ ```
374
+
375
+ - Every entry needs `name`, `start`, `end`, a project and an assignee. The last two may come from
376
+ the entry (`project` / `project_id`, `assignee` / `assignee_id`) or from a shared `--project` /
377
+ `--assignee` on the command line.
378
+ - **Unknown keys are refused before anything is sent.** This matters: the API accepts unknown body
379
+ fields with a 200 and silently drops them, so a typo would otherwise create every row with the
380
+ field missing and no error anywhere.
381
+ - **The call is atomic.** If any entry is rejected, none is created — verified live. There is no
382
+ entry limit (60 in one call was accepted), but two entries sharing a name inside one batch is an
383
+ HTTP 500.
384
+ - **Both `bulk-create` endpoints are 企业令牌 only and the docs declare no scope for them.** They work with
385
+ the CLI's client-credentials token. `pingcode api describe pjm.sprints.bulk` reports the same.