@zumino/cli 2.1.0 → 2.2.0
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.
- package/README.md +63 -24
- package/package.json +1 -1
- package/skill/SKILL.md +59 -14
- package/src/commands/auth.js +1 -1
- package/src/commands/commands.js +26 -0
- package/src/commands/epic.js +78 -32
- package/src/commands/find.js +41 -50
- package/src/commands/project.js +147 -0
- package/src/commands/queue.js +2 -1
- package/src/commands/request.js +153 -15
- package/src/commands/skill.js +11 -1
- package/src/commands/task.js +280 -75
- package/src/commands/workspace.js +51 -0
- package/src/events.js +104 -0
- package/src/flags.js +257 -0
- package/src/help.js +259 -0
- package/src/main.js +73 -86
- package/src/output.js +17 -29
- package/src/spec.js +1015 -0
- package/src/users.js +38 -0
package/README.md
CHANGED
|
@@ -21,34 +21,73 @@ stale client is told on the call it was already making, and an incompatible one
|
|
|
21
21
|
refuses to run rather than returning a plausible answer built on shapes the
|
|
22
22
|
server no longer sends.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Finding your way
|
|
25
|
+
|
|
26
|
+
The command list is not written down twice. `cli/src/spec.js` declares every
|
|
27
|
+
command, every flag and every value a flag accepts; the help pages, the argument
|
|
28
|
+
validation and the machine-readable dump are all generated from it.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
zumino # every command, grouped, one line each
|
|
32
|
+
zumino help task list # one command in full: flags, accepted values, examples
|
|
33
|
+
zumino commands # every command as a flat list, for grepping
|
|
34
|
+
zumino commands --json # the whole surface, for a program to read
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`zumino commands --json` is the one to reach for from a script or an agent. It
|
|
38
|
+
carries the vocabularies, so `--status in_review` is never discovered by having
|
|
39
|
+
`--status review` refused:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
zumino commands --json | jq -r '.values.taskStatus[]'
|
|
43
|
+
zumino commands --json | jq -r '.commands[] | select(.path|startswith("task")) | .usage'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The shape of it
|
|
47
|
+
|
|
48
|
+
**Generic reads span every kind; every write names its kind.** So `queue`,
|
|
49
|
+
`context` and `find` are verbs at the root, and everything that touches one kind
|
|
50
|
+
hangs off that kind's noun.
|
|
25
51
|
|
|
26
52
|
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
zumino task assign <CODE> <userId|->
|
|
34
|
-
zumino task spec <CODE> --plan T | --acceptance T
|
|
35
|
-
zumino task comment <CODE> <text>
|
|
36
|
-
zumino task link <CODE> <blocks|blocked-by|related|answers> <CODE|project#N>
|
|
37
|
-
zumino task ref <CODE> --url URL [--title T]
|
|
38
|
-
zumino task attention <CODE> [reason|-]
|
|
39
|
-
zumino task show <CODE>
|
|
40
|
-
|
|
41
|
-
zumino request create|answer|status|note|comment|show
|
|
42
|
-
zumino request promote <CODE|#N> --to <work-project>
|
|
43
|
-
zumino epic create|status|list|show # address as E6 or ACME-E6
|
|
44
|
-
|
|
45
|
-
zumino api <METHOD> <PATH> [--body JSON]
|
|
46
|
-
zumino auth login | status | list | logout
|
|
47
|
-
zumino init
|
|
48
|
-
zumino skill install [--dir D] [--check]
|
|
49
|
-
zumino self-update
|
|
53
|
+
orient workspace list · project list · project show · project tags · auth status
|
|
54
|
+
read queue · context <CODE> · find
|
|
55
|
+
one kind task list show events create edit status assign spec comment link ref attention tag untag
|
|
56
|
+
request list show events create edit answer status note comment promote tag untag
|
|
57
|
+
epic list show create edit status
|
|
58
|
+
else api · auth · init · skill install · self-update · commands · help
|
|
50
59
|
```
|
|
51
60
|
|
|
61
|
+
## Filters
|
|
62
|
+
|
|
63
|
+
Every list command carries the filters its endpoint performs, and **only** those.
|
|
64
|
+
Nothing here narrows a list locally: a client-side filter reads exactly like a
|
|
65
|
+
server-side one and is wrong invisibly, because it filters the page it was handed
|
|
66
|
+
rather than the project.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
zumino task list --status todo,in_progress --priority high --assignee me
|
|
70
|
+
zumino task list --epic E3 --sort manual # the order the board draws
|
|
71
|
+
zumino task list --status done --limit 100 --offset 100
|
|
72
|
+
zumino request list --status open --sort top --limit 10
|
|
73
|
+
zumino find "rate limit" --kind task,epic --state open
|
|
74
|
+
zumino task events ACME-14 --field status # how has this moved
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Three consequences of that rule worth knowing:
|
|
78
|
+
|
|
79
|
+
- **A flag a command does not accept is an error**, not a no-op. `zumino find
|
|
80
|
+
--priority high` says so and names the command that does filter by priority.
|
|
81
|
+
Every value is checked against the real vocabulary before the call is made.
|
|
82
|
+
- **`--status` is repeatable** (`--status todo --status done`) or
|
|
83
|
+
comma-separated; both produce one query parameter.
|
|
84
|
+
- **A paged answer says it is paged**, and names the flag that fetches the rest —
|
|
85
|
+
`1–50 of 312 — next page: --offset 50`. A table that stops at fifty rows and
|
|
86
|
+
says nothing is how a duplicate gets filed.
|
|
87
|
+
|
|
88
|
+
`--assignee me` resolves against the token's owner, so no `jq` is needed to find
|
|
89
|
+
your own user id.
|
|
90
|
+
|
|
52
91
|
`--json` prints the raw response instead of a table, on every command that
|
|
53
92
|
returns API data. The housekeeping commands — `auth login`/`logout`, `init`,
|
|
54
93
|
`skill install`, `self-update` — report progress in prose and ignore it.
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: zumino
|
|
3
|
-
version: 2.
|
|
3
|
+
version: 2.2.0
|
|
4
4
|
description: Read and modify Zumino projects from any directory with the `zumino` CLI. Two kinds of project — feedback projects (requests, votes, roadmap statuses, read publicly as a board) and work projects (tasks and epics, with a structured spec, an owner, and a readiness bar) — plus one generic read across every kind of item. Use whenever the user mentions Zumino, a product board, a public roadmap, requests or feedback, a project, a task, an epic, a backlog item, a spec, or asks to file/track/pick up a bug, feature request, idea, or piece of work.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Zumino — projects from the command line
|
|
8
8
|
|
|
9
|
-
Everything is `zumino`, installed globally and working from any directory.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Everything is `zumino`, installed globally and working from any directory.
|
|
10
|
+
|
|
11
|
+
**The surface documents itself, so this file does not restate it.**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
zumino # every command, grouped, one line each
|
|
15
|
+
zumino help task list # one command in full: flags, accepted values, examples
|
|
16
|
+
zumino commands --json # the whole surface, for a program to read
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`zumino commands --json` is the one to use. It carries every command, every flag
|
|
20
|
+
and — the part worth having — **every value each filter accepts**, under
|
|
21
|
+
`.values`. Read it rather than guessing a status and reading a 400 back.
|
|
22
|
+
|
|
23
|
+
This file is the part none of that can carry: **what the words mean, and what
|
|
24
|
+
will go wrong if you assume.**
|
|
12
25
|
|
|
13
26
|
If `zumino` is not installed: `npm i -g @zumino/cli`.
|
|
14
27
|
|
|
@@ -31,10 +44,16 @@ zumino api POST /workspaces/acme/projects/app/tasks --body '{"title":"…"}'
|
|
|
31
44
|
|
|
32
45
|
```bash
|
|
33
46
|
zumino auth status # who am I, against which host, from which source
|
|
47
|
+
zumino project list # what can I see, and which kind is each one
|
|
34
48
|
zumino queue # what can I pick up right now
|
|
35
49
|
zumino context ONS-14 # the whole brief for one task, as markdown
|
|
36
50
|
```
|
|
37
51
|
|
|
52
|
+
`zumino project list` is the orientation read, and its TYPE column decides which
|
|
53
|
+
half of the CLI applies: a **feedback** project holds requests (`#42`,
|
|
54
|
+
`zumino request …`), a **work** project holds tasks and epics (`ONS-14`,
|
|
55
|
+
`zumino task …`). `zumino workspace list` is the same question one level up.
|
|
56
|
+
|
|
38
57
|
`zumino context` is not a convenience. It assembles the epic, the description,
|
|
39
58
|
the plan, the acceptance criteria, the blockers and the requests a task answers
|
|
40
59
|
into one document. Six separate reads would let you lay them out yourself, and
|
|
@@ -42,7 +61,11 @@ that is exactly the failure: you would assemble a *plausible* brief instead of
|
|
|
42
61
|
the real one.
|
|
43
62
|
|
|
44
63
|
Add `--activity` when picking up work someone (or some earlier run) already
|
|
45
|
-
touched — it appends what has been tried.
|
|
64
|
+
touched — it appends what has been tried. `zumino task events ONS-14` is the same
|
|
65
|
+
history in full, and `--field status` narrows it to how the task has moved.
|
|
66
|
+
**Read it before picking a task up**: "what was tried and then reverted" is the
|
|
67
|
+
one piece of context the brief cannot hold, and re-doing an abandoned approach is
|
|
68
|
+
what it costs to skip.
|
|
46
69
|
|
|
47
70
|
## Read the domain model first
|
|
48
71
|
|
|
@@ -53,10 +76,17 @@ It is the one place the vocabulary is written down, and the registries in
|
|
|
53
76
|
|
|
54
77
|
This file deliberately carries **no copy** of any status list or kind list. A
|
|
55
78
|
second list is accurate the day it is written and wrong a release later, and you
|
|
56
|
-
would have no way of telling which one you were reading. From outside the repo
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
would have no way of telling which one you were reading. From outside the repo:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
zumino commands --json | jq '.values' # every vocabulary
|
|
83
|
+
zumino commands --json | jq -r '.values.taskStatus[]'
|
|
84
|
+
zumino help task list # the same lists, on the page
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The CLI refuses a value outside the real set before it sends anything, and names
|
|
88
|
+
the set in the refusal — so a wrong guess costs a message rather than a write.
|
|
89
|
+
Ask rather than guess.
|
|
60
90
|
|
|
61
91
|
## Three words mean something narrower here
|
|
62
92
|
|
|
@@ -89,12 +119,20 @@ finding worth reporting, not a reason to stop:
|
|
|
89
119
|
# Committed but not yet shaped — the queue will not offer these.
|
|
90
120
|
zumino find --state open --json | jq -r '.[] | select(.kind=="task") | .code'
|
|
91
121
|
zumino task show ONS-14 # readiness is on the task
|
|
122
|
+
```
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
Or ask the task endpoint directly, which has the statuses:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
zumino task list --status todo,shaping # committed
|
|
128
|
+
zumino task list --status backlog --open # not committed
|
|
96
129
|
```
|
|
97
130
|
|
|
131
|
+
`find` takes `--state open|closed` and **never `--status`**: the read that spans
|
|
132
|
+
every kind publishes no per-kind status (`docs/decisions/0003`). A status belongs
|
|
133
|
+
to a kind, so ask that kind — `zumino task list` or `zumino request list`. The
|
|
134
|
+
CLI says exactly that if you try, rather than answering an unfiltered list.
|
|
135
|
+
|
|
98
136
|
Shape one of those and it appears in the queue by itself. **Do not invent a
|
|
99
137
|
description or a criterion to make a task eligible** — that is laundering an
|
|
100
138
|
unshaped task as a shaped one, and the whole point of the bar is that it cannot
|
|
@@ -131,6 +169,10 @@ zumino task create --title "…" --description "…" # if not
|
|
|
131
169
|
zumino task ref ONS-14 --url <the PR you opened> # when you open one
|
|
132
170
|
```
|
|
133
171
|
|
|
172
|
+
**Check the page count.** A list that is a slice of a bigger answer says so and
|
|
173
|
+
names the flag that fetches the rest — `1–50 of 312 — next page: --offset 50`.
|
|
174
|
+
"Nothing matches" from a read you capped at ten is how a duplicate gets filed.
|
|
175
|
+
|
|
134
176
|
**Before finishing a piece of work, check the backlog.** A change that resolves,
|
|
135
177
|
contradicts or partially implements something already filed should say so on
|
|
136
178
|
that ticket rather than leaving it to be reconciled later.
|
|
@@ -210,8 +252,11 @@ cannot wait, stop and say which calls landed and which did not.
|
|
|
210
252
|
`in_review` and say what you did.
|
|
211
253
|
- If a task's readiness is below the bar, **raise attention rather than filling
|
|
212
254
|
the gap with a plausible guess**: `zumino task attention ONS-14 "…"`.
|
|
213
|
-
- If a project is ambiguous or missing, run `zumino
|
|
214
|
-
|
|
255
|
+
- If a project is ambiguous or missing, run `zumino project list` and ask — do
|
|
256
|
+
not guess.
|
|
257
|
+
- **Change several fields in one call**: `zumino task edit ONS-14 --priority high
|
|
258
|
+
--size L` is one entry in the history and reads as one decision. Five calls
|
|
259
|
+
read as five.
|
|
215
260
|
- An **assignee is always a person**, even when an agent does the work. There is
|
|
216
261
|
no agent identity; a token acts as its owner, and that is who the history
|
|
217
262
|
records.
|
package/src/commands/auth.js
CHANGED
|
@@ -146,7 +146,7 @@ function readRaw(promptText, stdin = process.stdin) {
|
|
|
146
146
|
|
|
147
147
|
export const __testables = { readRaw };
|
|
148
148
|
|
|
149
|
-
const SUB = { login, status, list, logout };
|
|
149
|
+
export const SUB = { login, status, list, logout };
|
|
150
150
|
|
|
151
151
|
export async function run(args, flags) {
|
|
152
152
|
const [sub = "status", ...rest] = args;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { VERSION } from "../client.js";
|
|
2
|
+
import { json, out } from "../output.js";
|
|
3
|
+
import { flat, surface } from "../help.js";
|
|
4
|
+
import { VALUES } from "../spec.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `zumino commands [--json]` — the whole surface, in one read.
|
|
8
|
+
*
|
|
9
|
+
* The command an agent runs first. Without it, discovering the CLI means
|
|
10
|
+
* paging through `--help` one command at a time and guessing which values a
|
|
11
|
+
* filter accepts; with `--json` it is a single object carrying every command,
|
|
12
|
+
* every flag, and — the part that matters — every enumerated value, so
|
|
13
|
+
* `--status review` is never sent to find out that the word is `in_review`.
|
|
14
|
+
*
|
|
15
|
+
* It reaches no network and needs no token: the surface is a property of the
|
|
16
|
+
* installed CLI, and the version handshake already says whether that CLI is
|
|
17
|
+
* current.
|
|
18
|
+
*/
|
|
19
|
+
export async function run(args, flags) {
|
|
20
|
+
if (flags.json) {
|
|
21
|
+
json(surface({ version: VERSION, values: VALUES }));
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
out(flat());
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
package/src/commands/epic.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { api } from "../client.js";
|
|
2
2
|
import { resolveContext } from "../config.js";
|
|
3
3
|
import { CliError } from "../errors.js";
|
|
4
|
-
import { clip, json, out, pick,
|
|
4
|
+
import { clip, dim, json, out, pick, safeText, table } from "../output.js";
|
|
5
|
+
import { givenFlag, oneValue } from "../flags.js";
|
|
5
6
|
import { projectPath, resolveItem } from "../items.js";
|
|
7
|
+
import { resolveUser } from "../users.js";
|
|
6
8
|
|
|
7
9
|
/*
|
|
8
10
|
* Epics group tasks, and are not tasks.
|
|
@@ -11,7 +13,7 @@ import { projectPath, resolveItem } from "../items.js";
|
|
|
11
13
|
* so there is deliberately nothing here that nests an epic inside anything.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
const SUB = {
|
|
16
|
+
export const SUB = { list, show, create, edit, status };
|
|
15
17
|
|
|
16
18
|
export async function run(args, flags) {
|
|
17
19
|
const [sub, ...rest] = args;
|
|
@@ -24,38 +26,20 @@ export async function run(args, flags) {
|
|
|
24
26
|
return fn(rest, flags);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// a real address (see `resolveItem`), so what is printed can be fed straight
|
|
38
|
-
// back to `zumino epic show`; the full `KEY-E<n>` form works too and is what
|
|
39
|
-
// `zumino find --kind epic` shows.
|
|
40
|
-
out(`E${res.number} ${safeText(res.title)}`);
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function status(args, flags) {
|
|
45
|
-
const [ref, value] = args;
|
|
46
|
-
if (!ref || !value) throw new CliError("zumino epic status <CODE|N> <open|closed>");
|
|
47
|
-
const ctx = resolveContext(flags);
|
|
48
|
-
const { path } = await resolveItem(ctx, ref, { kind: "epic" });
|
|
49
|
-
const res = pick(await api(ctx, "PATCH", path, { body: { status: value } }), "epic");
|
|
50
|
-
if (flags.json) return json(res), 0;
|
|
51
|
-
out(`${ref} ${res.status}`);
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Every epic in the project.
|
|
31
|
+
*
|
|
32
|
+
* **No filters and no paging, because the endpoint has none** — epics are the
|
|
33
|
+
* handful of outcomes a project is organised around, not its inventory, and a
|
|
34
|
+
* project with enough of them to need a page has stopped using them as
|
|
35
|
+
* groupings. Narrowing them locally would be a filter that looks like the
|
|
36
|
+
* server's and is not, so the answer to "only the open ones" is `--json | jq`,
|
|
37
|
+
* and the answer to "what is under one" is `zumino task list --epic E3`.
|
|
38
|
+
*/
|
|
55
39
|
async function list(args, flags) {
|
|
56
40
|
const ctx = resolveContext(flags);
|
|
57
41
|
const base = await projectPath(ctx);
|
|
58
|
-
const res = await api(ctx, "GET", `${base}/epics
|
|
42
|
+
const res = await api(ctx, "GET", `${base}/epics`);
|
|
59
43
|
const epics = res?.epics ?? [];
|
|
60
44
|
if (flags.json) return json(epics), 0;
|
|
61
45
|
if (epics.length === 0) return out("No epics."), 0;
|
|
@@ -68,12 +52,13 @@ async function list(args, flags) {
|
|
|
68
52
|
]),
|
|
69
53
|
{ head: ["EPIC", "STATUS", "DONE", "TITLE"] },
|
|
70
54
|
);
|
|
55
|
+
out(dim("\nThe work under one: zumino task list --epic E<n>"));
|
|
71
56
|
return 0;
|
|
72
57
|
}
|
|
73
58
|
|
|
74
59
|
async function show(args, flags) {
|
|
75
60
|
const [ref] = args;
|
|
76
|
-
if (!ref) throw new CliError("zumino epic show <CODE
|
|
61
|
+
if (!ref) throw new CliError("zumino epic show <E<n>|CODE>");
|
|
77
62
|
const ctx = resolveContext(flags);
|
|
78
63
|
const { path } = await resolveItem(ctx, ref, { kind: "epic" });
|
|
79
64
|
const res = pick(await api(ctx, "GET", path), "epic");
|
|
@@ -83,3 +68,64 @@ async function show(args, flags) {
|
|
|
83
68
|
if (res.description) out(`\n${safeText(res.description)}`);
|
|
84
69
|
return 0;
|
|
85
70
|
}
|
|
71
|
+
|
|
72
|
+
/** The fields an epic write carries, for `create` and for `edit`. */
|
|
73
|
+
async function fields(ctx, flags) {
|
|
74
|
+
/** @type {Record<string, any>} */
|
|
75
|
+
const body = {};
|
|
76
|
+
for (const name of ["title", "description"]) {
|
|
77
|
+
const v = givenFlag(flags[name]);
|
|
78
|
+
if (v !== undefined) body[name] = v;
|
|
79
|
+
}
|
|
80
|
+
const status = oneValue(flags, "status");
|
|
81
|
+
if (status !== undefined) body.status = status;
|
|
82
|
+
const assignee = oneValue(flags, "assignee");
|
|
83
|
+
if (assignee !== undefined) {
|
|
84
|
+
body.assigneeId = await resolveUser(ctx, assignee, { flag: "--assignee", allowClear: true });
|
|
85
|
+
}
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function create(args, flags) {
|
|
90
|
+
const ctx = resolveContext(flags);
|
|
91
|
+
const title = oneValue(flags, "title") ?? args.join(" ").trim();
|
|
92
|
+
if (!title) throw new CliError("An epic needs a title.", { hint: 'zumino epic create --title "…"' });
|
|
93
|
+
const base = await projectPath(ctx);
|
|
94
|
+
const body = { ...(await fields(ctx, flags)), title };
|
|
95
|
+
const res = pick(await api(ctx, "POST", `${base}/epics`, { body }), "epic");
|
|
96
|
+
if (flags.json) return json(res), 0;
|
|
97
|
+
// An epic shape publishes neither `code` nor `ref` — just a number. `E<n>` is
|
|
98
|
+
// a real address (see `resolveItem`), so what is printed can be fed straight
|
|
99
|
+
// back to `zumino epic show`; the full `KEY-E<n>` form works too and is what
|
|
100
|
+
// `zumino find --kind epic` shows.
|
|
101
|
+
out(`E${res.number} ${safeText(res.title)}`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function edit(args, flags) {
|
|
106
|
+
const [ref] = args;
|
|
107
|
+
if (!ref) throw new CliError("zumino epic edit <E<n>|CODE> [fields]");
|
|
108
|
+
const ctx = resolveContext(flags);
|
|
109
|
+
const body = await fields(ctx, flags);
|
|
110
|
+
if (Object.keys(body).length === 0) {
|
|
111
|
+
throw new CliError("Nothing to change.", {
|
|
112
|
+
hint: "Name at least one field. See: zumino help epic edit",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const { path } = await resolveItem(ctx, ref, { kind: "epic" });
|
|
116
|
+
const res = pick(await api(ctx, "PATCH", path, { body }), "epic");
|
|
117
|
+
if (flags.json) return json(res), 0;
|
|
118
|
+
out(`E${res.number} ${Object.keys(body).join(", ")} written`);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function status(args, flags) {
|
|
123
|
+
const [ref, value] = args;
|
|
124
|
+
if (!ref || !value) throw new CliError("zumino epic status <E<n>|CODE> <open|in_progress|done|wont_do>");
|
|
125
|
+
const ctx = resolveContext(flags);
|
|
126
|
+
const { path } = await resolveItem(ctx, ref, { kind: "epic" });
|
|
127
|
+
const res = pick(await api(ctx, "PATCH", path, { body: { status: value } }), "epic");
|
|
128
|
+
if (flags.json) return json(res), 0;
|
|
129
|
+
out(`${ref} ${res.status}`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
package/src/commands/find.js
CHANGED
|
@@ -1,60 +1,53 @@
|
|
|
1
|
-
import { api } from "../client.js";
|
|
1
|
+
import { api, resolveWorkspace } from "../client.js";
|
|
2
2
|
import { resolveContext } from "../config.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const ITEM_KINDS = ["request", "task", "epic"];
|
|
7
|
-
import { resolveWorkspace } from "../client.js";
|
|
8
|
-
import { clip, codeOf, dim, json, out, table, positiveInt } from "../output.js";
|
|
3
|
+
import { csv, nonNegativeInt, oneValue, positiveInt, searchText } from "../flags.js";
|
|
4
|
+
import { clip, codeOf, dim, json, out, pageLine, table } from "../output.js";
|
|
5
|
+
import { resolveUser } from "../users.js";
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
|
-
* `zumino find
|
|
8
|
+
* `zumino find` — one read across every kind of item.
|
|
9
|
+
*
|
|
10
|
+
* Scoped to a project when one is resolved, and to the whole workspace when
|
|
11
|
+
* not. It reads the spine shape, which deliberately publishes no `status`
|
|
12
|
+
* (`docs/decisions/0003`) — so the STATE column says open or closed, and
|
|
13
|
+
* anything narrower is that kind's own list: `zumino task list --status
|
|
14
|
+
* in_progress`, `zumino request list --status planned`.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Every flag here is a filter the endpoint performs. `--status` and
|
|
17
|
+
* `--priority` are refused with the command that does do them, declared in
|
|
18
|
+
* `spec.js` — they were the two that used to reach the wire, be ignored, and
|
|
19
|
+
* answer 200 with an unfiltered list.
|
|
17
20
|
*/
|
|
18
21
|
export async function run(args, flags) {
|
|
19
22
|
const ctx = resolveContext(flags);
|
|
20
23
|
const workspace = await resolveWorkspace({ ...ctx, project: ctx.project });
|
|
21
|
-
const q = args.join(" ").trim();
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const q = searchText(args, flags);
|
|
26
|
+
// `!== undefined` rather than truthiness: `--assignee ""` is given and empty,
|
|
27
|
+
// and a falsy test would drop it and answer an unfiltered list.
|
|
28
|
+
const assignee =
|
|
29
|
+
flags.assignee !== undefined
|
|
30
|
+
? await resolveUser(ctx, oneValue(flags, "assignee"), { flag: "--assignee" })
|
|
31
|
+
: undefined;
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// keeps only recognised kinds and turns an empty set into `false`, so a wrong
|
|
46
|
-
// value answers 200 with an empty page. The skill prescribes `zumino find` as
|
|
47
|
-
// the duplicate check before filing, and `--kind bug` or `--kind feature` are
|
|
48
|
-
// plausible mistakes — they are real task *types* in this product — so a
|
|
49
|
-
// confident "Nothing matches" is how an agent files a duplicate.
|
|
50
|
-
if (flags.kind && !ITEM_KINDS.includes(flags.kind)) {
|
|
51
|
-
throw new CliError(`--kind takes ${ITEM_KINDS.join(", ")}, not "${flags.kind}".`, {
|
|
52
|
-
hint: "bug and feature are task *types*, not kinds — filter those with zumino task show.",
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
const res = await api(ctx, "GET", path, {
|
|
56
|
-
query: { q: q || undefined, kind: flags.kind, state: flags.state, limit: positiveInt(flags.limit, "limit") },
|
|
57
|
-
});
|
|
33
|
+
const offset = nonNegativeInt(flags.offset, "offset");
|
|
34
|
+
const query = {
|
|
35
|
+
q,
|
|
36
|
+
kind: csv(flags, "kind"),
|
|
37
|
+
state: oneValue(flags, "state"),
|
|
38
|
+
assignee,
|
|
39
|
+
sort: oneValue(flags, "sort"),
|
|
40
|
+
offset,
|
|
41
|
+
limit: positiveInt(flags.limit, "limit"),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// The two scopes are written as two calls rather than one call against a
|
|
45
|
+
// computed path: `test/cli-routes.test.ts` reads these templates out of the
|
|
46
|
+
// source to hold them against the server's own routes, and a path assembled
|
|
47
|
+
// into a variable first is a path nothing checks.
|
|
48
|
+
const res = ctx.project
|
|
49
|
+
? await api(ctx, "GET", `/workspaces/${workspace}/projects/${ctx.project}/items`, { query })
|
|
50
|
+
: await api(ctx, "GET", `/workspaces/${workspace}/items`, { query });
|
|
58
51
|
|
|
59
52
|
const items = res?.items ?? [];
|
|
60
53
|
if (flags.json) {
|
|
@@ -76,8 +69,6 @@ export async function run(args, flags) {
|
|
|
76
69
|
]),
|
|
77
70
|
{ head: ["CODE", "KIND", "STATE", "TITLE", "PROJECT"] },
|
|
78
71
|
);
|
|
79
|
-
|
|
80
|
-
out(dim(`\n${items.length} of ${res.total} — narrow with --kind, or raise --limit.`));
|
|
81
|
-
}
|
|
72
|
+
out(dim(`\n${pageLine(res, items.length, offset)}`));
|
|
82
73
|
return 0;
|
|
83
74
|
}
|