@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 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 both:
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.9.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.9.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
- if (positionals.length > 1) {
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
- if (parsedArgs.has(token.name)) {
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
- if (positionals.length > 1) {
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
- if (parsedArgs.has(token.name)) {
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
- if (positionals.length !== 2) {
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
- if (optionCount > 1) {
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
- if (positionals.length !== 1) {
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
  }
@@ -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
- return `${taskKey}${status} | ${taskColumn.name} | ${taskMatch.title}${description}`;
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: { title: { type: "string" }, description: { type: "string" } },
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
- if (positionals.length > 1) {
42
- quit("Only one task key can be specified");
43
- }
53
+ validatePositionalCount(positionals, 1, "Only one task key can be specified");
44
54
 
45
- const parsedArgs = new Set<string>();
46
- for (const token of tokens) {
47
- if (token.kind !== "option") {
48
- continue;
49
- }
55
+ validateUniqueOptions(tokens, ["add-label", "remove-label"]);
50
56
 
51
- if (parsedArgs.has(token.name)) {
52
- quit(`Option "--${token.name}" can only be specified once`);
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
- parsedArgs.add(token.name);
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
- if (values.title === undefined && values.description === undefined) {
59
- quit("Provide at least one edit flag: --title or --description");
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
- Titles cannot be blank.
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: Array<string>) => {
78
- const projectKey = args[0];
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
- const taskTitle = args[1];
84
- if (taskTitle == null) {
85
- quit("A task needs at least a title");
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 taskDesc = args[2];
89
-
90
- // TODO: do empty strings bypass this check?
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> <title> [description]
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" "Sort the numbers and meet the quarterly quota"`,
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
- return `${t.id.padEnd(longestIdLength)}${status} | ${t.column} | ${t.title}${description}`;
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 deletedTask = task.delete(taskKey);
295
- console.info(`Deleted ${formatTask(deletedTask)}`);
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
+ }