@nickmeriano/task 0.5.0 → 0.7.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 (53) hide show
  1. package/README.md +104 -8
  2. package/dist/claim.d.ts +76 -0
  3. package/dist/claim.d.ts.map +1 -0
  4. package/dist/claim.js +237 -0
  5. package/dist/claim.js.map +1 -0
  6. package/dist/claim.test.d.ts +15 -0
  7. package/dist/claim.test.d.ts.map +1 -0
  8. package/dist/claim.test.js +185 -0
  9. package/dist/claim.test.js.map +1 -0
  10. package/dist/cli.js +304 -17
  11. package/dist/cli.js.map +1 -1
  12. package/dist/file-store.d.ts +113 -0
  13. package/dist/file-store.d.ts.map +1 -0
  14. package/dist/file-store.js +604 -0
  15. package/dist/file-store.js.map +1 -0
  16. package/dist/index.d.ts +7 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/server.d.ts.map +1 -1
  21. package/dist/server.js +11 -7
  22. package/dist/server.js.map +1 -1
  23. package/dist/store.d.ts +55 -15
  24. package/dist/store.d.ts.map +1 -1
  25. package/dist/store.js +63 -29
  26. package/dist/store.js.map +1 -1
  27. package/dist/store.test.d.ts +12 -0
  28. package/dist/store.test.d.ts.map +1 -0
  29. package/dist/store.test.js +252 -0
  30. package/dist/store.test.js.map +1 -0
  31. package/dist/ticket-doc.d.ts +57 -0
  32. package/dist/ticket-doc.d.ts.map +1 -0
  33. package/dist/ticket-doc.js +197 -0
  34. package/dist/ticket-doc.js.map +1 -0
  35. package/dist/types.d.ts +22 -1
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +4 -4
  39. package/skill/SKILL.md +45 -8
  40. package/src/claim.test.ts +242 -0
  41. package/src/claim.ts +302 -0
  42. package/src/cli.ts +313 -20
  43. package/src/file-store.ts +693 -0
  44. package/src/index.ts +42 -4
  45. package/src/server.ts +15 -11
  46. package/src/store.test.ts +305 -0
  47. package/src/store.ts +99 -40
  48. package/src/ticket-doc.ts +226 -0
  49. package/src/types.ts +22 -1
  50. package/ui/dist/assets/index-CXW8uT5f.css +1 -0
  51. package/ui/dist/assets/{index-D_qmmh3D.js → index-oJzomUDL.js} +58 -58
  52. package/ui/dist/index.html +2 -2
  53. package/ui/dist/assets/index-Da14ye1f.css +0 -1
package/src/cli.ts CHANGED
@@ -1,6 +1,8 @@
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
@@ -13,11 +15,22 @@ 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 { ClaimError, claim, claimableTasks, release } from "./claim.ts"
20
+ import { detectRepo, parseSlug, publish, resolveHost } from "./publish.ts"
21
+ import { createTaskServer } from "./server.ts"
22
+ import { initProject, migrateBoard, openBoard } from "./file-store.ts"
23
+ import {
24
+ CONFIG_FILE,
25
+ TASK_DIR,
26
+ boardConfig,
27
+ findBoards,
28
+ findBoardsByPrefix,
29
+ findRoot,
30
+ findScopeRoot,
31
+ type Store,
32
+ } from "./store.ts"
33
+ import { STATUSES, isStatus, type Status, type Task, type TaskPatch } from "./types.ts"
21
34
 
22
35
  // node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
23
36
  // that runs it on every invocation. Filter that one warning, keep the rest.
@@ -62,6 +75,9 @@ const BOOLEAN_FLAGS = new Set([
62
75
  "yes",
63
76
  "needs-human",
64
77
  "no-needs-human",
78
+ "archived",
79
+ "claimable",
80
+ "release",
65
81
  "open",
66
82
  "no-open",
67
83
  "strict-port",
@@ -102,10 +118,68 @@ function fail(message: string): never {
102
118
  process.exit(1)
103
119
  }
104
120
 
105
- function openStore(): TaskStore {
121
+ function openStore(): Store {
106
122
  const root = findRoot(process.cwd())
107
123
  if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
108
- return new TaskStore(root)
124
+ return openBoard(root)
125
+ }
126
+
127
+ /** The prefix a ref carries, if any: "TAS-12" → "TAS", "12" → null. */
128
+ function refPrefix(ref: string): string | null {
129
+ const match = /^([A-Za-z0-9]+)-\d+$/.exec(ref.trim())
130
+ return match ? match[1].toUpperCase() : null
131
+ }
132
+
133
+ /**
134
+ * The board a ref belongs to. A bare number means the nearest board, as ever —
135
+ * but a prefixed id is an address, and it routes: if the prefix isn't the
136
+ * nearest board's, every board in the repo (walking up to the outermost board
137
+ * root, then down) is searched for it, so `task show TAS-12` works from
138
+ * anywhere in a monorepo. What this must never do is what it used to: silently
139
+ * strip a foreign prefix and act on the nearest board's ticket of that number.
140
+ */
141
+ function openStoreFor(ref: string | undefined): Store {
142
+ const prefix = ref ? refPrefix(ref) : null
143
+ if (!prefix) return openStore()
144
+
145
+ const nearest = findRoot(process.cwd())
146
+ if (nearest) {
147
+ const store = openBoard(nearest)
148
+ if (store.config.prefix.toUpperCase() === prefix) return store
149
+ store.close()
150
+ }
151
+
152
+ const scope = findScopeRoot(process.cwd())
153
+ const matches = findBoardsByPrefix(scope, prefix)
154
+ if (matches.length === 1) return openBoard(matches[0].root)
155
+ if (matches.length > 1) {
156
+ fail(`prefix ${prefix} is ambiguous — boards at: ${matches.map((m) => m.id).join(", ")}`)
157
+ }
158
+ const known = findBoards(scope)
159
+ .map((b) => {
160
+ try {
161
+ return `${boardConfig(b.root).prefix} (${b.id})`
162
+ } catch {
163
+ return null
164
+ }
165
+ })
166
+ .filter(Boolean)
167
+ fail(
168
+ `no board with prefix ${prefix} in this repo${known.length ? ` — boards here: ${known.join(", ")}` : ""}`,
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Parse a ref against an already-chosen board, refusing a foreign prefix
174
+ * instead of reinterpreting it — this guards the second id in `task link`,
175
+ * where the board was picked by the first.
176
+ */
177
+ function parseRefOn(store: Store, ref: string): number {
178
+ const prefix = refPrefix(ref)
179
+ if (prefix && prefix !== store.config.prefix.toUpperCase()) {
180
+ fail(`${ref} is not on the ${store.config.prefix} board — links can't cross boards`)
181
+ }
182
+ return store.parseId(ref)
109
183
  }
110
184
 
111
185
  // ── Input normalization ──────────────────────────────────────────────────────
@@ -213,10 +287,29 @@ function cmdAdd(args: Args): void {
213
287
 
214
288
  function cmdList(args: Args): void {
215
289
  const store = openStore()
290
+ if (args.flags.claimable) {
291
+ // The dispatcher's queue view (TAS-21): claimable already pins the status
292
+ // set and consults origin, so the board-shaping flags don't compose.
293
+ for (const flag of ["status", "all", "archived"]) {
294
+ if (args.flags[flag] !== undefined) fail(`--claimable can't be combined with --${flag}`)
295
+ }
296
+ const tasks = claimableTasks(store)
297
+ if (args.flags.json) {
298
+ console.log(JSON.stringify({ tasks }, null, 2))
299
+ } else if (tasks.length === 0) {
300
+ console.log("nothing claimable — no unblocked, unclaimed todo tickets")
301
+ } else {
302
+ table(tasks.map(taskRow))
303
+ }
304
+ return
305
+ }
306
+ const archived = Boolean(args.flags.archived)
216
307
  const statusFlag = str(args.flags, "status")
217
308
  const statuses = statusFlag
218
309
  ? statusFlag.split(",").map(parseStatus)
219
- : args.flags.all
310
+ : // The archive is all done/canceled, and --all already means everything —
311
+ // the open-tickets default only applies to the plain board listing.
312
+ args.flags.all || archived
220
313
  ? undefined
221
314
  : (["backlog", "todo", "in_progress"] as Status[])
222
315
  const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag")
@@ -225,6 +318,7 @@ function cmdList(args: Args): void {
225
318
  tags: tagFlag ? parseTags(tagFlag) : undefined,
226
319
  milestone: str(args.flags, "milestone"),
227
320
  needsHuman: args.flags["needs-human"] ? true : undefined,
321
+ archived: archived || undefined,
228
322
  })
229
323
  // Present in board order: grouped by status column, then position.
230
324
  const order = new Map(STATUSES.map((s, i) => [s, i]))
@@ -232,16 +326,109 @@ function cmdList(args: Args): void {
232
326
  if (args.flags.json) {
233
327
  console.log(JSON.stringify({ tasks }, null, 2))
234
328
  } else if (tasks.length === 0) {
235
- console.log(args.flags.all ? "no tasks" : "no open tasks (--all includes done/canceled)")
329
+ console.log(
330
+ archived
331
+ ? "no archived tasks"
332
+ : args.flags.all
333
+ ? "no tasks"
334
+ : "no open tasks (--all includes done/canceled)",
335
+ )
236
336
  } else {
237
337
  table(tasks.map(taskRow))
238
338
  }
239
339
  }
240
340
 
341
+ /**
342
+ * `task boards` — every board in this repo, found the way `task serve` finds
343
+ * them but anchored at the *outermost* board root, so it answers from anywhere
344
+ * in a monorepo. The `*` marks the board the other commands would target from
345
+ * here.
346
+ */
347
+ function cmdBoards(args: Args): void {
348
+ const cwd = process.cwd()
349
+ const scope = findScopeRoot(cwd)
350
+ const boards = findBoards(scope)
351
+ if (boards.length === 0) {
352
+ fail("no .task directory found in this directory, any parent, or below — run `task init` first")
353
+ }
354
+ const nearest = findRoot(cwd)
355
+ const rows = boards.map((ref) => {
356
+ const store = openBoard(ref.root)
357
+ const open = store.list({ statuses: ["backlog", "todo", "in_progress"] }).length
358
+ store.close()
359
+ return {
360
+ id: ref.id,
361
+ name: store.config.name,
362
+ prefix: store.config.prefix,
363
+ open,
364
+ current: ref.root === nearest,
365
+ }
366
+ })
367
+ if (args.flags.json) {
368
+ console.log(JSON.stringify({ boards: rows }, null, 2))
369
+ } else {
370
+ table(
371
+ rows.map((b) => [
372
+ b.current ? "*" : "",
373
+ b.prefix,
374
+ b.name,
375
+ b.id,
376
+ `${b.open} open`,
377
+ ]),
378
+ )
379
+ }
380
+ }
381
+
382
+ /**
383
+ * `task archive <id>` / `task archive --all` — move finished tickets to
384
+ * `.task/archive/`, out of the board and off every hot path. History, not a
385
+ * hiding place: only done/canceled tickets qualify, everything stays readable
386
+ * via `show` and `list --archived`, and `task unarchive` puts one back.
387
+ */
388
+ function cmdArchive(args: Args): void {
389
+ const ref = args.positional[0]
390
+ if (args.flags.all && ref) fail("pass an id or --all, not both")
391
+ if (!args.flags.all && !ref) fail("usage: task archive <id> | task archive --all")
392
+
393
+ if (ref) {
394
+ const store = openStoreFor(ref)
395
+ const task = store.archive(store.parseId(ref))
396
+ if (args.flags.json) {
397
+ console.log(JSON.stringify({ task }, null, 2))
398
+ } else {
399
+ console.log(`archived ${task.id} ${task.title}`)
400
+ }
401
+ return
402
+ }
403
+
404
+ const store = openStore()
405
+ const finished = store.list({ statuses: ["done", "canceled"] })
406
+ const archived = finished.map((t) => store.archive(t.number))
407
+ if (args.flags.json) {
408
+ console.log(JSON.stringify({ archived }, null, 2))
409
+ } else if (archived.length === 0) {
410
+ console.log("nothing to archive — no done or canceled tasks on the board")
411
+ } else {
412
+ for (const task of archived) console.log(`archived ${task.id} ${task.title}`)
413
+ }
414
+ }
415
+
416
+ function cmdUnarchive(args: Args): void {
417
+ const ref = args.positional[0]
418
+ if (!ref) fail("usage: task unarchive <id>")
419
+ const store = openStoreFor(ref)
420
+ const task = store.unarchive(store.parseId(ref))
421
+ if (args.flags.json) {
422
+ console.log(JSON.stringify({ task }, null, 2))
423
+ } else {
424
+ printTask(task)
425
+ }
426
+ }
427
+
241
428
  function cmdShow(args: Args): void {
242
429
  const ref = args.positional[0]
243
430
  if (!ref) fail("usage: task show <id>")
244
- const store = openStore()
431
+ const store = openStoreFor(ref)
245
432
  const number = store.parseId(ref)
246
433
  const task = store.get(number)
247
434
  if (!task) fail(`no such task: ${store.displayId(number)}`)
@@ -260,6 +447,7 @@ function cmdShow(args: Args): void {
260
447
  .join(", ")
261
448
  console.log(`${task.id} ${task.title}`)
262
449
  console.log(`status ${task.status}`)
450
+ if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
263
451
  if (task.needsHuman) console.log(`needs a human`)
264
452
  if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
265
453
  if (task.milestone) console.log(`milestone ${task.milestone}`)
@@ -281,7 +469,7 @@ function cmdShow(args: Args): void {
281
469
  function cmdUpdate(args: Args, forcedStatus?: Status): void {
282
470
  const ref = args.positional[0]
283
471
  if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
284
- const store = openStore()
472
+ const store = openStoreFor(ref)
285
473
  const number = store.parseId(ref)
286
474
  const patch = patchFromFlags(args.flags)
287
475
  if (forcedStatus) patch.status = forcedStatus
@@ -310,7 +498,7 @@ function cmdComment(args: Args): void {
310
498
  const [ref, ...rest] = args.positional
311
499
  const body = rest.join(" ").trim()
312
500
  if (!ref || !body) fail(`usage: task comment <id> <text> [--author <who>]`)
313
- const store = openStore()
501
+ const store = openStoreFor(ref)
314
502
  const author = resolveAuthor(str(args.flags, "author")).name
315
503
  const comment = store.addComment(store.parseId(ref), body, author)
316
504
  if (args.flags.json) {
@@ -332,9 +520,9 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
332
520
  if (!ref || (blocks ? blockedBy : !blockedBy)) {
333
521
  fail(`usage: task ${action} <id> (--blocks <id> | --blocked-by <id>)`)
334
522
  }
335
- const store = openStore()
523
+ const store = openStoreFor(ref)
336
524
  const relation = blocks ? "blocks" : "blocked_by"
337
- const target = store.parseId((blocks ?? blockedBy)!)
525
+ const target = parseRefOn(store, (blocks ?? blockedBy)!)
338
526
  const number = store.parseId(ref)
339
527
  const task =
340
528
  action === "link" ? store.link(number, relation, target) : store.unlink(number, relation, target)
@@ -350,6 +538,44 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
350
538
  }
351
539
  }
352
540
 
541
+ /**
542
+ * `task claim <id>` / `task claim --release <id>` — see claim.ts for the
543
+ * mechanics. Exit codes are the contract callers script against: 0 claimed,
544
+ * 1 already claimed (pick the next ticket), 2 not claimable (fix something).
545
+ */
546
+ function cmdClaim(args: Args): void {
547
+ const ref = args.positional[0]
548
+ if (!ref) fail("usage: task claim <id> | task claim --release <id>")
549
+ const store = openStoreFor(ref)
550
+ const number = store.parseId(ref)
551
+ try {
552
+ if (args.flags.release) {
553
+ const result = release(store, number)
554
+ if (args.flags.json) {
555
+ console.log(JSON.stringify({ released: result }, null, 2))
556
+ } else if (!result.remote && !result.local) {
557
+ console.log(`${store.displayId(number)} wasn't claimed — no ${result.branch} to delete`)
558
+ } else {
559
+ const where = [result.remote && "origin", result.local && "local"].filter(Boolean)
560
+ console.log(`released ${store.displayId(number)} — deleted ${result.branch} (${where.join(" and ")})`)
561
+ }
562
+ return
563
+ }
564
+ const result = claim(store, number)
565
+ if (args.flags.json) {
566
+ console.log(JSON.stringify({ task: result.task, branch: result.branch }, null, 2))
567
+ } else {
568
+ console.log(`claimed ${result.task.id} — on ${result.branch} (from ${result.base}), status in_progress`)
569
+ }
570
+ } catch (error) {
571
+ if (error instanceof ClaimError) {
572
+ console.error(`error: ${error.message}`)
573
+ process.exit(error.kind === "claimed" ? 1 : 2)
574
+ }
575
+ throw error
576
+ }
577
+ }
578
+
353
579
  const AUTHOR_SOURCE: Record<string, string> = {
354
580
  flag: "--author",
355
581
  env: "$TASK_AUTHOR",
@@ -367,10 +593,34 @@ function cmdWhoami(args: Args): void {
367
593
  }
368
594
  }
369
595
 
596
+ /**
597
+ * `task migrate` — from the legacy committed SQLite database to text-canonical
598
+ * storage: one markdown file per ticket and per comment under .task/tickets/,
599
+ * with the database left on disk as an ignored backup. Additive and safe to
600
+ * re-run planning-wise: it refuses to run twice.
601
+ */
602
+ function cmdMigrate(args: Args): void {
603
+ const root = findRoot(process.cwd())
604
+ if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
605
+ const result = migrateBoard(root)
606
+ if (args.flags.json) {
607
+ console.log(JSON.stringify({ migrated: result }, null, 2))
608
+ return
609
+ }
610
+ console.log(
611
+ `Migrated ${result.tasks} task${result.tasks === 1 ? "" : "s"} and ${result.comments} comment${result.comments === 1 ? "" : "s"} to .task/tickets/`,
612
+ )
613
+ console.log(`tasks.db stays on disk as a backup, but it's ignored now — the files are the state.`)
614
+ console.log(``)
615
+ console.log(`Next:`)
616
+ console.log(` git rm --cached ${join(TASK_DIR, "tasks.db")} stop tracking the database`)
617
+ console.log(` git add ${TASK_DIR} commit the tickets`)
618
+ }
619
+
370
620
  function cmdDelete(args: Args): void {
371
621
  const ref = args.positional[0]
372
622
  if (!ref) fail("usage: task delete <id>")
373
- const store = openStore()
623
+ const store = openStoreFor(ref)
374
624
  const number = store.parseId(ref)
375
625
  store.delete(number)
376
626
  console.log(`deleted ${store.displayId(number)}`)
@@ -560,8 +810,10 @@ function runPublishFlow(
560
810
 
561
811
  const HELP = `task — a task manager that lives in your repo
562
812
 
563
- State is a SQLite database in .task/ at the project root. Commit it: tasks and
564
- their status travel with the code they describe. Any command works from any
813
+ State is plain text in .task/ at the project root one markdown file per
814
+ ticket and per comment under .task/tickets/. Commit it: tasks travel with the
815
+ code they describe, ticket changes show up as readable diffs in PRs, and two
816
+ branches editing different tickets merge cleanly. Any command works from any
565
817
  subdirectory (it walks up to find .task/, like git).
566
818
 
567
819
  Usage
@@ -569,7 +821,7 @@ Usage
569
821
  task add <title> [--description <text>] [--status <s>] [--tags <a,b>]
570
822
  [--milestone <m>] [--needs-human]
571
823
  task list [--status <s1,s2>] [--tag <a,b>] [--milestone <m>]
572
- [--needs-human] [--all]
824
+ [--needs-human] [--all] [--archived] [--claimable]
573
825
  task show <id>
574
826
  task update <id> [--title <t>] [--description <text>] [--status <s>]
575
827
  [--tags <a,b>] [--milestone <m>]
@@ -582,8 +834,34 @@ Usage
582
834
  task link <id> --blocks <id> mark a dependency — one relation, visible from
583
835
  both tasks (A blocked by B ⇔ B blocks A)
584
836
  task unlink <id> (--blocks <id> | --blocked-by <id>)
837
+ task claim <id> claim a ticket before working it: branch
838
+ <claimPrefix><prefix>-<n> off origin's default
839
+ branch, ticket → in_progress as its first
840
+ commit, pushed. The namespace defaults to
841
+ task/claim/ — set "claimPrefix" in
842
+ .task/config.json to change it (e.g.
843
+ "claude/task/", which Claude cloud sessions
844
+ can push). Branch on origin = claimed —
845
+ git's atomic ref creation is the lock, so two
846
+ concurrent claimers can't both win. Exit codes:
847
+ 0 claimed, 1 already claimed, 2 not claimable
848
+ (not todo, blocked, needs-human, dirty tree)
849
+ task claim --release <id> abandon a claim: delete the branch on origin
850
+ and locally — the status flip only lived on
851
+ the branch, so deleting it is the revert
852
+ task list --claimable the claim queue: todo tickets in position
853
+ order, minus blocked / needs-human / already
854
+ claimed on origin. Top entry is next up
585
855
  task comment <id> <text> [--author <who>]
586
856
  task delete <id>
857
+ task boards every board in this repo — prefix, name, path,
858
+ open count; * marks the one commands target here
859
+ task archive <id> move a done/canceled ticket to .task/archive/,
860
+ out of the board and off the hot path — still
861
+ readable via show and list --archived, and its
862
+ number stays reserved
863
+ task archive --all archive everything done or canceled
864
+ task unarchive <id> put an archived ticket back on the board
587
865
  task whoami who your comments are attributed to
588
866
  task serve [--port <n>] [--no-open] [--strict-port]
589
867
  board + table UI with live updates, opened in
@@ -602,9 +880,14 @@ Usage
602
880
  task unpublish [--repo <owner/name>]
603
881
  take that URL down. Removes the board, not the
604
882
  tasks — those are in .task/ either way
883
+ task migrate move a pre-0.6 board off its committed SQLite
884
+ database and onto text files in .task/tickets/.
885
+ The database stays on disk as an ignored backup
605
886
 
606
887
  Values
607
- <id> TAS-12, or just 12
888
+ <id> TAS-12, or just 12. A bare number means the nearest board; a
889
+ prefixed id routes to whichever board in the repo owns that
890
+ prefix, so TAS-12 works from anywhere in a monorepo
608
891
  status ${STATUSES.join(" ")}
609
892
  --tag a,b matches a task carrying *either* tag
610
893
  --needs-human this can't be finished by an agent alone
@@ -654,12 +937,22 @@ function main(): void | Promise<void> {
654
937
  return cmdLink(args, "link")
655
938
  case "unlink":
656
939
  return cmdLink(args, "unlink")
940
+ case "claim":
941
+ return cmdClaim(args)
657
942
  case "comment":
658
943
  return cmdComment(args)
659
944
  case "whoami":
660
945
  return cmdWhoami(args)
661
946
  case "delete":
662
947
  return cmdDelete(args)
948
+ case "boards":
949
+ return cmdBoards(args)
950
+ case "archive":
951
+ return cmdArchive(args)
952
+ case "unarchive":
953
+ return cmdUnarchive(args)
954
+ case "migrate":
955
+ return cmdMigrate(args)
663
956
  case "serve":
664
957
  return cmdServe(args)
665
958
  case "publish":