@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.
- package/README.md +77 -9
- package/dist/cli.js +290 -19
- 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 +30 -7
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +69 -16
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +169 -39
- 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 +35 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
- package/skill/SKILL.md +40 -9
- package/src/cli.ts +304 -22
- package/src/file-store.ts +693 -0
- package/src/index.ts +30 -4
- package/src/server.ts +31 -11
- package/src/store.test.ts +305 -0
- package/src/store.ts +210 -49
- package/src/ticket-doc.ts +226 -0
- package/src/types.ts +35 -3
- package/ui/dist/assets/index-CXW8uT5f.css +1 -0
- package/ui/dist/assets/{index-Dm3ToURf.js → index-oJzomUDL.js} +67 -67
- package/ui/dist/index.html +2 -2
- 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,
|
|
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
|
|
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.
|
|
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 ──────────────────────────────────────────────────────
|
|
@@ -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
|
|
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
|
-
:
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
509
|
-
|
|
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>
|
|
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
|
-
|
|
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":
|