@nickmeriano/task 0.1.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/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/author.d.ts +17 -0
- package/dist/author.d.ts.map +1 -0
- package/dist/author.js +52 -0
- package/dist/author.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +415 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +292 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +63 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +346 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +81 -0
- package/skill/SKILL.md +109 -0
- package/src/author.ts +57 -0
- package/src/cli.ts +419 -0
- package/src/index.ts +6 -0
- package/src/server.ts +311 -0
- package/src/store.ts +418 -0
- package/src/types.ts +75 -0
- package/ui/dist/assets/index-BopXdeSy.js +229 -0
- package/ui/dist/assets/index-Cl_P2tLU.css +1 -0
- package/ui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/ui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/ui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/ui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/ui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/ui/dist/icon.svg +4 -0
- package/ui/dist/index.html +21 -0
package/src/author.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process"
|
|
2
|
+
import process from "node:process"
|
|
3
|
+
|
|
4
|
+
export type AuthorSource = "flag" | "env" | "git" | "fallback"
|
|
5
|
+
|
|
6
|
+
export interface Author {
|
|
7
|
+
name: string
|
|
8
|
+
source: AuthorSource
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ANONYMOUS = "anonymous"
|
|
12
|
+
|
|
13
|
+
/** Resolved at most once per process — git config doesn't change mid-command. */
|
|
14
|
+
const gitNameCache = new Map<string, string | null>()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ask git who you are. Every way this can fail — git not installed, not a
|
|
18
|
+
* repository, `user.name` never set — is the same non-answer, so they all
|
|
19
|
+
* collapse to null rather than being told apart.
|
|
20
|
+
*/
|
|
21
|
+
function gitName(cwd: string): string | null {
|
|
22
|
+
const cached = gitNameCache.get(cwd)
|
|
23
|
+
if (cached !== undefined) return cached
|
|
24
|
+
let name: string | null = null
|
|
25
|
+
try {
|
|
26
|
+
const out = execFileSync("git", ["config", "--get", "user.name"], {
|
|
27
|
+
cwd,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
timeout: 1000,
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
31
|
+
})
|
|
32
|
+
name = out.trim() || null
|
|
33
|
+
} catch {
|
|
34
|
+
name = null
|
|
35
|
+
}
|
|
36
|
+
gitNameCache.set(cwd, name)
|
|
37
|
+
return name
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Who is writing this comment. No config file: git already knows your name and
|
|
42
|
+
* it's the same one your commits carry, so attribution costs zero setup.
|
|
43
|
+
*
|
|
44
|
+
* --author <who> → $TASK_AUTHOR → git config user.name → "anonymous"
|
|
45
|
+
*
|
|
46
|
+
* Agents keep passing `--author claude` explicitly, which is what the first
|
|
47
|
+
* step is for.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveAuthor(flag?: string, cwd: string = process.cwd()): Author {
|
|
50
|
+
const flagged = flag?.trim()
|
|
51
|
+
if (flagged) return { name: flagged, source: "flag" }
|
|
52
|
+
const env = process.env.TASK_AUTHOR?.trim()
|
|
53
|
+
if (env) return { name: env, source: "env" }
|
|
54
|
+
const git = gitName(cwd)
|
|
55
|
+
if (git) return { name: git, source: "git" }
|
|
56
|
+
return { name: ANONYMOUS, source: "fallback" }
|
|
57
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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).
|
|
4
|
+
//
|
|
5
|
+
// task init
|
|
6
|
+
// task add "Wire up webhooks" --tags api,infra --milestone launch
|
|
7
|
+
// task list --status todo,in_progress
|
|
8
|
+
// task start TASK-3 && task done TASK-3
|
|
9
|
+
// task serve
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs"
|
|
12
|
+
import { basename, join } from "node:path"
|
|
13
|
+
import process from "node:process"
|
|
14
|
+
import { resolveAuthor } from "./author.js"
|
|
15
|
+
import { createTaskServer } from "./server.js"
|
|
16
|
+
import { CONFIG_FILE, TASK_DIR, TaskStore, findBoards, findRoot, initProject } from "./store.js"
|
|
17
|
+
import { STATUSES, isStatus, type Status, type Task, type TaskPatch } from "./types.js"
|
|
18
|
+
|
|
19
|
+
// node:sqlite still emits an ExperimentalWarning on Node 22 — noise in a CLI
|
|
20
|
+
// that runs it on every invocation. Filter that one warning, keep the rest.
|
|
21
|
+
process.removeAllListeners("warning")
|
|
22
|
+
process.on("warning", (warning) => {
|
|
23
|
+
if (warning.name !== "ExperimentalWarning") console.error(warning)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const VERSION = "0.1.0"
|
|
27
|
+
const DEFAULT_PORT = 4400
|
|
28
|
+
|
|
29
|
+
// ── Argument parsing: positionals + --flag value / --flag ────────────────────
|
|
30
|
+
|
|
31
|
+
interface Args {
|
|
32
|
+
positional: string[]
|
|
33
|
+
flags: Record<string, string | boolean>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Every valueless flag has to be listed here: an unregistered one swallows the
|
|
37
|
+
// next argv token, so `task list --needs-human todo` would silently eat `todo`.
|
|
38
|
+
const BOOLEAN_FLAGS = new Set([
|
|
39
|
+
"json",
|
|
40
|
+
"all",
|
|
41
|
+
"help",
|
|
42
|
+
"version",
|
|
43
|
+
"yes",
|
|
44
|
+
"needs-human",
|
|
45
|
+
"no-needs-human",
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
function parseArgs(argv: string[]): Args {
|
|
49
|
+
const positional: string[] = []
|
|
50
|
+
const flags: Record<string, string | boolean> = {}
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const arg = argv[i]
|
|
53
|
+
if (arg.startsWith("--")) {
|
|
54
|
+
const name = arg.slice(2)
|
|
55
|
+
const eq = name.indexOf("=")
|
|
56
|
+
if (eq >= 0) {
|
|
57
|
+
flags[name.slice(0, eq)] = name.slice(eq + 1)
|
|
58
|
+
} else if (BOOLEAN_FLAGS.has(name) || i + 1 >= argv.length || argv[i + 1].startsWith("--")) {
|
|
59
|
+
flags[name] = true
|
|
60
|
+
} else {
|
|
61
|
+
flags[name] = argv[++i]
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
positional.push(arg)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { positional, flags }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function str(flags: Args["flags"], name: string): string | undefined {
|
|
71
|
+
const v = flags[name]
|
|
72
|
+
return typeof v === "string" ? v : undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fail(message: string): never {
|
|
76
|
+
console.error(`error: ${message}`)
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openStore(): TaskStore {
|
|
81
|
+
const root = findRoot(process.cwd())
|
|
82
|
+
if (!root) fail("no .task directory found in this directory or any parent — run `task init` first")
|
|
83
|
+
return new TaskStore(root)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Input normalization ──────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function parseStatus(value: string): Status {
|
|
89
|
+
const normalized = value.toLowerCase().replace(/-/g, "_")
|
|
90
|
+
if (!isStatus(normalized)) fail(`invalid status "${value}" — one of: ${STATUSES.join(", ")}`)
|
|
91
|
+
return normalized
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseTags(value: string): string[] {
|
|
95
|
+
return value.split(",").map((t) => t.trim()).filter(Boolean)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function patchFromFlags(flags: Args["flags"]): TaskPatch {
|
|
99
|
+
const patch: TaskPatch = {}
|
|
100
|
+
const title = str(flags, "title")
|
|
101
|
+
if (title !== undefined) patch.title = title
|
|
102
|
+
const description = str(flags, "description") ?? str(flags, "desc")
|
|
103
|
+
if (description !== undefined) patch.description = description
|
|
104
|
+
const status = str(flags, "status")
|
|
105
|
+
if (status !== undefined) patch.status = parseStatus(status)
|
|
106
|
+
const tags = str(flags, "tags") ?? str(flags, "tag")
|
|
107
|
+
if (tags !== undefined) patch.tags = parseTags(tags)
|
|
108
|
+
const milestone = str(flags, "milestone")
|
|
109
|
+
if (milestone !== undefined) patch.milestone = milestone.trim() || null
|
|
110
|
+
if (flags["needs-human"]) patch.needsHuman = true
|
|
111
|
+
if (flags["no-needs-human"]) patch.needsHuman = false
|
|
112
|
+
return patch
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Output helpers ───────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
const STATUS_GLYPH: Record<Status, string> = {
|
|
118
|
+
backlog: "◌",
|
|
119
|
+
todo: "○",
|
|
120
|
+
in_progress: "◐",
|
|
121
|
+
done: "●",
|
|
122
|
+
canceled: "⊘",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function table(rows: string[][]): void {
|
|
126
|
+
if (rows.length === 0) return
|
|
127
|
+
const widths = rows[0].map((_, col) => Math.max(...rows.map((r) => (r[col] ?? "").length)))
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
console.log(row.map((cell, col) => (cell ?? "").padEnd(widths[col])).join(" ").trimEnd())
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function taskRow(t: Task): string[] {
|
|
134
|
+
// Exactly six cells, always — table() lines columns up by index.
|
|
135
|
+
return [
|
|
136
|
+
t.id,
|
|
137
|
+
`${STATUS_GLYPH[t.status]} ${t.status}`,
|
|
138
|
+
t.needsHuman ? "⚑" : "",
|
|
139
|
+
t.title,
|
|
140
|
+
t.tags.join(","),
|
|
141
|
+
t.milestone ?? "",
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function printTask(t: Task): void {
|
|
146
|
+
console.log(`${t.id} ${STATUS_GLYPH[t.status]} ${t.status} ${t.title}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function cmdInit(args: Args): void {
|
|
152
|
+
const cwd = process.cwd()
|
|
153
|
+
if (findRoot(cwd) === cwd) fail("already initialized — .task/ exists here")
|
|
154
|
+
const name = str(args.flags, "name") ?? basename(cwd)
|
|
155
|
+
const store = initProject(cwd, { name, prefix: str(args.flags, "prefix")?.toUpperCase() })
|
|
156
|
+
console.log(`Initialized ${store.config.name} (${store.config.prefix}-…) in .task/`)
|
|
157
|
+
|
|
158
|
+
console.log(`\nNext:`)
|
|
159
|
+
console.log(` task add "My first task" create a task`)
|
|
160
|
+
console.log(` task list see what's open`)
|
|
161
|
+
console.log(` task serve open the board UI`)
|
|
162
|
+
console.log(`\nCommit .task/ — tasks belong in the repo, next to the code they describe.`)
|
|
163
|
+
// Deliberately not installed for you: where an agent's skills live differs
|
|
164
|
+
// per agent and per repo layout, and a task manager guessing at that is how
|
|
165
|
+
// you end up with a copy somewhere nothing reads. `skill/` ships in this
|
|
166
|
+
// package — point your agent's skill installer at it.
|
|
167
|
+
console.log(`Teach your agent the conventions: skill/SKILL.md in this package.`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function cmdAdd(args: Args): void {
|
|
171
|
+
const title = args.positional.join(" ").trim()
|
|
172
|
+
if (!title) fail(`usage: task add <title> [--description …] [--status …] [--tags a,b] [--milestone …] [--needs-human]`)
|
|
173
|
+
const store = openStore()
|
|
174
|
+
const task = store.create({ title, ...patchFromFlags(args.flags) })
|
|
175
|
+
if (args.flags.json) {
|
|
176
|
+
console.log(JSON.stringify({ task }, null, 2))
|
|
177
|
+
} else {
|
|
178
|
+
printTask(task)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function cmdList(args: Args): void {
|
|
183
|
+
const store = openStore()
|
|
184
|
+
const statusFlag = str(args.flags, "status")
|
|
185
|
+
const statuses = statusFlag
|
|
186
|
+
? statusFlag.split(",").map(parseStatus)
|
|
187
|
+
: args.flags.all
|
|
188
|
+
? undefined
|
|
189
|
+
: (["backlog", "todo", "in_progress"] as Status[])
|
|
190
|
+
const tagFlag = str(args.flags, "tags") ?? str(args.flags, "tag")
|
|
191
|
+
const tasks = store.list({
|
|
192
|
+
statuses,
|
|
193
|
+
tags: tagFlag ? parseTags(tagFlag) : undefined,
|
|
194
|
+
milestone: str(args.flags, "milestone"),
|
|
195
|
+
needsHuman: args.flags["needs-human"] ? true : undefined,
|
|
196
|
+
})
|
|
197
|
+
// Present in board order: grouped by status column, then position.
|
|
198
|
+
const order = new Map(STATUSES.map((s, i) => [s, i]))
|
|
199
|
+
tasks.sort((a, b) => order.get(a.status)! - order.get(b.status)! || a.position - b.position)
|
|
200
|
+
if (args.flags.json) {
|
|
201
|
+
console.log(JSON.stringify({ tasks }, null, 2))
|
|
202
|
+
} else if (tasks.length === 0) {
|
|
203
|
+
console.log(args.flags.all ? "no tasks" : "no open tasks (--all includes done/canceled)")
|
|
204
|
+
} else {
|
|
205
|
+
table(tasks.map(taskRow))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function cmdShow(args: Args): void {
|
|
210
|
+
const ref = args.positional[0]
|
|
211
|
+
if (!ref) fail("usage: task show <id>")
|
|
212
|
+
const store = openStore()
|
|
213
|
+
const number = store.parseId(ref)
|
|
214
|
+
const task = store.get(number)
|
|
215
|
+
if (!task) fail(`no such task: ${store.displayId(number)}`)
|
|
216
|
+
const comments = store.comments(number)
|
|
217
|
+
if (args.flags.json) {
|
|
218
|
+
console.log(JSON.stringify({ task, comments }, null, 2))
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
console.log(`${task.id} ${task.title}`)
|
|
222
|
+
console.log(`status ${task.status}`)
|
|
223
|
+
if (task.needsHuman) console.log(`needs a human`)
|
|
224
|
+
if (task.tags.length) console.log(`tags ${task.tags.join(", ")}`)
|
|
225
|
+
if (task.milestone) console.log(`milestone ${task.milestone}`)
|
|
226
|
+
console.log(`created ${task.createdAt}`)
|
|
227
|
+
console.log(`updated ${task.updatedAt}`)
|
|
228
|
+
if (task.description) console.log(`\n${task.description}`)
|
|
229
|
+
if (comments.length) {
|
|
230
|
+
console.log(``)
|
|
231
|
+
for (const c of comments) {
|
|
232
|
+
console.log(`— ${c.author || "anonymous"} · ${c.createdAt}`)
|
|
233
|
+
console.log(` ${c.body.split("\n").join("\n ")}`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cmdUpdate(args: Args, forcedStatus?: Status): void {
|
|
239
|
+
const ref = args.positional[0]
|
|
240
|
+
if (!ref) fail("usage: task update <id> [--status …] [--title …] …")
|
|
241
|
+
const store = openStore()
|
|
242
|
+
const patch = patchFromFlags(args.flags)
|
|
243
|
+
if (forcedStatus) patch.status = forcedStatus
|
|
244
|
+
if (Object.keys(patch).length === 0) fail("nothing to update — pass at least one flag")
|
|
245
|
+
const task = store.update(store.parseId(ref), patch)
|
|
246
|
+
if (args.flags.json) {
|
|
247
|
+
console.log(JSON.stringify({ task }, null, 2))
|
|
248
|
+
} else {
|
|
249
|
+
printTask(task)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function cmdMove(args: Args): void {
|
|
254
|
+
const [ref, status] = args.positional
|
|
255
|
+
if (!ref || !status) fail("usage: task move <id> <status>")
|
|
256
|
+
cmdUpdate({ positional: [ref], flags: { ...args.flags, status } })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function cmdComment(args: Args): void {
|
|
260
|
+
const [ref, ...rest] = args.positional
|
|
261
|
+
const body = rest.join(" ").trim()
|
|
262
|
+
if (!ref || !body) fail(`usage: task comment <id> <text> [--author <who>]`)
|
|
263
|
+
const store = openStore()
|
|
264
|
+
const author = resolveAuthor(str(args.flags, "author")).name
|
|
265
|
+
const comment = store.addComment(store.parseId(ref), body, author)
|
|
266
|
+
if (args.flags.json) {
|
|
267
|
+
console.log(JSON.stringify({ comment }, null, 2))
|
|
268
|
+
} else {
|
|
269
|
+
console.log(`commented on ${comment.taskId}`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const AUTHOR_SOURCE: Record<string, string> = {
|
|
274
|
+
flag: "--author",
|
|
275
|
+
env: "$TASK_AUTHOR",
|
|
276
|
+
git: "git config user.name",
|
|
277
|
+
fallback: "no name found — set git config user.name or $TASK_AUTHOR",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Deliberately never opens the store, so it answers outside a .task/ repo. */
|
|
281
|
+
function cmdWhoami(args: Args): void {
|
|
282
|
+
const author = resolveAuthor(str(args.flags, "author"))
|
|
283
|
+
if (args.flags.json) {
|
|
284
|
+
console.log(JSON.stringify(author, null, 2))
|
|
285
|
+
} else {
|
|
286
|
+
console.log(`${author.name} (${AUTHOR_SOURCE[author.source]})`)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cmdDelete(args: Args): void {
|
|
291
|
+
const ref = args.positional[0]
|
|
292
|
+
if (!ref) fail("usage: task delete <id>")
|
|
293
|
+
const store = openStore()
|
|
294
|
+
const number = store.parseId(ref)
|
|
295
|
+
store.delete(number)
|
|
296
|
+
console.log(`deleted ${store.displayId(number)}`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function cmdServe(args: Args): void {
|
|
300
|
+
// Serve is the one command that's monorepo-aware: anchor at the nearest
|
|
301
|
+
// board like every other command, but if there is none, serve whatever
|
|
302
|
+
// boards live *below* here instead of failing.
|
|
303
|
+
const serveRoot = findRoot(process.cwd()) ?? process.cwd()
|
|
304
|
+
const boards = findBoards(serveRoot)
|
|
305
|
+
if (boards.length === 0) {
|
|
306
|
+
fail("no .task directory found in this directory, any parent, or below — run `task init` first")
|
|
307
|
+
}
|
|
308
|
+
const port = Number(str(args.flags, "port") ?? DEFAULT_PORT)
|
|
309
|
+
if (!Number.isInteger(port) || port <= 0) fail(`invalid port: ${str(args.flags, "port")}`)
|
|
310
|
+
const server = createTaskServer(serveRoot)
|
|
311
|
+
server.on("error", (error: NodeJS.ErrnoException) => {
|
|
312
|
+
if (error.code === "EADDRINUSE") fail(`port ${port} is in use — try --port <n>`)
|
|
313
|
+
fail(error.message)
|
|
314
|
+
})
|
|
315
|
+
server.listen(port, () => {
|
|
316
|
+
const boardName = (root: string): string =>
|
|
317
|
+
(JSON.parse(readFileSync(join(root, TASK_DIR, CONFIG_FILE), "utf8")) as { name: string })
|
|
318
|
+
.name
|
|
319
|
+
if (boards.length === 1) {
|
|
320
|
+
console.log(`${boardName(boards[0].root)} · task board`)
|
|
321
|
+
} else {
|
|
322
|
+
console.log(`${boards.length} boards · task board`)
|
|
323
|
+
for (const board of boards) console.log(` ${board.id} ${boardName(board.root)}`)
|
|
324
|
+
}
|
|
325
|
+
console.log(` http://localhost:${port}`)
|
|
326
|
+
console.log(`Live: edits from any terminal or agent show up as they happen. Ctrl-C to stop.`)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Help + dispatch ──────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
const HELP = `task — a task manager that lives in your repo
|
|
333
|
+
|
|
334
|
+
State is a SQLite database in .task/ at the project root. Commit it: tasks and
|
|
335
|
+
their status travel with the code they describe. Any command works from any
|
|
336
|
+
subdirectory (it walks up to find .task/, like git).
|
|
337
|
+
|
|
338
|
+
Usage
|
|
339
|
+
task init [--name <name>] [--prefix <PREFIX>]
|
|
340
|
+
task add <title> [--description <text>] [--status <s>] [--tags <a,b>]
|
|
341
|
+
[--milestone <m>] [--needs-human]
|
|
342
|
+
task list [--status <s1,s2>] [--tag <a,b>] [--milestone <m>]
|
|
343
|
+
[--needs-human] [--all]
|
|
344
|
+
task show <id>
|
|
345
|
+
task update <id> [--title <t>] [--description <text>] [--status <s>]
|
|
346
|
+
[--tags <a,b>] [--milestone <m>]
|
|
347
|
+
[--needs-human | --no-needs-human]
|
|
348
|
+
task move <id> <status> shorthand for update --status
|
|
349
|
+
task start <id> → in_progress
|
|
350
|
+
task done <id> → done
|
|
351
|
+
task comment <id> <text> [--author <who>]
|
|
352
|
+
task delete <id>
|
|
353
|
+
task whoami who your comments are attributed to
|
|
354
|
+
task serve [--port <n>] board + table UI with live updates (default port ${DEFAULT_PORT});
|
|
355
|
+
serves every board at or below here — in a
|
|
356
|
+
monorepo the header becomes a board switcher
|
|
357
|
+
|
|
358
|
+
Values
|
|
359
|
+
<id> TASK-12, or just 12
|
|
360
|
+
status ${STATUSES.join(" ")}
|
|
361
|
+
--tag a,b matches a task carrying *either* tag
|
|
362
|
+
--needs-human this can't be finished by an agent alone
|
|
363
|
+
clearing --tags "" drops all tags, --milestone "" clears it
|
|
364
|
+
|
|
365
|
+
Comment authors resolve --author → $TASK_AUTHOR → git config user.name →
|
|
366
|
+
anonymous, so there is nothing to set up. \`task whoami\` shows which one won.
|
|
367
|
+
|
|
368
|
+
Every read/write command accepts --json for machine-readable output — that's
|
|
369
|
+
the interface AI agents should use.
|
|
370
|
+
`
|
|
371
|
+
|
|
372
|
+
function main(): void {
|
|
373
|
+
const [command, ...rest] = process.argv.slice(2)
|
|
374
|
+
const args = parseArgs(rest)
|
|
375
|
+
|
|
376
|
+
if (!command || command === "help" || args.flags.help) {
|
|
377
|
+
console.log(HELP)
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
if (command === "--version" || args.flags.version) {
|
|
381
|
+
console.log(VERSION)
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
switch (command) {
|
|
386
|
+
case "init":
|
|
387
|
+
return cmdInit(args)
|
|
388
|
+
case "add":
|
|
389
|
+
return cmdAdd(args)
|
|
390
|
+
case "list":
|
|
391
|
+
return cmdList(args)
|
|
392
|
+
case "show":
|
|
393
|
+
return cmdShow(args)
|
|
394
|
+
case "update":
|
|
395
|
+
return cmdUpdate(args)
|
|
396
|
+
case "move":
|
|
397
|
+
return cmdMove(args)
|
|
398
|
+
case "start":
|
|
399
|
+
return cmdUpdate(args, "in_progress")
|
|
400
|
+
case "done":
|
|
401
|
+
return cmdUpdate(args, "done")
|
|
402
|
+
case "comment":
|
|
403
|
+
return cmdComment(args)
|
|
404
|
+
case "whoami":
|
|
405
|
+
return cmdWhoami(args)
|
|
406
|
+
case "delete":
|
|
407
|
+
return cmdDelete(args)
|
|
408
|
+
case "serve":
|
|
409
|
+
return cmdServe(args)
|
|
410
|
+
default:
|
|
411
|
+
fail(`unknown command "${command}" — run \`task help\``)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
main()
|
|
417
|
+
} catch (error) {
|
|
418
|
+
fail(error instanceof Error ? error.message : String(error))
|
|
419
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Programmatic access to the same store the CLI and `task serve` use — for
|
|
2
|
+
// scripts that want to read or write .task/ without shelling out.
|
|
3
|
+
export { TaskStore, findRoot, findBoards, initProject, derivePrefix, type BoardRef } from "./store.js"
|
|
4
|
+
export { createTaskServer } from "./server.js"
|
|
5
|
+
export { resolveAuthor, ANONYMOUS, type Author, type AuthorSource } from "./author.js"
|
|
6
|
+
export * from "./types.js"
|