@nickmeriano/task 0.4.2 → 0.6.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.
Files changed (43) hide show
  1. package/README.md +77 -9
  2. package/dist/cli.js +290 -19
  3. package/dist/cli.js.map +1 -1
  4. package/dist/file-store.d.ts +113 -0
  5. package/dist/file-store.d.ts.map +1 -0
  6. package/dist/file-store.js +604 -0
  7. package/dist/file-store.js.map +1 -0
  8. package/dist/index.d.ts +6 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +30 -7
  14. package/dist/server.js.map +1 -1
  15. package/dist/store.d.ts +69 -16
  16. package/dist/store.d.ts.map +1 -1
  17. package/dist/store.js +169 -39
  18. package/dist/store.js.map +1 -1
  19. package/dist/store.test.d.ts +12 -0
  20. package/dist/store.test.d.ts.map +1 -0
  21. package/dist/store.test.js +252 -0
  22. package/dist/store.test.js.map +1 -0
  23. package/dist/ticket-doc.d.ts +57 -0
  24. package/dist/ticket-doc.d.ts.map +1 -0
  25. package/dist/ticket-doc.js +197 -0
  26. package/dist/ticket-doc.js.map +1 -0
  27. package/dist/types.d.ts +35 -3
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/types.js.map +1 -1
  30. package/package.json +4 -4
  31. package/skill/SKILL.md +40 -9
  32. package/src/cli.ts +304 -22
  33. package/src/file-store.ts +693 -0
  34. package/src/index.ts +30 -4
  35. package/src/server.ts +31 -11
  36. package/src/store.test.ts +305 -0
  37. package/src/store.ts +210 -49
  38. package/src/ticket-doc.ts +226 -0
  39. package/src/types.ts +35 -3
  40. package/ui/dist/assets/index-CXW8uT5f.css +1 -0
  41. package/ui/dist/assets/{index-Dm3ToURf.js → index-oJzomUDL.js} +67 -67
  42. package/ui/dist/index.html +2 -2
  43. package/ui/dist/assets/index-DXFbw9bM.css +0 -1
package/src/cli.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // The `task` CLI — tasks that live in your repo. Zero dependencies: hand-rolled
3
- // flag parsing, node:sqlite for storage, plain text out (--json for agents).
3
+ // flag parsing, plain-text tickets in .task/tickets/ for storage (node:sqlite
4
+ // still reads boards from before `task migrate`), plain text out (--json for
5
+ // agents).
4
6
  //
5
7
  // task init
6
8
  // task add "Wire up webhooks" --tags api,infra --milestone launch
7
9
  // task list --status todo,in_progress
8
- // task start TASK-3 && task done TASK-3
10
+ // task start TAS-3 && task done TAS-3
9
11
  // task serve
10
12
 
11
13
  import { spawn } from "node:child_process"
@@ -13,11 +15,21 @@ import { readFileSync } from "node:fs"
13
15
  import type { Server } from "node:http"
14
16
  import { basename, join } from "node:path"
15
17
  import process from "node:process"
16
- import { resolveAuthor } from "./author.js"
17
- import { detectRepo, parseSlug, publish, resolveHost } from "./publish.js"
18
- import { createTaskServer } from "./server.js"
19
- import { CONFIG_FILE, TASK_DIR, TaskStore, findBoards, findRoot, initProject } from "./store.js"
20
- import { STATUSES, isStatus, type Status, type Task, type TaskPatch } from "./types.js"
18
+ import { resolveAuthor } from "./author.ts"
19
+ import { detectRepo, parseSlug, publish, resolveHost } from "./publish.ts"
20
+ import { createTaskServer } from "./server.ts"
21
+ import { initProject, migrateBoard, openBoard } from "./file-store.ts"
22
+ import {
23
+ CONFIG_FILE,
24
+ TASK_DIR,
25
+ boardConfig,
26
+ findBoards,
27
+ findBoardsByPrefix,
28
+ findRoot,
29
+ findScopeRoot,
30
+ type Store,
31
+ } from "./store.ts"
32
+ import { STATUSES, isStatus, type Status, type Task, type TaskPatch } from "./types.ts"
21
33
 
22
34
  // node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
23
35
  // that runs it on every invocation. Filter that one warning, keep the rest.
@@ -62,6 +74,7 @@ const BOOLEAN_FLAGS = new Set([
62
74
  "yes",
63
75
  "needs-human",
64
76
  "no-needs-human",
77
+ "archived",
65
78
  "open",
66
79
  "no-open",
67
80
  "strict-port",
@@ -102,10 +115,68 @@ function fail(message: string): never {
102
115
  process.exit(1)
103
116
  }
104
117
 
105
- function openStore(): TaskStore {
118
+ function openStore(): Store {
106
119
  const root = findRoot(process.cwd())
107
120
  if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
108
- return new TaskStore(root)
121
+ return openBoard(root)
122
+ }
123
+
124
+ /** The prefix a ref carries, if any: "TAS-12" → "TAS", "12" → null. */
125
+ function refPrefix(ref: string): string | null {
126
+ const match = /^([A-Za-z0-9]+)-\d+$/.exec(ref.trim())
127
+ return match ? match[1].toUpperCase() : null
128
+ }
129
+
130
+ /**
131
+ * The board a ref belongs to. A bare number means the nearest board, as ever —
132
+ * but a prefixed id is an address, and it routes: if the prefix isn't the
133
+ * nearest board's, every board in the repo (walking up to the outermost board
134
+ * root, then down) is searched for it, so `task show TAS-12` works from
135
+ * anywhere in a monorepo. What this must never do is what it used to: silently
136
+ * strip a foreign prefix and act on the nearest board's ticket of that number.
137
+ */
138
+ function openStoreFor(ref: string | undefined): Store {
139
+ const prefix = ref ? refPrefix(ref) : null
140
+ if (!prefix) return openStore()
141
+
142
+ const nearest = findRoot(process.cwd())
143
+ if (nearest) {
144
+ const store = openBoard(nearest)
145
+ if (store.config.prefix.toUpperCase() === prefix) return store
146
+ store.close()
147
+ }
148
+
149
+ const scope = findScopeRoot(process.cwd())
150
+ const matches = findBoardsByPrefix(scope, prefix)
151
+ if (matches.length === 1) return openBoard(matches[0].root)
152
+ if (matches.length > 1) {
153
+ fail(`prefix ${prefix} is ambiguous — boards at: ${matches.map((m) => m.id).join(", ")}`)
154
+ }
155
+ const known = findBoards(scope)
156
+ .map((b) => {
157
+ try {
158
+ return `${boardConfig(b.root).prefix} (${b.id})`
159
+ } catch {
160
+ return null
161
+ }
162
+ })
163
+ .filter(Boolean)
164
+ fail(
165
+ `no board with prefix ${prefix} in this repo${known.length ? ` — boards here: ${known.join(", ")}` : ""}`,
166
+ )
167
+ }
168
+
169
+ /**
170
+ * Parse a ref against an already-chosen board, refusing a foreign prefix
171
+ * instead of reinterpreting it — this guards the second id in `task link`,
172
+ * where the board was picked by the first.
173
+ */
174
+ function parseRefOn(store: Store, ref: string): number {
175
+ const prefix = refPrefix(ref)
176
+ if (prefix && prefix !== store.config.prefix.toUpperCase()) {
177
+ fail(`${ref} is not on the ${store.config.prefix} board — links can't cross boards`)
178
+ }
179
+ return store.parseId(ref)
109
180
  }
110
181
 
111
182
  // ── Input normalization ──────────────────────────────────────────────────────
@@ -134,6 +205,10 @@ function patchFromFlags(flags: Args["flags"]): TaskPatch {
134
205
  if (milestone !== undefined) patch.milestone = milestone.trim() || null
135
206
  if (flags["needs-human"]) patch.needsHuman = true
136
207
  if (flags["no-needs-human"]) patch.needsHuman = false
208
+ // Replace semantics like --tags; `--pr` (append one) is handled per-command
209
+ // because appending needs the task's current list.
210
+ const prs = str(flags, "prs")
211
+ if (prs !== undefined) patch.prs = prs.split(",").map((p) => p.trim()).filter(Boolean)
137
212
  return patch
138
213
  }
139
214
 
@@ -196,7 +271,10 @@ function cmdAdd(args: Args): void {
196
271
  const title = args.positional.join(" ").trim()
197
272
  if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--milestone …] [--needs-human]`)
198
273
  const store = openStore()
199
- const task = store.create({ title, ...patchFromFlags(args.flags) })
274
+ const patch = patchFromFlags(args.flags)
275
+ const pr = str(args.flags, "pr")
276
+ if (pr) patch.prs = [...(patch.prs ?? []), pr]
277
+ const task = store.create({ title, ...patch })
200
278
  if (args.flags.json) {
201
279
  console.log(JSON.stringify({ task }, null, 2))
202
280
  } else {
@@ -206,10 +284,13 @@ function cmdAdd(args: Args): void {
206
284
 
207
285
  function cmdList(args: Args): void {
208
286
  const store = openStore()
287
+ const archived = Boolean(args.flags.archived)
209
288
  const statusFlag = str(args.flags, "status")
210
289
  const statuses = statusFlag
211
290
  ? statusFlag.split(",").map(parseStatus)
212
- : args.flags.all
291
+ : // The archive is all done/canceled, and --all already means everything —
292
+ // the open-tickets default only applies to the plain board listing.
293
+ args.flags.all || archived
213
294
  ? undefined
214
295
  : (["backlog", "todo", "in_progress"] as Status[])
215
296
  const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag")
@@ -218,6 +299,7 @@ function cmdList(args: Args): void {
218
299
  tags: tagFlag ? parseTags(tagFlag) : undefined,
219
300
  milestone: str(args.flags, "milestone"),
220
301
  needsHuman: args.flags["needs-human"] ? true : undefined,
302
+ archived: archived || undefined,
221
303
  })
222
304
  // Present in board order: grouped by status column, then position.
223
305
  const order = new Map(STATUSES.map((s, i) => [s, i]))
@@ -225,16 +307,109 @@ function cmdList(args: Args): void {
225
307
  if (args.flags.json) {
226
308
  console.log(JSON.stringify({ tasks }, null, 2))
227
309
  } else if (tasks.length === 0) {
228
- console.log(args.flags.all ? "no tasks" : "no open tasks (--all includes done/canceled)")
310
+ console.log(
311
+ archived
312
+ ? "no archived tasks"
313
+ : args.flags.all
314
+ ? "no tasks"
315
+ : "no open tasks (--all includes done/canceled)",
316
+ )
229
317
  } else {
230
318
  table(tasks.map(taskRow))
231
319
  }
232
320
  }
233
321
 
322
+ /**
323
+ * `task boards` — every board in this repo, found the way `task serve` finds
324
+ * them but anchored at the *outermost* board root, so it answers from anywhere
325
+ * in a monorepo. The `*` marks the board the other commands would target from
326
+ * here.
327
+ */
328
+ function cmdBoards(args: Args): void {
329
+ const cwd = process.cwd()
330
+ const scope = findScopeRoot(cwd)
331
+ const boards = findBoards(scope)
332
+ if (boards.length === 0) {
333
+ fail("no .task directory found in this directory, any parent, or below — run `task init` first")
334
+ }
335
+ const nearest = findRoot(cwd)
336
+ const rows = boards.map((ref) => {
337
+ const store = openBoard(ref.root)
338
+ const open = store.list({ statuses: ["backlog", "todo", "in_progress"] }).length
339
+ store.close()
340
+ return {
341
+ id: ref.id,
342
+ name: store.config.name,
343
+ prefix: store.config.prefix,
344
+ open,
345
+ current: ref.root === nearest,
346
+ }
347
+ })
348
+ if (args.flags.json) {
349
+ console.log(JSON.stringify({ boards: rows }, null, 2))
350
+ } else {
351
+ table(
352
+ rows.map((b) => [
353
+ b.current ? "*" : "",
354
+ b.prefix,
355
+ b.name,
356
+ b.id,
357
+ `${b.open} open`,
358
+ ]),
359
+ )
360
+ }
361
+ }
362
+
363
+ /**
364
+ * `task archive <id>` / `task archive --all` — move finished tickets to
365
+ * `.task/archive/`, out of the board and off every hot path. History, not a
366
+ * hiding place: only done/canceled tickets qualify, everything stays readable
367
+ * via `show` and `list --archived`, and `task unarchive` puts one back.
368
+ */
369
+ function cmdArchive(args: Args): void {
370
+ const ref = args.positional[0]
371
+ if (args.flags.all && ref) fail("pass an id or --all, not both")
372
+ if (!args.flags.all && !ref) fail("usage: task archive <id> | task archive --all")
373
+
374
+ if (ref) {
375
+ const store = openStoreFor(ref)
376
+ const task = store.archive(store.parseId(ref))
377
+ if (args.flags.json) {
378
+ console.log(JSON.stringify({ task }, null, 2))
379
+ } else {
380
+ console.log(`archived ${task.id} ${task.title}`)
381
+ }
382
+ return
383
+ }
384
+
385
+ const store = openStore()
386
+ const finished = store.list({ statuses: ["done", "canceled"] })
387
+ const archived = finished.map((t) => store.archive(t.number))
388
+ if (args.flags.json) {
389
+ console.log(JSON.stringify({ archived }, null, 2))
390
+ } else if (archived.length === 0) {
391
+ console.log("nothing to archive — no done or canceled tasks on the board")
392
+ } else {
393
+ for (const task of archived) console.log(`archived ${task.id} ${task.title}`)
394
+ }
395
+ }
396
+
397
+ function cmdUnarchive(args: Args): void {
398
+ const ref = args.positional[0]
399
+ if (!ref) fail("usage: task unarchive <id>")
400
+ const store = openStoreFor(ref)
401
+ const task = store.unarchive(store.parseId(ref))
402
+ if (args.flags.json) {
403
+ console.log(JSON.stringify({ task }, null, 2))
404
+ } else {
405
+ printTask(task)
406
+ }
407
+ }
408
+
234
409
  function cmdShow(args: Args): void {
235
410
  const ref = args.positional[0]
236
411
  if (!ref) fail("usage: task show <id>")
237
- const store = openStore()
412
+ const store = openStoreFor(ref)
238
413
  const number = store.parseId(ref)
239
414
  const task = store.get(number)
240
415
  if (!task) fail(`no such task: ${store.displayId(number)}`)
@@ -243,11 +418,23 @@ function cmdShow(args: Args): void {
243
418
  console.log(JSON.stringify({ task, comments }, null, 2))
244
419
  return
245
420
  }
421
+ // A linked task is only as useful as knowing whether it's still in the way.
422
+ const describeLinks = (numbers: number[]): string =>
423
+ numbers
424
+ .map((n) => {
425
+ const other = store.get(n)
426
+ return other ? `${other.id} (${other.status})` : store.displayId(n)
427
+ })
428
+ .join(", ")
246
429
  console.log(`${task.id} ${task.title}`)
247
430
  console.log(`status ${task.status}`)
431
+ if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
248
432
  if (task.needsHuman) console.log(`needs a human`)
249
433
  if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
250
434
  if (task.milestone) console.log(`milestone ${task.milestone}`)
435
+ if (task.blockedBy.length) console.log(`blocked by ${describeLinks(task.blockedBy)}`)
436
+ if (task.blocks.length) console.log(`blocks ${describeLinks(task.blocks)}`)
437
+ for (const pr of task.prs) console.log(`pr ${pr}`)
251
438
  console.log(`created ${task.createdAt}`)
252
439
  console.log(`updated ${task.updatedAt}`)
253
440
  if (task.description) console.log(`\n${task.description}`)
@@ -263,11 +450,18 @@ function cmdShow(args: Args): void {
263
450
  function cmdUpdate(args: Args, forcedStatus?: Status): void {
264
451
  const ref = args.positional[0]
265
452
  if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
266
- const store = openStore()
453
+ const store = openStoreFor(ref)
454
+ const number = store.parseId(ref)
267
455
  const patch = patchFromFlags(args.flags)
268
456
  if (forcedStatus) patch.status = forcedStatus
457
+ const pr = str(args.flags, "pr")
458
+ if (pr) {
459
+ // Append, dedup — `--pr <url>` is "attach this PR", not "replace the list".
460
+ const current = patch.prs ?? store.get(number)?.prs ?? []
461
+ patch.prs = current.includes(pr) ? current : [...current, pr]
462
+ }
269
463
  if (Object.keys(patch).length === 0) fail("nothing to update — pass at least one flag")
270
- const task = store.update(store.parseId(ref), patch)
464
+ const task = store.update(number, patch)
271
465
  if (args.flags.json) {
272
466
  console.log(JSON.stringify({ task }, null, 2))
273
467
  } else {
@@ -285,7 +479,7 @@ function cmdComment(args: Args): void {
285
479
  const [ref, ...rest] = args.positional
286
480
  const body = rest.join(" ").trim()
287
481
  if (!ref || !body) fail(`usage: task comment <id> <text> [--author <who>]`)
288
- const store = openStore()
482
+ const store = openStoreFor(ref)
289
483
  const author = resolveAuthor(str(args.flags, "author")).name
290
484
  const comment = store.addComment(store.parseId(ref), body, author)
291
485
  if (args.flags.json) {
@@ -295,6 +489,36 @@ function cmdComment(args: Args): void {
295
489
  }
296
490
  }
297
491
 
492
+ /**
493
+ * `task link TAS-3 --blocked-by TAS-5` / `task unlink TAS-3 --blocks TAS-9`.
494
+ * One relation with two spellings: either flag writes the same (blocker,
495
+ * blocked) row, so the other task's `task show` reflects it immediately.
496
+ */
497
+ function cmdLink(args: Args, action: "link" | "unlink"): void {
498
+ const ref = args.positional[0]
499
+ const blocks = str(args.flags, "blocks")
500
+ const blockedBy = str(args.flags, "blocked-by")
501
+ if (!ref || (blocks ? blockedBy : !blockedBy)) {
502
+ fail(`usage: task ${action} <id> (--blocks <id> | --blocked-by <id>)`)
503
+ }
504
+ const store = openStoreFor(ref)
505
+ const relation = blocks ? "blocks" : "blocked_by"
506
+ const target = parseRefOn(store, (blocks ?? blockedBy)!)
507
+ const number = store.parseId(ref)
508
+ const task =
509
+ action === "link" ? store.link(number, relation, target) : store.unlink(number, relation, target)
510
+ if (args.flags.json) {
511
+ console.log(JSON.stringify({ task }, null, 2))
512
+ } else {
513
+ const verb = action === "link" ? "now" : "no longer"
514
+ const [subject, object] =
515
+ relation === "blocks"
516
+ ? [task.id, store.displayId(target)]
517
+ : [store.displayId(target), task.id]
518
+ console.log(`${subject} ${verb} blocks ${object}`)
519
+ }
520
+ }
521
+
298
522
  const AUTHOR_SOURCE: Record<string, string> = {
299
523
  flag: "--author",
300
524
  env: "$TASK_AUTHOR",
@@ -312,10 +536,34 @@ function cmdWhoami(args: Args): void {
312
536
  }
313
537
  }
314
538
 
539
+ /**
540
+ * `task migrate` — from the legacy committed SQLite database to text-canonical
541
+ * storage: one markdown file per ticket and per comment under .task/tickets/,
542
+ * with the database left on disk as an ignored backup. Additive and safe to
543
+ * re-run planning-wise: it refuses to run twice.
544
+ */
545
+ function cmdMigrate(args: Args): void {
546
+ const root = findRoot(process.cwd())
547
+ if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
548
+ const result = migrateBoard(root)
549
+ if (args.flags.json) {
550
+ console.log(JSON.stringify({ migrated: result }, null, 2))
551
+ return
552
+ }
553
+ console.log(
554
+ `Migrated ${result.tasks} task${result.tasks === 1 ? "" : "s"} and ${result.comments} comment${result.comments === 1 ? "" : "s"} to .task/tickets/`,
555
+ )
556
+ console.log(`tasks.db stays on disk as a backup, but it's ignored now — the files are the state.`)
557
+ console.log(``)
558
+ console.log(`Next:`)
559
+ console.log(` git rm --cached ${join(TASK_DIR, "tasks.db")} stop tracking the database`)
560
+ console.log(` git add ${TASK_DIR} commit the tickets`)
561
+ }
562
+
315
563
  function cmdDelete(args: Args): void {
316
564
  const ref = args.positional[0]
317
565
  if (!ref) fail("usage: task delete <id>")
318
- const store = openStore()
566
+ const store = openStoreFor(ref)
319
567
  const number = store.parseId(ref)
320
568
  store.delete(number)
321
569
  console.log(`deleted ${store.displayId(number)}`)
@@ -505,8 +753,10 @@ function runPublishFlow(
505
753
 
506
754
  const HELP = `task — a task manager that lives in your repo
507
755
 
508
- State is a SQLite database in .task/ at the project root. Commit it: tasks and
509
- their status travel with the code they describe. Any command works from any
756
+ State is plain text in .task/ at the project root one markdown file per
757
+ ticket and per comment under .task/tickets/. Commit it: tasks travel with the
758
+ code they describe, ticket changes show up as readable diffs in PRs, and two
759
+ branches editing different tickets merge cleanly. Any command works from any
510
760
  subdirectory (it walks up to find .task/, like git).
511
761
 
512
762
  Usage
@@ -514,16 +764,29 @@ Usage
514
764
  task add <title> [--description <text>] [--status <s>] [--tags <a,b>]
515
765
  [--milestone <m>] [--needs-human]
516
766
  task list [--status <s1,s2>] [--tag <a,b>] [--milestone <m>]
517
- [--needs-human] [--all]
767
+ [--needs-human] [--all] [--archived]
518
768
  task show <id>
519
769
  task update <id> [--title <t>] [--description <text>] [--status <s>]
520
770
  [--tags <a,b>] [--milestone <m>]
521
771
  [--needs-human | --no-needs-human]
772
+ [--pr <url>] [--prs <url1,url2>]
522
773
  task move <id> <status> shorthand for update --status
523
774
  task start <id> → in_progress
524
775
  task done <id> → done
776
+ task link <id> --blocked-by <id>
777
+ task link <id> --blocks <id> mark a dependency — one relation, visible from
778
+ both tasks (A blocked by B ⇔ B blocks A)
779
+ task unlink <id> (--blocks <id> | --blocked-by <id>)
525
780
  task comment <id> <text> [--author <who>]
526
781
  task delete <id>
782
+ task boards every board in this repo — prefix, name, path,
783
+ open count; * marks the one commands target here
784
+ task archive <id> move a done/canceled ticket to .task/archive/,
785
+ out of the board and off the hot path — still
786
+ readable via show and list --archived, and its
787
+ number stays reserved
788
+ task archive --all archive everything done or canceled
789
+ task unarchive <id> put an archived ticket back on the board
527
790
  task whoami who your comments are attributed to
528
791
  task serve [--port <n>] [--no-open] [--strict-port]
529
792
  board + table UI with live updates, opened in
@@ -542,13 +805,20 @@ Usage
542
805
  task unpublish [--repo <owner/name>]
543
806
  take that URL down. Removes the board, not the
544
807
  tasks — those are in .task/ either way
808
+ task migrate move a pre-0.6 board off its committed SQLite
809
+ database and onto text files in .task/tickets/.
810
+ The database stays on disk as an ignored backup
545
811
 
546
812
  Values
547
- <id> TASK-12, or just 12
813
+ <id> TAS-12, or just 12. A bare number means the nearest board; a
814
+ prefixed id routes to whichever board in the repo owns that
815
+ prefix, so TAS-12 works from anywhere in a monorepo
548
816
  status ${STATUSES.join(" ")}
549
817
  --tag a,b matches a task carrying *either* tag
550
818
  --needs-human this can't be finished by an agent alone
551
- clearing --tags "" drops all tags, --milestone "" clears it
819
+ --pr <url> attach a pull request (appends); --prs replaces the whole list
820
+ clearing --tags "" drops all tags, --milestone "" clears it, --prs ""
821
+ detaches all PRs
552
822
 
553
823
  Comment authors resolve --author → $TASK_AUTHOR → git config user.name →
554
824
  anonymous, so there is nothing to set up. \`task whoami\` shows which one won.
@@ -588,12 +858,24 @@ function main(): void | Promise<void> {
588
858
  return cmdUpdate(args, "in_progress")
589
859
  case "done":
590
860
  return cmdUpdate(args, "done")
861
+ case "link":
862
+ return cmdLink(args, "link")
863
+ case "unlink":
864
+ return cmdLink(args, "unlink")
591
865
  case "comment":
592
866
  return cmdComment(args)
593
867
  case "whoami":
594
868
  return cmdWhoami(args)
595
869
  case "delete":
596
870
  return cmdDelete(args)
871
+ case "boards":
872
+ return cmdBoards(args)
873
+ case "archive":
874
+ return cmdArchive(args)
875
+ case "unarchive":
876
+ return cmdUnarchive(args)
877
+ case "migrate":
878
+ return cmdMigrate(args)
597
879
  case "serve":
598
880
  return cmdServe(args)
599
881
  case "publish":