@nickmeriano/task 0.5.0 → 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.
- package/README.md +63 -8
- package/dist/cli.js +220 -17
- package/dist/cli.js.map +1 -1
- package/dist/file-store.d.ts +113 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +604 -0
- package/dist/file-store.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -7
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +55 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +63 -29
- package/dist/store.js.map +1 -1
- package/dist/store.test.d.ts +12 -0
- package/dist/store.test.d.ts.map +1 -0
- package/dist/store.test.js +252 -0
- package/dist/store.test.js.map +1 -0
- package/dist/ticket-doc.d.ts +57 -0
- package/dist/ticket-doc.d.ts.map +1 -0
- package/dist/ticket-doc.js +197 -0
- package/dist/ticket-doc.js.map +1 -0
- package/dist/types.d.ts +14 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
- package/skill/SKILL.md +23 -8
- package/src/cli.ts +236 -20
- package/src/file-store.ts +693 -0
- package/src/index.ts +30 -4
- package/src/server.ts +15 -11
- package/src/store.test.ts +305 -0
- package/src/store.ts +99 -40
- package/src/ticket-doc.ts +226 -0
- package/src/types.ts +14 -1
- package/ui/dist/assets/index-CXW8uT5f.css +1 -0
- package/ui/dist/assets/{index-D_qmmh3D.js → index-oJzomUDL.js} +58 -58
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-Da14ye1f.css +0 -1
package/skill/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: tasks
|
|
3
3
|
description: >-
|
|
4
4
|
Work with this repository's built-in task manager (the `task` CLI — a .task/
|
|
5
|
-
directory
|
|
5
|
+
directory of plain-text tickets, from @nickmeriano/task). Use this
|
|
6
6
|
skill whenever the user mentions tasks, tickets, issues, todos, backlog,
|
|
7
7
|
kanban, board, "what's next", "what should I work on", or asks you to plan,
|
|
8
8
|
track, or report progress on work — and also on your own initiative: when you
|
|
@@ -13,11 +13,14 @@ description: >-
|
|
|
13
13
|
|
|
14
14
|
# tasks — the repo's task manager
|
|
15
15
|
|
|
16
|
-
This repository tracks its work in `.task/` at the project root:
|
|
17
|
-
|
|
18
|
-
they describe — when you update a task, the user
|
|
19
|
-
UI at `task serve` updates in realtime), and the
|
|
20
|
-
code, so it survives sessions
|
|
16
|
+
This repository tracks its work in `.task/` at the project root: one markdown
|
|
17
|
+
file per ticket under `.task/tickets/`, driven by the `task` CLI. Tasks live in
|
|
18
|
+
the repo, next to the code they describe — when you update a task, the user
|
|
19
|
+
sees it instantly (the board UI at `task serve` updates in realtime), and the
|
|
20
|
+
state is committed with the code, so it survives sessions, travels with
|
|
21
|
+
branches, and shows up as readable diffs in PRs. Always write through the CLI
|
|
22
|
+
rather than editing ticket files by hand — it keeps numbering, positions and
|
|
23
|
+
relations consistent.
|
|
21
24
|
|
|
22
25
|
Run `task` from anywhere in the repo; it walks up to find `.task/` like git
|
|
23
26
|
finds `.git`. If the CLI isn't on PATH, use `npx @nickmeriano/task` instead.
|
|
@@ -45,6 +48,10 @@ task unlink <id> --blocked-by <id> # remove one (or --blocks)
|
|
|
45
48
|
task update <id> --pr <url> # attach a pull request (appends)
|
|
46
49
|
task comment <id> "text" --author claude
|
|
47
50
|
task delete <id>
|
|
51
|
+
task boards --json # every board in the repo, with prefixes
|
|
52
|
+
task archive <id> --json # done/canceled ticket -> .task/archive/
|
|
53
|
+
task archive --all --json # archive everything done or canceled
|
|
54
|
+
task unarchive <id> --json # put one back on the board
|
|
48
55
|
task whoami # who comments are attributed to
|
|
49
56
|
```
|
|
50
57
|
|
|
@@ -53,11 +60,19 @@ the user can read it from a phone, and take it away again. Both open a browser
|
|
|
53
60
|
and wait for a human, so **never run either on your own initiative**; suggest
|
|
54
61
|
`task publish` if the user wants to see the board remotely.
|
|
55
62
|
|
|
56
|
-
- `<id>` is `PREFIX-12` or just `12`.
|
|
63
|
+
- `<id>` is `PREFIX-12` or just `12`. A bare number means the nearest board;
|
|
64
|
+
a prefixed id routes to whichever board in the repo owns that prefix, so
|
|
65
|
+
`TAS-12` works from any directory. `task boards --json` lists them all.
|
|
57
66
|
- Statuses: `backlog` `todo` `in_progress` `done` `canceled`.
|
|
58
67
|
- `--tag a,b` matches a task carrying *either* tag, not both.
|
|
59
68
|
- Clear a field by passing it empty: `--tags ""`, `--milestone ""`, `--prs ""`.
|
|
60
|
-
- `task list` hides done/canceled by default; `--all` shows everything
|
|
69
|
+
- `task list` hides done/canceled by default; `--all` shows everything, and
|
|
70
|
+
`--archived` lists the archive instead of the board.
|
|
71
|
+
- Archiving is history, not deletion: only done/canceled tickets qualify,
|
|
72
|
+
their numbers stay reserved, and `task show` still reads them. Archived
|
|
73
|
+
tickets refuse edits until unarchived. Don't archive on your own
|
|
74
|
+
initiative — suggest `task archive --all` when finished work is piling up,
|
|
75
|
+
and let the user decide.
|
|
61
76
|
|
|
62
77
|
**Dependencies.** `task link A --blocked-by B` and `task link B --blocks A`
|
|
63
78
|
write the same relation — each task's `task show` lists both its `blocked`
|
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,
|
|
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,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.
|
|
17
|
-
import { detectRepo, parseSlug, publish, resolveHost } from "./publish.
|
|
18
|
-
import { createTaskServer } from "./server.
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
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():
|
|
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
|
|
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 ──────────────────────────────────────────────────────
|
|
@@ -213,10 +284,13 @@ function cmdAdd(args: Args): void {
|
|
|
213
284
|
|
|
214
285
|
function cmdList(args: Args): void {
|
|
215
286
|
const store = openStore()
|
|
287
|
+
const archived = Boolean(args.flags.archived)
|
|
216
288
|
const statusFlag = str(args.flags, "status")
|
|
217
289
|
const statuses = statusFlag
|
|
218
290
|
? statusFlag.split(",").map(parseStatus)
|
|
219
|
-
:
|
|
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
|
|
220
294
|
? undefined
|
|
221
295
|
: (["backlog", "todo", "in_progress"] as Status[])
|
|
222
296
|
const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag")
|
|
@@ -225,6 +299,7 @@ function cmdList(args: Args): void {
|
|
|
225
299
|
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
226
300
|
milestone: str(args.flags, "milestone"),
|
|
227
301
|
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
302
|
+
archived: archived || undefined,
|
|
228
303
|
})
|
|
229
304
|
// Present in board order: grouped by status column, then position.
|
|
230
305
|
const order = new Map(STATUSES.map((s, i) => [s, i]))
|
|
@@ -232,16 +307,109 @@ function cmdList(args: Args): void {
|
|
|
232
307
|
if (args.flags.json) {
|
|
233
308
|
console.log(JSON.stringify({ tasks }, null, 2))
|
|
234
309
|
} else if (tasks.length === 0) {
|
|
235
|
-
console.log(
|
|
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
|
+
)
|
|
236
317
|
} else {
|
|
237
318
|
table(tasks.map(taskRow))
|
|
238
319
|
}
|
|
239
320
|
}
|
|
240
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
|
+
|
|
241
409
|
function cmdShow(args: Args): void {
|
|
242
410
|
const ref = args.positional[0]
|
|
243
411
|
if (!ref) fail("usage: task show <id>")
|
|
244
|
-
const store =
|
|
412
|
+
const store = openStoreFor(ref)
|
|
245
413
|
const number = store.parseId(ref)
|
|
246
414
|
const task = store.get(number)
|
|
247
415
|
if (!task) fail(`no such task: ${store.displayId(number)}`)
|
|
@@ -260,6 +428,7 @@ function cmdShow(args: Args): void {
|
|
|
260
428
|
.join(", ")
|
|
261
429
|
console.log(`${task.id} ${task.title}`)
|
|
262
430
|
console.log(`status ${task.status}`)
|
|
431
|
+
if (task.archived) console.log(`archived yes — \`task unarchive ${task.id}\` to edit`)
|
|
263
432
|
if (task.needsHuman) console.log(`needs a human`)
|
|
264
433
|
if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
|
|
265
434
|
if (task.milestone) console.log(`milestone ${task.milestone}`)
|
|
@@ -281,7 +450,7 @@ function cmdShow(args: Args): void {
|
|
|
281
450
|
function cmdUpdate(args: Args, forcedStatus?: Status): void {
|
|
282
451
|
const ref = args.positional[0]
|
|
283
452
|
if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
|
|
284
|
-
const store =
|
|
453
|
+
const store = openStoreFor(ref)
|
|
285
454
|
const number = store.parseId(ref)
|
|
286
455
|
const patch = patchFromFlags(args.flags)
|
|
287
456
|
if (forcedStatus) patch.status = forcedStatus
|
|
@@ -310,7 +479,7 @@ function cmdComment(args: Args): void {
|
|
|
310
479
|
const [ref, ...rest] = args.positional
|
|
311
480
|
const body = rest.join(" ").trim()
|
|
312
481
|
if (!ref || !body) fail(`usage: task comment <id> <text> [--author <who>]`)
|
|
313
|
-
const store =
|
|
482
|
+
const store = openStoreFor(ref)
|
|
314
483
|
const author = resolveAuthor(str(args.flags, "author")).name
|
|
315
484
|
const comment = store.addComment(store.parseId(ref), body, author)
|
|
316
485
|
if (args.flags.json) {
|
|
@@ -332,9 +501,9 @@ function cmdLink(args: Args, action: "link" | "unlink"): void {
|
|
|
332
501
|
if (!ref || (blocks ? blockedBy : !blockedBy)) {
|
|
333
502
|
fail(`usage: task ${action} <id> (--blocks <id> | --blocked-by <id>)`)
|
|
334
503
|
}
|
|
335
|
-
const store =
|
|
504
|
+
const store = openStoreFor(ref)
|
|
336
505
|
const relation = blocks ? "blocks" : "blocked_by"
|
|
337
|
-
const target = store
|
|
506
|
+
const target = parseRefOn(store, (blocks ?? blockedBy)!)
|
|
338
507
|
const number = store.parseId(ref)
|
|
339
508
|
const task =
|
|
340
509
|
action === "link" ? store.link(number, relation, target) : store.unlink(number, relation, target)
|
|
@@ -367,10 +536,34 @@ function cmdWhoami(args: Args): void {
|
|
|
367
536
|
}
|
|
368
537
|
}
|
|
369
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
|
+
|
|
370
563
|
function cmdDelete(args: Args): void {
|
|
371
564
|
const ref = args.positional[0]
|
|
372
565
|
if (!ref) fail("usage: task delete <id>")
|
|
373
|
-
const store =
|
|
566
|
+
const store = openStoreFor(ref)
|
|
374
567
|
const number = store.parseId(ref)
|
|
375
568
|
store.delete(number)
|
|
376
569
|
console.log(`deleted ${store.displayId(number)}`)
|
|
@@ -560,8 +753,10 @@ function runPublishFlow(
|
|
|
560
753
|
|
|
561
754
|
const HELP = `task — a task manager that lives in your repo
|
|
562
755
|
|
|
563
|
-
State is
|
|
564
|
-
|
|
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
|
|
565
760
|
subdirectory (it walks up to find .task/, like git).
|
|
566
761
|
|
|
567
762
|
Usage
|
|
@@ -569,7 +764,7 @@ Usage
|
|
|
569
764
|
task add <title> [--description <text>] [--status <s>] [--tags <a,b>]
|
|
570
765
|
[--milestone <m>] [--needs-human]
|
|
571
766
|
task list [--status <s1,s2>] [--tag <a,b>] [--milestone <m>]
|
|
572
|
-
[--needs-human] [--all]
|
|
767
|
+
[--needs-human] [--all] [--archived]
|
|
573
768
|
task show <id>
|
|
574
769
|
task update <id> [--title <t>] [--description <text>] [--status <s>]
|
|
575
770
|
[--tags <a,b>] [--milestone <m>]
|
|
@@ -584,6 +779,14 @@ Usage
|
|
|
584
779
|
task unlink <id> (--blocks <id> | --blocked-by <id>)
|
|
585
780
|
task comment <id> <text> [--author <who>]
|
|
586
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
|
|
587
790
|
task whoami who your comments are attributed to
|
|
588
791
|
task serve [--port <n>] [--no-open] [--strict-port]
|
|
589
792
|
board + table UI with live updates, opened in
|
|
@@ -602,9 +805,14 @@ Usage
|
|
|
602
805
|
task unpublish [--repo <owner/name>]
|
|
603
806
|
take that URL down. Removes the board, not the
|
|
604
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
|
|
605
811
|
|
|
606
812
|
Values
|
|
607
|
-
<id> TAS-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
|
|
608
816
|
status ${STATUSES.join(" ")}
|
|
609
817
|
--tag a,b matches a task carrying *either* tag
|
|
610
818
|
--needs-human this can't be finished by an agent alone
|
|
@@ -660,6 +868,14 @@ function main(): void | Promise<void> {
|
|
|
660
868
|
return cmdWhoami(args)
|
|
661
869
|
case "delete":
|
|
662
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)
|
|
663
879
|
case "serve":
|
|
664
880
|
return cmdServe(args)
|
|
665
881
|
case "publish":
|