@nijaru/tk 0.0.2 → 0.0.4
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 +8 -8
- package/package.json +1 -1
- package/src/cli.test.ts +83 -3
- package/src/cli.ts +102 -399
- package/src/db/storage.ts +137 -115
- package/src/lib/completions.ts +56 -56
- package/src/lib/format.test.ts +26 -12
- package/src/lib/format.ts +34 -34
- package/src/lib/help.ts +249 -0
- package/src/lib/time.ts +115 -0
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { version } from "../package.json";
|
|
|
4
4
|
|
|
5
5
|
import * as storage from "./db/storage";
|
|
6
6
|
import { parsePriority } from "./lib/priority";
|
|
7
|
+
import { parseDueDate, parseEstimate } from "./lib/time";
|
|
7
8
|
import {
|
|
8
9
|
formatTaskList,
|
|
9
10
|
formatTaskDetail,
|
|
@@ -12,19 +13,36 @@ import {
|
|
|
12
13
|
green,
|
|
13
14
|
red,
|
|
14
15
|
yellow,
|
|
16
|
+
dim,
|
|
15
17
|
} from "./lib/format";
|
|
16
18
|
import { findRoot, setWorkingDir } from "./lib/root";
|
|
17
19
|
import { parseId } from "./types";
|
|
18
|
-
import type { Status } from "./types";
|
|
20
|
+
import type { Status, TaskWithMeta } from "./types";
|
|
19
21
|
import { BASH_COMPLETION, ZSH_COMPLETION, FISH_COMPLETION } from "./lib/completions";
|
|
22
|
+
import { MAIN_HELP, COMMAND_HELP } from "./lib/help";
|
|
20
23
|
|
|
21
24
|
const VALID_STATUSES: Status[] = ["open", "active", "done"];
|
|
22
25
|
const PROJECT_PATTERN = /^[a-z][a-z0-9]*$/;
|
|
23
26
|
|
|
27
|
+
const COMMON_OPTIONS = {
|
|
28
|
+
project: { type: "string", short: "P" },
|
|
29
|
+
priority: { type: "string", short: "p" },
|
|
30
|
+
labels: { type: "string", short: "l" },
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const TASK_MUTATION_OPTIONS = {
|
|
34
|
+
...COMMON_OPTIONS,
|
|
35
|
+
description: { type: "string", short: "d" },
|
|
36
|
+
assignees: { type: "string", short: "A" },
|
|
37
|
+
parent: { type: "string" },
|
|
38
|
+
estimate: { type: "string" },
|
|
39
|
+
due: { type: "string" },
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
24
42
|
function validateProject(name: string): void {
|
|
25
43
|
if (!PROJECT_PATTERN.test(name)) {
|
|
26
44
|
throw new Error(
|
|
27
|
-
`Invalid project name: ${name}. Use lowercase letters and numbers, starting with a letter.`,
|
|
45
|
+
`Invalid project name: ${name}. Use lowercase letters and numbers, starting with a letter (e.g., 'api', 'web2').`,
|
|
28
46
|
);
|
|
29
47
|
}
|
|
30
48
|
}
|
|
@@ -39,74 +57,16 @@ function parseStatus(input: string | undefined): Status | undefined {
|
|
|
39
57
|
|
|
40
58
|
function parseLimit(input: string | undefined): number | undefined {
|
|
41
59
|
if (!input) return undefined;
|
|
42
|
-
|
|
43
|
-
if (isNaN(n) || n < 1) {
|
|
60
|
+
if (!/^\d+$/.test(input)) {
|
|
44
61
|
throw new Error(`Invalid limit: ${input}. Must be a positive number.`);
|
|
45
62
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function parseEstimate(input: string | undefined): number | undefined {
|
|
50
|
-
if (!input) return undefined;
|
|
51
|
-
const n = parseInt(input, 10);
|
|
52
|
-
if (isNaN(n) || n < 0) {
|
|
53
|
-
throw new Error(`Invalid estimate: ${input}. Must be a non-negative number.`);
|
|
63
|
+
const n = Number(input);
|
|
64
|
+
if (n < 1) {
|
|
65
|
+
throw new Error(`Invalid limit: ${input}. Must be a positive number.`);
|
|
54
66
|
}
|
|
55
67
|
return n;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
function formatLocalDate(date: Date): string {
|
|
59
|
-
const year = date.getFullYear();
|
|
60
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
61
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
62
|
-
return `${year}-${month}-${day}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseDueDate(input: string | undefined): string | undefined {
|
|
66
|
-
if (!input) return undefined;
|
|
67
|
-
if (input === "-") return undefined; // clear
|
|
68
|
-
|
|
69
|
-
// Handle relative dates like +7d
|
|
70
|
-
if (input.startsWith("+")) {
|
|
71
|
-
const match = input.match(/^\+(\d+)([dwmh])$/);
|
|
72
|
-
if (match && match[1] && match[2]) {
|
|
73
|
-
const num = match[1];
|
|
74
|
-
const unit = match[2];
|
|
75
|
-
const n = parseInt(num, 10);
|
|
76
|
-
const now = new Date();
|
|
77
|
-
switch (unit) {
|
|
78
|
-
case "h":
|
|
79
|
-
now.setHours(now.getHours() + n);
|
|
80
|
-
break;
|
|
81
|
-
case "d":
|
|
82
|
-
now.setDate(now.getDate() + n);
|
|
83
|
-
break;
|
|
84
|
-
case "w":
|
|
85
|
-
now.setDate(now.getDate() + n * 7);
|
|
86
|
-
break;
|
|
87
|
-
case "m":
|
|
88
|
-
now.setMonth(now.getMonth() + n);
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
return formatLocalDate(now);
|
|
92
|
-
}
|
|
93
|
-
throw new Error(`Invalid relative date: ${input}. Use format like +7d, +2w, +1m`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Validate YYYY-MM-DD format - return as-is to avoid timezone issues
|
|
97
|
-
const dateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
98
|
-
if (dateMatch) {
|
|
99
|
-
const [, , month, day] = dateMatch;
|
|
100
|
-
const m = parseInt(month!, 10);
|
|
101
|
-
const d = parseInt(day!, 10);
|
|
102
|
-
// Basic validation: month 1-12, day 1-31
|
|
103
|
-
if (m >= 1 && m <= 12 && d >= 1 && d <= 31) {
|
|
104
|
-
return input; // Return as-is, already in correct format
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
throw new Error(`Invalid date: ${input}. Use YYYY-MM-DD or +Nd format.`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
70
|
function parseLabels(input: string | undefined): string[] | undefined {
|
|
111
71
|
if (!input) return undefined;
|
|
112
72
|
return input
|
|
@@ -146,18 +106,32 @@ function resolveId(input: string | undefined, context: string): string {
|
|
|
146
106
|
const resolved = storage.resolveId(input);
|
|
147
107
|
if (resolved) return resolved;
|
|
148
108
|
|
|
149
|
-
// Check if it's a valid full ID format
|
|
150
|
-
if (parseId(input)) return input;
|
|
151
|
-
|
|
152
109
|
// Check for ambiguous matches
|
|
153
110
|
const matches = storage.findMatchingIds(input);
|
|
154
111
|
if (matches.length > 1) {
|
|
155
112
|
throw new Error(
|
|
156
|
-
`Ambiguous ID '${input}' matches ${matches.length} tasks: ${matches.join(", ")}
|
|
113
|
+
`Ambiguous ID '${input}' matches ${matches.length} tasks: ${matches.join(", ")}. Use more characters to narrow it down.`,
|
|
157
114
|
);
|
|
158
115
|
}
|
|
159
116
|
|
|
160
|
-
|
|
117
|
+
// If it's a valid full ID format but doesn't exist, we still return it
|
|
118
|
+
// and let getTask handle the "not found" error consistently.
|
|
119
|
+
if (parseId(input)) return input;
|
|
120
|
+
|
|
121
|
+
throw new Error(`Task not found: ${input}. Run 'tk ls' to see available tasks.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolves an ID and fetches the task.
|
|
126
|
+
* Handles "Task not found" and cleanup output automatically.
|
|
127
|
+
*/
|
|
128
|
+
function resolveTask(input: string | undefined, context: string): TaskWithMeta {
|
|
129
|
+
const id = resolveId(input, context);
|
|
130
|
+
const result = storage.getTask(id);
|
|
131
|
+
if (!result) error(`Task not found: ${id}. Run 'tk ls' to see available tasks.`);
|
|
132
|
+
|
|
133
|
+
outputCleanup(id, result.cleanup);
|
|
134
|
+
return result.task;
|
|
161
135
|
}
|
|
162
136
|
|
|
163
137
|
const rawArgs = process.argv.slice(2);
|
|
@@ -223,258 +197,12 @@ function error(message: string): never {
|
|
|
223
197
|
process.exit(1);
|
|
224
198
|
}
|
|
225
199
|
|
|
226
|
-
function showHelp() {
|
|
227
|
-
console.log(
|
|
228
|
-
|
|
229
|
-
USAGE:
|
|
230
|
-
tk <command> [options]
|
|
231
|
-
|
|
232
|
-
COMMANDS:
|
|
233
|
-
init Initialize .tasks/ directory
|
|
234
|
-
add Create task
|
|
235
|
-
ls, list List tasks
|
|
236
|
-
ready List ready tasks (open + unblocked)
|
|
237
|
-
show Show task details
|
|
238
|
-
start Start working (open -> active)
|
|
239
|
-
done Complete task
|
|
240
|
-
reopen Reopen task
|
|
241
|
-
edit Edit task
|
|
242
|
-
log Add log entry
|
|
243
|
-
block Add blocker
|
|
244
|
-
unblock Remove blocker
|
|
245
|
-
rm, remove Delete task
|
|
246
|
-
clean Remove old done tasks
|
|
247
|
-
check Check for data issues
|
|
248
|
-
config Show/set configuration
|
|
249
|
-
completions Output shell completions
|
|
250
|
-
|
|
251
|
-
GLOBAL OPTIONS:
|
|
252
|
-
-C <dir> Run in different directory
|
|
253
|
-
--json Output as JSON
|
|
254
|
-
-h, --help Show help
|
|
255
|
-
-V Show version
|
|
256
|
-
|
|
257
|
-
Run 'tk <command> --help' for command-specific options.
|
|
258
|
-
`);
|
|
200
|
+
function showHelp(): void {
|
|
201
|
+
console.log(MAIN_HELP);
|
|
259
202
|
}
|
|
260
203
|
|
|
261
|
-
function showCommandHelp(cmd: string) {
|
|
262
|
-
const
|
|
263
|
-
init: `tk init - Initialize .tasks/ directory
|
|
264
|
-
|
|
265
|
-
USAGE:
|
|
266
|
-
tk init [options]
|
|
267
|
-
|
|
268
|
-
OPTIONS:
|
|
269
|
-
-P, --project <name> Set default project name
|
|
270
|
-
`,
|
|
271
|
-
add: `tk add - Create a new task
|
|
272
|
-
|
|
273
|
-
USAGE:
|
|
274
|
-
tk add <title> [options]
|
|
275
|
-
|
|
276
|
-
OPTIONS:
|
|
277
|
-
-p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
|
|
278
|
-
-P, --project <name> Project prefix for ID
|
|
279
|
-
-d, --description <text> Description
|
|
280
|
-
-l, --labels <csv> Labels (comma-separated)
|
|
281
|
-
-A, --assignees <csv> Assignees (comma-separated, @me for git user)
|
|
282
|
-
--parent <id> Parent task ID
|
|
283
|
-
--estimate <n> Estimate (user-defined units)
|
|
284
|
-
--due <date> Due date (YYYY-MM-DD or +Nh/+Nd/+Nw/+Nm)
|
|
285
|
-
|
|
286
|
-
EXAMPLES:
|
|
287
|
-
tk add "Fix login bug" -p 1
|
|
288
|
-
tk add "New feature" -P api -l bug,urgent
|
|
289
|
-
tk add "Sprint task" --due +7d
|
|
290
|
-
`,
|
|
291
|
-
ls: `tk ls - List tasks
|
|
292
|
-
|
|
293
|
-
USAGE:
|
|
294
|
-
tk ls [options]
|
|
295
|
-
|
|
296
|
-
OPTIONS:
|
|
297
|
-
-s, --status <status> Filter by status (open, active, done)
|
|
298
|
-
-p, --priority <0-4> Filter by priority
|
|
299
|
-
-P, --project <name> Filter by project
|
|
300
|
-
-l, --label <label> Filter by label
|
|
301
|
-
--assignee <name> Filter by assignee
|
|
302
|
-
--parent <id> Filter by parent
|
|
303
|
-
--roots Show only root tasks (no parent)
|
|
304
|
-
--overdue Show only overdue tasks
|
|
305
|
-
-n, --limit <n> Limit results (default: 20)
|
|
306
|
-
-a, --all Show all (no limit)
|
|
307
|
-
|
|
308
|
-
EXAMPLES:
|
|
309
|
-
tk ls -s open -p 1 # Urgent open tasks
|
|
310
|
-
tk ls --overdue # Overdue tasks
|
|
311
|
-
tk ls -P api --roots # Root tasks in api project
|
|
312
|
-
`,
|
|
313
|
-
list: `tk list - List tasks (alias for 'ls')
|
|
314
|
-
|
|
315
|
-
Run 'tk ls --help' for options.
|
|
316
|
-
`,
|
|
317
|
-
ready: `tk ready - List ready tasks (open + unblocked)
|
|
318
|
-
|
|
319
|
-
USAGE:
|
|
320
|
-
tk ready
|
|
321
|
-
|
|
322
|
-
Shows open tasks that are not blocked by any incomplete task.
|
|
323
|
-
`,
|
|
324
|
-
show: `tk show - Show task details
|
|
325
|
-
|
|
326
|
-
USAGE:
|
|
327
|
-
tk show <id>
|
|
328
|
-
|
|
329
|
-
EXAMPLES:
|
|
330
|
-
tk show tk-1
|
|
331
|
-
tk show 1 # If unambiguous
|
|
332
|
-
`,
|
|
333
|
-
start: `tk start - Start working on a task
|
|
334
|
-
|
|
335
|
-
USAGE:
|
|
336
|
-
tk start <id>
|
|
337
|
-
|
|
338
|
-
Changes status from open to active.
|
|
339
|
-
`,
|
|
340
|
-
done: `tk done - Complete a task
|
|
341
|
-
|
|
342
|
-
USAGE:
|
|
343
|
-
tk done <id>
|
|
344
|
-
|
|
345
|
-
Changes status to done.
|
|
346
|
-
`,
|
|
347
|
-
reopen: `tk reopen - Reopen a task
|
|
348
|
-
|
|
349
|
-
USAGE:
|
|
350
|
-
tk reopen <id>
|
|
351
|
-
|
|
352
|
-
Changes status back to open.
|
|
353
|
-
`,
|
|
354
|
-
edit: `tk edit - Edit a task
|
|
355
|
-
|
|
356
|
-
USAGE:
|
|
357
|
-
tk edit <id> [options]
|
|
358
|
-
|
|
359
|
-
OPTIONS:
|
|
360
|
-
-t, --title <text> New title
|
|
361
|
-
-d, --description <text> New description
|
|
362
|
-
-p, --priority <0-4> New priority
|
|
363
|
-
-l, --labels <csv> Replace labels (use +tag/-tag to add/remove)
|
|
364
|
-
-A, --assignees <csv> Replace assignees
|
|
365
|
-
--parent <id> Set parent (use - to clear)
|
|
366
|
-
--estimate <n> Set estimate (use - to clear)
|
|
367
|
-
--due <date> Set due date (use - to clear)
|
|
368
|
-
|
|
369
|
-
EXAMPLES:
|
|
370
|
-
tk edit tk-1 -t "New title"
|
|
371
|
-
tk edit tk-1 -l +urgent # Add label
|
|
372
|
-
tk edit tk-1 --due - # Clear due date
|
|
373
|
-
`,
|
|
374
|
-
log: `tk log - Add a log entry to a task
|
|
375
|
-
|
|
376
|
-
USAGE:
|
|
377
|
-
tk log <id> "<message>"
|
|
378
|
-
|
|
379
|
-
Message must be quoted.
|
|
380
|
-
|
|
381
|
-
EXAMPLES:
|
|
382
|
-
tk log tk-1 "Started implementation"
|
|
383
|
-
tk log tk-1 "Blocked on API changes"
|
|
384
|
-
`,
|
|
385
|
-
block: `tk block - Add a blocker dependency
|
|
386
|
-
|
|
387
|
-
USAGE:
|
|
388
|
-
tk block <task> <blocker>
|
|
389
|
-
|
|
390
|
-
The first task becomes blocked by the second.
|
|
391
|
-
|
|
392
|
-
EXAMPLES:
|
|
393
|
-
tk block tk-2 tk-1 # tk-2 is blocked by tk-1
|
|
394
|
-
`,
|
|
395
|
-
unblock: `tk unblock - Remove a blocker dependency
|
|
396
|
-
|
|
397
|
-
USAGE:
|
|
398
|
-
tk unblock <task> <blocker>
|
|
399
|
-
|
|
400
|
-
EXAMPLES:
|
|
401
|
-
tk unblock tk-2 tk-1
|
|
402
|
-
`,
|
|
403
|
-
rm: `tk rm - Delete a task
|
|
404
|
-
|
|
405
|
-
USAGE:
|
|
406
|
-
tk rm <id>
|
|
407
|
-
|
|
408
|
-
EXAMPLES:
|
|
409
|
-
tk rm tk-1
|
|
410
|
-
`,
|
|
411
|
-
remove: `tk remove - Delete a task (alias for 'rm')
|
|
412
|
-
|
|
413
|
-
USAGE:
|
|
414
|
-
tk remove <id>
|
|
415
|
-
`,
|
|
416
|
-
clean: `tk clean - Remove old completed tasks
|
|
417
|
-
|
|
418
|
-
USAGE:
|
|
419
|
-
tk clean [options]
|
|
420
|
-
|
|
421
|
-
OPTIONS:
|
|
422
|
-
--older-than <days> Age threshold in days (default: from config, 14)
|
|
423
|
-
-f, --force Remove all done tasks (ignores age and disabled state)
|
|
424
|
-
|
|
425
|
-
EXAMPLES:
|
|
426
|
-
tk clean # Remove done tasks older than config.clean_after days
|
|
427
|
-
tk clean --older-than 30 # Remove done tasks older than 30 days
|
|
428
|
-
tk clean --force # Remove all done tasks regardless of age
|
|
429
|
-
`,
|
|
430
|
-
check: `tk check - Check for data issues
|
|
431
|
-
|
|
432
|
-
USAGE:
|
|
433
|
-
tk check
|
|
434
|
-
|
|
435
|
-
Scans all tasks for issues. Auto-fixable issues (orphaned references, ID
|
|
436
|
-
mismatches) are fixed automatically. Unfixable issues (corrupted JSON) are
|
|
437
|
-
reported for manual intervention.
|
|
438
|
-
|
|
439
|
-
Note: Auto-fixing also happens during normal task operations (show, done,
|
|
440
|
-
edit, etc.) - this command is for bulk cleanup or diagnostics.
|
|
441
|
-
|
|
442
|
-
EXAMPLES:
|
|
443
|
-
tk check # Scan and fix all tasks
|
|
444
|
-
`,
|
|
445
|
-
config: `tk config - Show or set configuration
|
|
446
|
-
|
|
447
|
-
USAGE:
|
|
448
|
-
tk config Show all config
|
|
449
|
-
tk config project Show default project
|
|
450
|
-
tk config project <name> Set default project
|
|
451
|
-
tk config project <new> --rename <old> Rename project (old-* → new-*)
|
|
452
|
-
tk config alias List aliases
|
|
453
|
-
tk config alias <name> <path> Add alias
|
|
454
|
-
tk config alias --rm <name> Remove alias
|
|
455
|
-
|
|
456
|
-
EXAMPLES:
|
|
457
|
-
tk config project api
|
|
458
|
-
tk config project lsmvec --rename cloudlsmvec
|
|
459
|
-
tk config alias web src/web
|
|
460
|
-
`,
|
|
461
|
-
completions: `tk completions - Output shell completions
|
|
462
|
-
|
|
463
|
-
USAGE:
|
|
464
|
-
tk completions <shell>
|
|
465
|
-
|
|
466
|
-
SHELLS:
|
|
467
|
-
bash Bash completion script
|
|
468
|
-
zsh Zsh completion script
|
|
469
|
-
fish Fish completion script
|
|
470
|
-
|
|
471
|
-
EXAMPLES:
|
|
472
|
-
eval "$(tk completions bash)" # Add to ~/.bashrc
|
|
473
|
-
eval "$(tk completions zsh)" # Add to ~/.zshrc
|
|
474
|
-
`,
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
const help = helps[cmd];
|
|
204
|
+
function showCommandHelp(cmd: string): void {
|
|
205
|
+
const help = COMMAND_HELP[cmd];
|
|
478
206
|
if (help) {
|
|
479
207
|
console.log(help);
|
|
480
208
|
} else {
|
|
@@ -526,7 +254,7 @@ function main() {
|
|
|
526
254
|
if (info.exists) {
|
|
527
255
|
output(
|
|
528
256
|
{ path: info.tasksDir, created: false },
|
|
529
|
-
|
|
257
|
+
dim(`Already initialized: ${info.tasksDir}`),
|
|
530
258
|
);
|
|
531
259
|
} else {
|
|
532
260
|
const path = storage.initTasks(values.project);
|
|
@@ -538,16 +266,7 @@ function main() {
|
|
|
538
266
|
case "add": {
|
|
539
267
|
const { values, positionals } = parseArgs({
|
|
540
268
|
args,
|
|
541
|
-
options:
|
|
542
|
-
description: { type: "string", short: "d" },
|
|
543
|
-
priority: { type: "string", short: "p" },
|
|
544
|
-
project: { type: "string", short: "P" },
|
|
545
|
-
labels: { type: "string", short: "l" },
|
|
546
|
-
assignees: { type: "string", short: "A" },
|
|
547
|
-
parent: { type: "string" },
|
|
548
|
-
estimate: { type: "string" },
|
|
549
|
-
due: { type: "string" },
|
|
550
|
-
},
|
|
269
|
+
options: TASK_MUTATION_OPTIONS,
|
|
551
270
|
allowPositionals: true,
|
|
552
271
|
});
|
|
553
272
|
const title = positionals[0]?.trim();
|
|
@@ -562,7 +281,8 @@ function main() {
|
|
|
562
281
|
let parentId: string | undefined;
|
|
563
282
|
if (values.parent) {
|
|
564
283
|
const resolved = storage.resolveId(values.parent);
|
|
565
|
-
if (!resolved)
|
|
284
|
+
if (!resolved)
|
|
285
|
+
error(`Parent task not found: ${values.parent}. Run 'tk ls' to see available tasks.`);
|
|
566
286
|
parentId = resolved;
|
|
567
287
|
const parentResult = storage.validateParent(parentId);
|
|
568
288
|
if (!parentResult.ok) error(parentResult.error!);
|
|
@@ -588,10 +308,9 @@ function main() {
|
|
|
588
308
|
const { values } = parseArgs({
|
|
589
309
|
args,
|
|
590
310
|
options: {
|
|
311
|
+
...COMMON_OPTIONS,
|
|
312
|
+
label: COMMON_OPTIONS.labels, // alias for consistency
|
|
591
313
|
status: { type: "string", short: "s" },
|
|
592
|
-
priority: { type: "string", short: "p" },
|
|
593
|
-
project: { type: "string", short: "P" },
|
|
594
|
-
label: { type: "string", short: "l" },
|
|
595
314
|
assignee: { type: "string" },
|
|
596
315
|
parent: { type: "string" },
|
|
597
316
|
roots: { type: "boolean" },
|
|
@@ -609,7 +328,8 @@ function main() {
|
|
|
609
328
|
let parentFilter: string | undefined;
|
|
610
329
|
if (values.parent) {
|
|
611
330
|
const resolved = storage.resolveId(values.parent);
|
|
612
|
-
if (!resolved)
|
|
331
|
+
if (!resolved)
|
|
332
|
+
error(`Parent task not found: ${values.parent}. Run 'tk ls' to see available tasks.`);
|
|
613
333
|
parentFilter = resolved;
|
|
614
334
|
}
|
|
615
335
|
|
|
@@ -617,7 +337,7 @@ function main() {
|
|
|
617
337
|
status,
|
|
618
338
|
priority,
|
|
619
339
|
project: values.project,
|
|
620
|
-
label: values.label,
|
|
340
|
+
label: values.label ?? values.labels,
|
|
621
341
|
assignee: values.assignee,
|
|
622
342
|
parent: parentFilter,
|
|
623
343
|
roots: values.roots,
|
|
@@ -630,49 +350,40 @@ function main() {
|
|
|
630
350
|
|
|
631
351
|
case "ready": {
|
|
632
352
|
const list = storage.listReadyTasks();
|
|
633
|
-
output(
|
|
353
|
+
output(
|
|
354
|
+
list,
|
|
355
|
+
formatTaskList(list, undefined, "No ready tasks. All tasks are either done or blocked."),
|
|
356
|
+
);
|
|
634
357
|
break;
|
|
635
358
|
}
|
|
636
359
|
|
|
637
360
|
case "show": {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
if (!result) error(`Task not found: ${id}`);
|
|
641
|
-
outputCleanup(id, result.cleanup);
|
|
642
|
-
output(result.task, formatTaskDetail(result.task, result.task.logs));
|
|
361
|
+
const task = resolveTask(args[0], "show");
|
|
362
|
+
output(task, formatTaskDetail(task, task.logs));
|
|
643
363
|
break;
|
|
644
364
|
}
|
|
645
365
|
|
|
646
366
|
case "start": {
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (result.task.status === "done") error(`Task already done. Use 'tk reopen ${id}' first.`);
|
|
654
|
-
const updated = storage.updateTaskStatus(id, "active");
|
|
655
|
-
output(updated, green(`Started: ${id}`));
|
|
367
|
+
const task = resolveTask(args[0], "start");
|
|
368
|
+
if (task.status === "active")
|
|
369
|
+
error(`Task already active. Use 'tk done ${task.id}' to complete it.`);
|
|
370
|
+
if (task.status === "done") error(`Task already done. Use 'tk reopen ${task.id}' first.`);
|
|
371
|
+
const updated = storage.updateTaskStatus(task.id, "active");
|
|
372
|
+
output(updated, green(`Started: ${task.id}`));
|
|
656
373
|
break;
|
|
657
374
|
}
|
|
658
375
|
|
|
659
376
|
case "done": {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
outputCleanup(id, result.cleanup);
|
|
664
|
-
const updated = storage.updateTaskStatus(id, "done");
|
|
665
|
-
output(updated, green(`Completed: ${id}`));
|
|
377
|
+
const task = resolveTask(args[0], "done");
|
|
378
|
+
const updated = storage.updateTaskStatus(task.id, "done");
|
|
379
|
+
output(updated, green(`Completed: ${task.id}`));
|
|
666
380
|
break;
|
|
667
381
|
}
|
|
668
382
|
|
|
669
383
|
case "reopen": {
|
|
670
|
-
const
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
outputCleanup(id, result.cleanup);
|
|
674
|
-
const updated = storage.updateTaskStatus(id, "open");
|
|
675
|
-
output(updated, green(`Reopened: ${id}`));
|
|
384
|
+
const task = resolveTask(args[0], "reopen");
|
|
385
|
+
const updated = storage.updateTaskStatus(task.id, "open");
|
|
386
|
+
output(updated, green(`Reopened: ${task.id}`));
|
|
676
387
|
break;
|
|
677
388
|
}
|
|
678
389
|
|
|
@@ -680,21 +391,12 @@ function main() {
|
|
|
680
391
|
const { values, positionals } = parseArgs({
|
|
681
392
|
args,
|
|
682
393
|
options: {
|
|
394
|
+
...TASK_MUTATION_OPTIONS,
|
|
683
395
|
title: { type: "string", short: "t" },
|
|
684
|
-
description: { type: "string", short: "d" },
|
|
685
|
-
priority: { type: "string", short: "p" },
|
|
686
|
-
labels: { type: "string", short: "l" },
|
|
687
|
-
assignees: { type: "string", short: "A" },
|
|
688
|
-
parent: { type: "string" },
|
|
689
|
-
estimate: { type: "string" },
|
|
690
|
-
due: { type: "string" },
|
|
691
396
|
},
|
|
692
397
|
allowPositionals: true,
|
|
693
398
|
});
|
|
694
|
-
const
|
|
695
|
-
const result = storage.getTask(id);
|
|
696
|
-
if (!result) error(`Task not found: ${id}`);
|
|
697
|
-
outputCleanup(id, result.cleanup);
|
|
399
|
+
const task = resolveTask(positionals[0], "edit");
|
|
698
400
|
|
|
699
401
|
// Handle label modifications (+tag, -tag)
|
|
700
402
|
let labels: string[] | undefined;
|
|
@@ -702,13 +404,11 @@ function main() {
|
|
|
702
404
|
if (values.labels.startsWith("+")) {
|
|
703
405
|
// Add label (avoid duplicates)
|
|
704
406
|
const newLabel = values.labels.slice(1);
|
|
705
|
-
labels =
|
|
706
|
-
? result.task.labels
|
|
707
|
-
: [...result.task.labels, newLabel];
|
|
407
|
+
labels = task.labels.includes(newLabel) ? task.labels : [...task.labels, newLabel];
|
|
708
408
|
} else if (values.labels.startsWith("-")) {
|
|
709
409
|
// Remove label
|
|
710
410
|
const removeLabel = values.labels.slice(1);
|
|
711
|
-
labels =
|
|
411
|
+
labels = task.labels.filter((l: string) => l !== removeLabel);
|
|
712
412
|
} else {
|
|
713
413
|
// Replace labels
|
|
714
414
|
labels = parseLabels(values.labels);
|
|
@@ -721,13 +421,14 @@ function main() {
|
|
|
721
421
|
resolvedParent = null;
|
|
722
422
|
} else if (values.parent) {
|
|
723
423
|
const resolved = storage.resolveId(values.parent);
|
|
724
|
-
if (!resolved)
|
|
424
|
+
if (!resolved)
|
|
425
|
+
error(`Parent task not found: ${values.parent}. Run 'tk ls' to see available tasks.`);
|
|
725
426
|
resolvedParent = resolved;
|
|
726
|
-
const parentResult = storage.validateParent(resolvedParent, id);
|
|
427
|
+
const parentResult = storage.validateParent(resolvedParent, task.id);
|
|
727
428
|
if (!parentResult.ok) error(parentResult.error!);
|
|
728
429
|
}
|
|
729
430
|
|
|
730
|
-
const updated = storage.updateTask(id, {
|
|
431
|
+
const updated = storage.updateTask(task.id, {
|
|
731
432
|
title: values.title?.trim() || undefined,
|
|
732
433
|
description: values.description,
|
|
733
434
|
priority: values.priority ? parsePriority(values.priority) : undefined,
|
|
@@ -737,12 +438,12 @@ function main() {
|
|
|
737
438
|
estimate: values.estimate === "-" ? null : (parseEstimate(values.estimate) ?? undefined),
|
|
738
439
|
due_date: values.due === "-" ? null : (parseDueDate(values.due) ?? undefined),
|
|
739
440
|
});
|
|
740
|
-
output(updated, green(`Updated: ${id}`));
|
|
441
|
+
output(updated, green(`Updated: ${task.id}`));
|
|
741
442
|
break;
|
|
742
443
|
}
|
|
743
444
|
|
|
744
445
|
case "log": {
|
|
745
|
-
const
|
|
446
|
+
const task = resolveTask(args[0], "log");
|
|
746
447
|
const message = args[1]?.trim();
|
|
747
448
|
if (!message) error('Message required: tk log <id> "<message>"');
|
|
748
449
|
if (args.length > 2) {
|
|
@@ -751,19 +452,19 @@ function main() {
|
|
|
751
452
|
` Got ${args.length - 1} arguments instead of 1`,
|
|
752
453
|
);
|
|
753
454
|
}
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
outputCleanup(id, result.cleanup);
|
|
757
|
-
const entry = storage.addLogEntry(id, message);
|
|
758
|
-
output(entry, green(`Logged: ${id}`));
|
|
455
|
+
const entry = storage.addLogEntry(task.id, message);
|
|
456
|
+
output(entry, green(`Logged: ${task.id}`));
|
|
759
457
|
break;
|
|
760
458
|
}
|
|
761
459
|
|
|
762
460
|
case "block": {
|
|
763
|
-
if (!args[0] || !args[1])
|
|
461
|
+
if (!args[0] || !args[1])
|
|
462
|
+
error(
|
|
463
|
+
"Two IDs required: tk block <task> <blocker>. The first task becomes blocked by the second.",
|
|
464
|
+
);
|
|
764
465
|
const taskId = resolveId(args[0], "block");
|
|
765
466
|
const blockerId = resolveId(args[1], "block");
|
|
766
|
-
if (taskId === blockerId) error("Task cannot block itself");
|
|
467
|
+
if (taskId === blockerId) error("Task cannot block itself.");
|
|
767
468
|
const result = storage.addBlock(taskId, blockerId);
|
|
768
469
|
if (!result.ok) error(result.error!);
|
|
769
470
|
output(
|
|
@@ -774,7 +475,7 @@ function main() {
|
|
|
774
475
|
}
|
|
775
476
|
|
|
776
477
|
case "unblock": {
|
|
777
|
-
if (!args[0] || !args[1]) error("
|
|
478
|
+
if (!args[0] || !args[1]) error("Two IDs required: tk unblock <task> <blocker>.");
|
|
778
479
|
const taskId = resolveId(args[0], "unblock");
|
|
779
480
|
const blockerId = resolveId(args[1], "unblock");
|
|
780
481
|
const removed = storage.removeBlock(taskId, blockerId);
|
|
@@ -790,7 +491,7 @@ function main() {
|
|
|
790
491
|
case "remove": {
|
|
791
492
|
const id = resolveId(args[0], "rm");
|
|
792
493
|
const deleted = storage.deleteTask(id);
|
|
793
|
-
if (!deleted) error(`Task not found: ${id}
|
|
494
|
+
if (!deleted) error(`Task not found: ${id}. Run 'tk ls' to see available tasks.`);
|
|
794
495
|
output({ id, deleted: true }, green(`Deleted: ${id}`));
|
|
795
496
|
break;
|
|
796
497
|
}
|
|
@@ -809,11 +510,10 @@ function main() {
|
|
|
809
510
|
// Get days from CLI or config
|
|
810
511
|
let days: number | false;
|
|
811
512
|
if (values["older-than"] !== undefined) {
|
|
812
|
-
|
|
813
|
-
if (isNaN(n) || n < 0) {
|
|
513
|
+
if (!/^\d+$/.test(values["older-than"])) {
|
|
814
514
|
error(`Invalid --older-than: ${values["older-than"]}. Use a number of days.`);
|
|
815
515
|
}
|
|
816
|
-
days =
|
|
516
|
+
days = Number(values["older-than"]);
|
|
817
517
|
} else {
|
|
818
518
|
days = config.clean_after;
|
|
819
519
|
// Validate config value at runtime
|
|
@@ -933,7 +633,7 @@ function main() {
|
|
|
933
633
|
const alias = positionals[0];
|
|
934
634
|
const path = positionals[1];
|
|
935
635
|
if (!alias || !path || !alias.trim()) {
|
|
936
|
-
error("Alias name and path
|
|
636
|
+
error("Alias name and path required: tk config alias <name> <path>");
|
|
937
637
|
}
|
|
938
638
|
const updated = storage.setAlias(alias, path);
|
|
939
639
|
output(updated, green(`Added alias: ${alias} → ${path}`));
|
|
@@ -941,7 +641,10 @@ function main() {
|
|
|
941
641
|
// List aliases
|
|
942
642
|
const aliases = config.aliases;
|
|
943
643
|
if (Object.keys(aliases).length === 0) {
|
|
944
|
-
output(
|
|
644
|
+
output(
|
|
645
|
+
{ aliases: {} },
|
|
646
|
+
"No aliases defined. Add one with: tk config alias <name> <path>",
|
|
647
|
+
);
|
|
945
648
|
} else {
|
|
946
649
|
const lines = Object.entries(aliases)
|
|
947
650
|
.map(([a, p]) => `${a} → ${p}`)
|
|
@@ -952,7 +655,7 @@ function main() {
|
|
|
952
655
|
break;
|
|
953
656
|
}
|
|
954
657
|
default:
|
|
955
|
-
error(`Unknown config command: ${subcommand}
|
|
658
|
+
error(`Unknown config command: ${subcommand}. Valid: project, alias.`);
|
|
956
659
|
}
|
|
957
660
|
break;
|
|
958
661
|
}
|
|
@@ -970,7 +673,7 @@ function main() {
|
|
|
970
673
|
console.log(FISH_COMPLETION);
|
|
971
674
|
break;
|
|
972
675
|
default:
|
|
973
|
-
error("Shell required: tk completions <bash|zsh|fish
|
|
676
|
+
error("Shell required: tk completions <bash|zsh|fish>. Add to your shell's rc file.");
|
|
974
677
|
}
|
|
975
678
|
break;
|
|
976
679
|
}
|