@zinn-dev/cli 0.9.0 → 0.11.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 +48 -3
- package/index.ts +6 -0
- package/package.json +2 -2
- package/src/lib.ts +31 -0
- package/src/routes/label-router.ts +74 -0
- package/src/routes/project-router.ts +5 -31
- package/src/routes/relation-router.ts +5 -13
- package/src/routes/routes.ts +2 -0
- package/src/routes/task-router.ts +135 -56
package/README.md
CHANGED
|
@@ -54,8 +54,8 @@ zinn project column create MDR "Boardlog"
|
|
|
54
54
|
Create a task with a title and an optional description. New tasks enter the project's first column:
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
|
-
zinn task create MDR "Refine 75% of the numbers"
|
|
58
|
-
zinn task create MDR "Review the employee handbook" "Prepare for a 75% Dance Experience"
|
|
57
|
+
zinn task create MDR --title "Refine 75% of the numbers"
|
|
58
|
+
zinn task create MDR --title "Review the employee handbook" --description "Prepare for a 75% Dance Experience"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
Tasks are created with keys such as `MDR-1`. List active tasks, optionally limited to one project, or view one task:
|
|
@@ -68,6 +68,39 @@ zinn task view MDR-1
|
|
|
68
68
|
|
|
69
69
|
When a project key is supplied, the list follows the project's column order and the task order within each column.
|
|
70
70
|
|
|
71
|
+
## Labels
|
|
72
|
+
|
|
73
|
+
Labels are global and reusable in all the available projects.
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
zinn label create urgent --color red --description "Needs attention soon"
|
|
77
|
+
zinn label create review
|
|
78
|
+
zinn label list
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Adding to tasks
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
zinn task create MDR --title "Check the numbers" --label urgent --label review
|
|
85
|
+
zinn task label add MDR-1 urgent review
|
|
86
|
+
zinn task label remove MDR-1 review
|
|
87
|
+
zinn task edit MDR-1 --add-label review --remove-label urgent
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For adding or removing multiple labels at the same time, repeat `--label` on creation, or `--add-label` and `--remove-label` while editing for each label.
|
|
91
|
+
Each value is considered as the label name: `--label "needs review"`, so if you have commas, then they are considered parts of names.
|
|
92
|
+
Label names are case-sensitive, and the whitespace from input is trimmed.
|
|
93
|
+
|
|
94
|
+
Descriptions default to an empty string and the label colors default to white. Available colors are _red_, _green_, _yellow_, _blue_, _magenta_, _cyan_, _white_, and _gray_.
|
|
95
|
+
|
|
96
|
+
Task creation and edits apply content and label changes together, or roll back together on failure.
|
|
97
|
+
Note that label changes also update the task's modification timestamp. Archived tasks can also have their labels edited.
|
|
98
|
+
|
|
99
|
+
Commands like `task view` and task mutation output show attached labels.
|
|
100
|
+
|
|
101
|
+
`zinn label delete urgent` asks for confirmation, then deletes the definition and removes it from every task. Declining confirmation leaves the label and its attachments unchanged.
|
|
102
|
+
Use `task label remove` to remove a label attachment from one task instead. Task/project deletion cleans up label attachments, but does not affect labels themselves.
|
|
103
|
+
|
|
71
104
|
## Task relations
|
|
72
105
|
|
|
73
106
|
```sh
|
|
@@ -91,7 +124,7 @@ Use `task relation create --help` to discover types.
|
|
|
91
124
|
|
|
92
125
|
## Edit tasks
|
|
93
126
|
|
|
94
|
-
Edit a task's title, description, or
|
|
127
|
+
Edit a task's title, description, priority, or labels:
|
|
95
128
|
|
|
96
129
|
```sh
|
|
97
130
|
zinn task edit MDR-1 --title "Meet the quarterly refinement quota"
|
|
@@ -163,3 +196,15 @@ zinn task move --help
|
|
|
163
196
|
The short `-h` form works in the same positions.
|
|
164
197
|
|
|
165
198
|
Running `zinn` without a command shows help
|
|
199
|
+
|
|
200
|
+
## Task priority
|
|
201
|
+
|
|
202
|
+
Priority is optional: `low`, `medium`, `high`, or `urgent`. New tasks have no assigned priority by default.
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
zinn task create MDR --title "Review the numbers" --priority high
|
|
206
|
+
zinn task edit MDR-1 --priority urgent
|
|
207
|
+
zinn task edit MDR-1 --priority none
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use `none` to clear priority. Omitting `--priority` on edits preserves it. Assigned priorities appear in task list and view output; board ordering remains unchanged.
|
package/index.ts
CHANGED
|
@@ -27,6 +27,12 @@ Commands:
|
|
|
27
27
|
task delete
|
|
28
28
|
task relation create
|
|
29
29
|
task relation list
|
|
30
|
+
task relation delete
|
|
31
|
+
task label add
|
|
32
|
+
task label remove
|
|
33
|
+
label create
|
|
34
|
+
label list
|
|
35
|
+
label delete
|
|
30
36
|
|
|
31
37
|
Run zinn <namespace> --help or zinn <command> --help for more information.
|
|
32
38
|
Running zinn without a command shows this help.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zinn-dev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A kanban workflow in the terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bugs": {
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@zinn-dev/core": "0.
|
|
45
|
+
"@zinn-dev/core": "0.11.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/lib.ts
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
|
+
import type { parseArgs } from "node:util";
|
|
2
|
+
|
|
3
|
+
type ParsedTokens = NonNullable<ReturnType<typeof parseArgs>["tokens"]>;
|
|
4
|
+
|
|
1
5
|
export function quit(reason: string): never {
|
|
2
6
|
console.error(reason);
|
|
3
7
|
process.exit(1);
|
|
4
8
|
}
|
|
9
|
+
|
|
10
|
+
export function validatePositionalCount(
|
|
11
|
+
positionals: Array<string>,
|
|
12
|
+
expected: number,
|
|
13
|
+
message: string,
|
|
14
|
+
) {
|
|
15
|
+
if (positionals.length !== expected) {
|
|
16
|
+
quit(message);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateUniqueOptions(tokens: ParsedTokens, repeatableOptions: Array<string> = []) {
|
|
21
|
+
const parsedArgs = new Set<string>();
|
|
22
|
+
|
|
23
|
+
for (const token of tokens) {
|
|
24
|
+
if (token.kind !== "option") {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isRepeatable = repeatableOptions.includes(token.name);
|
|
29
|
+
if (parsedArgs.has(token.name) && !isRepeatable) {
|
|
30
|
+
quit(`Option "--${token.name}" can only be specified once`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parsedArgs.add(token.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RouteDef } from "../types";
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { label, labelCreateSchema, labelDeleteSchema } from "@zinn-dev/core";
|
|
5
|
+
|
|
6
|
+
import { quit, validatePositionalCount, validateUniqueOptions } from "../lib";
|
|
7
|
+
|
|
8
|
+
export const labelRouter = {
|
|
9
|
+
create: {
|
|
10
|
+
run: (args) => {
|
|
11
|
+
const { values, positionals, tokens } = parseArgs({
|
|
12
|
+
args,
|
|
13
|
+
options: { description: { type: "string" }, color: { type: "string" } },
|
|
14
|
+
allowPositionals: true,
|
|
15
|
+
strict: true,
|
|
16
|
+
tokens: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
validatePositionalCount(positionals, 1, "Exactly one label name is required");
|
|
20
|
+
|
|
21
|
+
validateUniqueOptions(tokens);
|
|
22
|
+
|
|
23
|
+
const input = labelCreateSchema.parse({ name: positionals[0], ...values });
|
|
24
|
+
const created = label.create(input);
|
|
25
|
+
|
|
26
|
+
console.info(`${created.name} | ${created.color} | ${created.description}`);
|
|
27
|
+
},
|
|
28
|
+
help: `Usage: zinn label create <name> [--description <text>] [--color <color>]
|
|
29
|
+
|
|
30
|
+
Create a global label. Names are case-sensitive and cannot be blank.
|
|
31
|
+
Description defaults to an empty string; color defaults to white.
|
|
32
|
+
Colors: red, green, yellow, blue, magenta, cyan, white, gray.
|
|
33
|
+
|
|
34
|
+
Example: zinn label create bug --description "Unexpected behavior" --color red`,
|
|
35
|
+
},
|
|
36
|
+
list: {
|
|
37
|
+
run: (args) => {
|
|
38
|
+
parseArgs({ args, strict: true, allowPositionals: false });
|
|
39
|
+
for (const entry of label.getAll()) {
|
|
40
|
+
console.info(`${entry.name} | ${entry.color} | ${entry.description}`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
help: `Usage: zinn label list
|
|
44
|
+
|
|
45
|
+
List all global label definitions.`,
|
|
46
|
+
},
|
|
47
|
+
delete: {
|
|
48
|
+
run: (args) => {
|
|
49
|
+
const { positionals } = parseArgs({ args, strict: true, allowPositionals: true });
|
|
50
|
+
validatePositionalCount(positionals, 1, "Exactly one label name is required");
|
|
51
|
+
const props = labelDeleteSchema.parse({ name: positionals[0] });
|
|
52
|
+
const labelMatch = label.getByName(props.name);
|
|
53
|
+
|
|
54
|
+
if (labelMatch == null) {
|
|
55
|
+
quit(`A label with name "${props.name}" does not exist`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const canDelete = confirm(
|
|
59
|
+
`Are you sure you want to delete label "${labelMatch.name}" and remove it from all tasks?`,
|
|
60
|
+
);
|
|
61
|
+
if (!canDelete) {
|
|
62
|
+
console.info(`Deletion cancelled for label "${labelMatch.name}"`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const deleted = label.delete(props);
|
|
67
|
+
console.info(`Deleted label "${deleted.name}"`);
|
|
68
|
+
},
|
|
69
|
+
help: `Usage: zinn label delete <name>
|
|
70
|
+
|
|
71
|
+
Delete a label definition and remove it from all tasks after confirmation.
|
|
72
|
+
To remove a label from just one task, use zinn task label remove.`,
|
|
73
|
+
},
|
|
74
|
+
} satisfies RouteDef;
|
|
@@ -3,7 +3,7 @@ import type { RouteDef } from "../types";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
|
|
5
5
|
import { column, project } from "@zinn-dev/core";
|
|
6
|
-
import { quit } from "../lib";
|
|
6
|
+
import { quit, validatePositionalCount, validateUniqueOptions } from "../lib";
|
|
7
7
|
|
|
8
8
|
export const projectRouter = {
|
|
9
9
|
edit: {
|
|
@@ -22,22 +22,9 @@ export const projectRouter = {
|
|
|
22
22
|
quit("Project key must be specified");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
quit("Only one project key can be specified");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const parsedArgs = new Set<string>();
|
|
30
|
-
for (const token of tokens) {
|
|
31
|
-
if (token.kind !== "option") {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
25
|
+
validatePositionalCount(positionals, 1, "Only one project key can be specified");
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
quit(`The "--${token.name}" flag can only be specified once`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
parsedArgs.add(token.name);
|
|
40
|
-
}
|
|
27
|
+
validateUniqueOptions(tokens);
|
|
41
28
|
|
|
42
29
|
if (values.name === undefined) {
|
|
43
30
|
quit("Provide an edit flag: --name");
|
|
@@ -72,22 +59,9 @@ Example: zinn project edit MDR --name "Macrodata Refinement"`,
|
|
|
72
59
|
quit("Project key must be specified");
|
|
73
60
|
}
|
|
74
61
|
|
|
75
|
-
|
|
76
|
-
quit("Only one project key can be specified");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const parsedArgs = new Set<string>();
|
|
80
|
-
for (const token of tokens) {
|
|
81
|
-
if (token.kind !== "option") {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
62
|
+
validatePositionalCount(positionals, 1, "Only one project key can be specified");
|
|
84
63
|
|
|
85
|
-
|
|
86
|
-
quit(`The "--${token.name}" flag can only be specified once`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
parsedArgs.add(token.name);
|
|
90
|
-
}
|
|
64
|
+
validateUniqueOptions(tokens);
|
|
91
65
|
|
|
92
66
|
try {
|
|
93
67
|
const createdProject = project.create({ key: projectKey, name: values.name });
|
|
@@ -2,7 +2,7 @@ import type { RouteDef } from "../types";
|
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { relation, relationCreateSchema, task } from "@zinn-dev/core";
|
|
5
|
-
import { quit } from "../lib";
|
|
5
|
+
import { quit, validatePositionalCount, validateUniqueOptions } from "../lib";
|
|
6
6
|
|
|
7
7
|
export function formatRelations(taskKey: string) {
|
|
8
8
|
const taskMatch = task.getByKey(taskKey);
|
|
@@ -47,16 +47,9 @@ function parseRelationArgs(args: string[]) {
|
|
|
47
47
|
tokens: true,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
quit("Exactly two task keys are required: source, target");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const optionCount = tokens.filter((token) => token.kind === "option").length;
|
|
50
|
+
validatePositionalCount(positionals, 2, "Exactly two task keys are required: source, target");
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
// TODO: maybe a reusable option validator can be given expected option keys, and it can spit out unsupported options as well as help for what options are supported
|
|
58
|
-
quit('Option "--type" can only be specified once');
|
|
59
|
-
}
|
|
52
|
+
validateUniqueOptions(tokens);
|
|
60
53
|
|
|
61
54
|
const typeResult = relationCreateSchema.shape.relation_type.safeParse(values.type);
|
|
62
55
|
|
|
@@ -127,10 +120,9 @@ Example: zinn task relation create APP-1 APP-2 --type dependency`,
|
|
|
127
120
|
list: {
|
|
128
121
|
run: (args) => {
|
|
129
122
|
const { positionals } = parseArgs({ args, allowPositionals: true, strict: true });
|
|
130
|
-
|
|
131
|
-
quit("Provide exactly one task key");
|
|
132
|
-
}
|
|
123
|
+
validatePositionalCount(positionals, 1, "Provide exactly one task key");
|
|
133
124
|
const output = formatRelations(positionals[0]!);
|
|
125
|
+
|
|
134
126
|
if (output.length > 0) {
|
|
135
127
|
console.info(output);
|
|
136
128
|
}
|
package/src/routes/routes.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { projectRouter } from "./project-router";
|
|
2
2
|
import { taskRouter } from "./task-router";
|
|
3
|
+
import { labelRouter } from "./label-router";
|
|
3
4
|
|
|
4
5
|
// TODO: lazy load?
|
|
5
6
|
export const routes = {
|
|
6
7
|
project: projectRouter,
|
|
7
8
|
task: taskRouter,
|
|
9
|
+
label: labelRouter,
|
|
8
10
|
};
|
|
@@ -3,9 +3,11 @@ import type { RouteDef } from "../types";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { task, project, column } from "@zinn-dev/core";
|
|
5
5
|
|
|
6
|
-
import { quit } from "../lib";
|
|
6
|
+
import { quit, validatePositionalCount, validateUniqueOptions } from "../lib";
|
|
7
7
|
import { formatRelations, relationRouter } from "./relation-router";
|
|
8
8
|
|
|
9
|
+
// TODO: a CLI row formatter that accepts text columns and joins them with " | " would be useful
|
|
10
|
+
// Reuse it across task list/view and other command output in a separate change.
|
|
9
11
|
function formatTask(
|
|
10
12
|
taskMatch: ReturnType<typeof task.getByKey>,
|
|
11
13
|
options: { showsStatus?: boolean } = {},
|
|
@@ -17,16 +19,26 @@ function formatTask(
|
|
|
17
19
|
: "";
|
|
18
20
|
const description = taskMatch.description == null ? "" : ` | ${taskMatch.description}`;
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const priority = taskMatch.priority == null ? "" : ` | Priority: ${taskMatch.priority}`;
|
|
23
|
+
const labels = task.label.getAll(taskKey);
|
|
24
|
+
const labelText =
|
|
25
|
+
labels.length === 0 ? "" : ` | Labels: ${labels.map((label) => label.name).join(", ")}`;
|
|
26
|
+
|
|
27
|
+
return `${taskKey}${status} | ${taskColumn.name} | ${taskMatch.title}${description}${priority}${labelText}`;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export const taskRouter = {
|
|
24
|
-
relation: relationRouter,
|
|
25
31
|
edit: {
|
|
26
32
|
run: (args) => {
|
|
27
33
|
const { values, positionals, tokens } = parseArgs({
|
|
28
34
|
args,
|
|
29
|
-
options: {
|
|
35
|
+
options: {
|
|
36
|
+
title: { type: "string" },
|
|
37
|
+
description: { type: "string" },
|
|
38
|
+
priority: { type: "string" },
|
|
39
|
+
"add-label": { type: "string", multiple: true },
|
|
40
|
+
"remove-label": { type: "string", multiple: true },
|
|
41
|
+
},
|
|
30
42
|
allowPositionals: true,
|
|
31
43
|
strict: true,
|
|
32
44
|
tokens: true,
|
|
@@ -38,81 +50,99 @@ export const taskRouter = {
|
|
|
38
50
|
quit("Task key must be specified");
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
quit("Only one task key can be specified");
|
|
43
|
-
}
|
|
53
|
+
validatePositionalCount(positionals, 1, "Only one task key can be specified");
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
for (const token of tokens) {
|
|
47
|
-
if (token.kind !== "option") {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
55
|
+
validateUniqueOptions(tokens, ["add-label", "remove-label"]);
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const hasEditFlags =
|
|
58
|
+
values.title !== undefined ||
|
|
59
|
+
values.description !== undefined ||
|
|
60
|
+
values.priority !== undefined ||
|
|
61
|
+
values["add-label"] !== undefined ||
|
|
62
|
+
values["remove-label"] !== undefined;
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
if (!hasEditFlags) {
|
|
65
|
+
quit(
|
|
66
|
+
"Provide at least one edit flag: --title, --description, --priority, --add-label, or --remove-label",
|
|
67
|
+
);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
const editedTask = task.edit({
|
|
71
|
+
taskKey,
|
|
72
|
+
title: values.title,
|
|
73
|
+
description: values.description,
|
|
74
|
+
priority: parsePriority(values.priority),
|
|
75
|
+
addLabelNames: values["add-label"],
|
|
76
|
+
removeLabelNames: values["remove-label"],
|
|
77
|
+
});
|
|
61
78
|
|
|
62
|
-
const editedTask = task.edit({ taskKey, ...values });
|
|
63
79
|
console.info(formatTask(editedTask));
|
|
64
80
|
},
|
|
65
|
-
help: `Usage: zinn task edit <task-key> [--title <text>] [--description <text>]
|
|
81
|
+
help: `Usage: zinn task edit <task-key> [--title <text>] [--description <text>] [--priority <low|medium|high|urgent|none>] [--add-label <name>] [--remove-label <name>]
|
|
66
82
|
|
|
67
83
|
Change the supplied fields and preserve everything else.
|
|
84
|
+
Use --priority none to clear priority.
|
|
68
85
|
Provide at least one edit flag. Use --description "" for an empty description.
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
Repeat --add-label or --remove-label for multiple existing labels.
|
|
88
|
+
Adding an attached label or removing an unattached label does not do anything.
|
|
71
89
|
Archived tasks can be edited. Unchanged values leave the task unchanged.
|
|
90
|
+
|
|
91
|
+
The same label cannot be both added and removed.
|
|
92
|
+
Titles cannot be blank.
|
|
93
|
+
|
|
72
94
|
Use --title="--example" for text beginning with a dash.
|
|
73
95
|
|
|
74
96
|
Example: zinn task edit MDR-1 --title "Meet the quarterly refinement quota"`,
|
|
75
97
|
},
|
|
76
98
|
create: {
|
|
77
|
-
run: (args
|
|
78
|
-
const
|
|
99
|
+
run: (args) => {
|
|
100
|
+
const { values, positionals, tokens } = parseArgs({
|
|
101
|
+
args,
|
|
102
|
+
options: {
|
|
103
|
+
title: { type: "string" },
|
|
104
|
+
description: { type: "string" },
|
|
105
|
+
priority: { type: "string" },
|
|
106
|
+
label: { type: "string", multiple: true },
|
|
107
|
+
},
|
|
108
|
+
allowPositionals: true,
|
|
109
|
+
strict: true,
|
|
110
|
+
tokens: true,
|
|
111
|
+
});
|
|
112
|
+
const projectKey = positionals[0];
|
|
79
113
|
if (projectKey == null) {
|
|
80
114
|
quit("A task needs to belong to a project");
|
|
81
115
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
validatePositionalCount(
|
|
117
|
+
positionals,
|
|
118
|
+
1,
|
|
119
|
+
"Only one project key can be specified; use --title and --description",
|
|
120
|
+
);
|
|
121
|
+
if (values.title === undefined) {
|
|
122
|
+
quit("A task needs at least a title: use --title");
|
|
86
123
|
}
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const standardizedKey = project.standardizeKey(projectKey);
|
|
94
|
-
// TODO: `getByKey` already standardizes the key, but to display it in standardized format, I also used it here. Should it run twice on the same thing?
|
|
95
|
-
const projectMatch = project.getByKey(standardizedKey);
|
|
96
|
-
if (projectMatch == null) {
|
|
97
|
-
quit(`Project with key "${standardizedKey}" does not exist`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// TODO: should it non-null (??) or non-falsy (||) check?
|
|
101
|
-
const createdTask = task.create({
|
|
102
|
-
project_id: projectMatch.id,
|
|
103
|
-
title: taskTitle,
|
|
104
|
-
description: taskDesc ?? null,
|
|
105
|
-
});
|
|
106
|
-
console.info(formatTask(createdTask));
|
|
107
|
-
} catch (err: any) {
|
|
108
|
-
quit(err?.message ?? "Something went wrong");
|
|
124
|
+
validateUniqueOptions(tokens, ["label"]);
|
|
125
|
+
const projectMatch = project.getByKey(projectKey);
|
|
126
|
+
if (projectMatch == null) {
|
|
127
|
+
quit(`Project with key "${project.standardizeKey(projectKey)}" does not exist`);
|
|
109
128
|
}
|
|
129
|
+
const createdTask = task.create({
|
|
130
|
+
project_id: projectMatch.id,
|
|
131
|
+
title: values.title,
|
|
132
|
+
description: values.description ?? null,
|
|
133
|
+
priority: parsePriority(values.priority),
|
|
134
|
+
labelNames: values.label,
|
|
135
|
+
});
|
|
136
|
+
console.info(formatTask(createdTask));
|
|
110
137
|
},
|
|
111
|
-
help: `Usage: zinn task create <project-key> <
|
|
138
|
+
help: `Usage: zinn task create <project-key> --title <text> [--description <text>] [--priority <low|medium|high|urgent|none>] [--label <name>]
|
|
112
139
|
|
|
113
140
|
Create a task in the project's first column.
|
|
141
|
+
Priority defaults to none (unassigned).
|
|
142
|
+
Repeat --label to attach multiple existing labels. Unknown labels are errors.
|
|
143
|
+
Titles cannot be blank. Use --title="--example" for text beginning with a dash.
|
|
114
144
|
|
|
115
|
-
Example: zinn task create MDR "Refine the numbers" "
|
|
145
|
+
Example: zinn task create MDR --title "Refine the numbers" --description "Meet the quarterly quota" --label urgent`,
|
|
116
146
|
},
|
|
117
147
|
list: {
|
|
118
148
|
run: (args) => {
|
|
@@ -147,6 +177,7 @@ Example: zinn task create MDR "Refine the numbers" "Sort the numbers and meet th
|
|
|
147
177
|
status: t.archived_at == null ? "Active" : "Archived",
|
|
148
178
|
title: t.title,
|
|
149
179
|
description: t.description,
|
|
180
|
+
priority: t.priority,
|
|
150
181
|
column: taskColumn.name,
|
|
151
182
|
};
|
|
152
183
|
});
|
|
@@ -166,7 +197,8 @@ Example: zinn task create MDR "Refine the numbers" "Sort the numbers and meet th
|
|
|
166
197
|
// TODO: console output formatting for standardizied output
|
|
167
198
|
const status = showsAll ? ` | ${t.status.padEnd(longestStatusLength)}` : "";
|
|
168
199
|
const description = t.description == null ? "" : ` | ${t.description}`;
|
|
169
|
-
|
|
200
|
+
const priority = t.priority == null ? "" : ` | Priority: ${t.priority}`;
|
|
201
|
+
return `${t.id.padEnd(longestIdLength)}${status} | ${t.column} | ${t.title}${description}${priority}`;
|
|
170
202
|
})
|
|
171
203
|
.join("\n"),
|
|
172
204
|
);
|
|
@@ -196,7 +228,7 @@ Example: zinn task list MDR --all`,
|
|
|
196
228
|
},
|
|
197
229
|
help: `Usage: zinn task view <task-key>
|
|
198
230
|
|
|
199
|
-
Show a task with its current column, description, and relations.
|
|
231
|
+
Show a task with its current column, description, priority, labels, and relations.
|
|
200
232
|
|
|
201
233
|
Example: zinn task view MDR-1`,
|
|
202
234
|
},
|
|
@@ -291,8 +323,9 @@ Example: zinn task order MDR-2 before MDR-1`,
|
|
|
291
323
|
const canDelete = confirm(`Are you sure you want to delete ${taskKey}?`);
|
|
292
324
|
|
|
293
325
|
if (canDelete) {
|
|
294
|
-
const
|
|
295
|
-
|
|
326
|
+
const formattedTask = formatTask(taskMatch);
|
|
327
|
+
task.delete(taskKey);
|
|
328
|
+
console.info(`Deleted ${formattedTask}`);
|
|
296
329
|
} else {
|
|
297
330
|
console.info(`Deletion cancelled for ${formatTask(taskMatch)}`);
|
|
298
331
|
}
|
|
@@ -340,4 +373,50 @@ Unarchive a task at the bottom of its previous column.
|
|
|
340
373
|
|
|
341
374
|
Example: zinn task unarchive MDR-1`,
|
|
342
375
|
},
|
|
376
|
+
relation: relationRouter,
|
|
377
|
+
label: {
|
|
378
|
+
add: {
|
|
379
|
+
run: (args) => {
|
|
380
|
+
const props = parseTaskLabelArgs(args);
|
|
381
|
+
console.info(formatTask(task.label.add(props)));
|
|
382
|
+
},
|
|
383
|
+
help: `Usage: zinn task label add <task-key> <label-name> [label-name ...]
|
|
384
|
+
|
|
385
|
+
Add existing labels to a task. Already attached labels are unchanged.`,
|
|
386
|
+
},
|
|
387
|
+
remove: {
|
|
388
|
+
run: (args) => {
|
|
389
|
+
const props = parseTaskLabelArgs(args);
|
|
390
|
+
console.info(formatTask(task.label.remove(props)));
|
|
391
|
+
},
|
|
392
|
+
help: `Usage: zinn task label remove <task-key> <label-name> [label-name ...]
|
|
393
|
+
|
|
394
|
+
Remove existing labels from a task. Unattached labels are unchanged.
|
|
395
|
+
Label definitions are preserved.`,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
343
398
|
} satisfies RouteDef;
|
|
399
|
+
|
|
400
|
+
function parseTaskLabelArgs(args: string[]) {
|
|
401
|
+
const { positionals } = parseArgs({ args, strict: true, allowPositionals: true });
|
|
402
|
+
if (positionals.length < 2) {
|
|
403
|
+
quit("Provide a task key and at least one label name");
|
|
404
|
+
}
|
|
405
|
+
return { taskKey: positionals[0]!, labelNames: positionals.slice(1) };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function parsePriority(value: string | undefined) {
|
|
409
|
+
switch (value) {
|
|
410
|
+
case undefined:
|
|
411
|
+
return undefined;
|
|
412
|
+
case "none":
|
|
413
|
+
return null;
|
|
414
|
+
case "low":
|
|
415
|
+
case "medium":
|
|
416
|
+
case "high":
|
|
417
|
+
case "urgent":
|
|
418
|
+
return value;
|
|
419
|
+
default:
|
|
420
|
+
quit("Priority must be low, medium, high, urgent, or none");
|
|
421
|
+
}
|
|
422
|
+
}
|