@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/db/storage.ts
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
writeFileSync,
|
|
8
8
|
realpathSync,
|
|
9
9
|
lstatSync,
|
|
10
|
+
renameSync,
|
|
10
11
|
} from "fs";
|
|
11
12
|
import { join, resolve, basename } from "path";
|
|
12
13
|
import { getTasksDir, getWorkingDir } from "../lib/root";
|
|
14
|
+
import { isTaskOverdue } from "../lib/time";
|
|
13
15
|
import type { Task, Config, Status, Priority, TaskWithMeta, LogEntry } from "../types";
|
|
14
16
|
import { DEFAULT_CONFIG, taskId, parseId, generateRef } from "../types";
|
|
15
17
|
|
|
@@ -31,6 +33,22 @@ function getConfigPath(tasksDir: string): string {
|
|
|
31
33
|
return join(tasksDir, "config.json");
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Write a file atomically by writing to a temporary file and renaming it.
|
|
38
|
+
*/
|
|
39
|
+
function atomicWrite(path: string, content: string): void {
|
|
40
|
+
const tempPath = `${path}.tmp.${generateRef()}`;
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(tempPath, content);
|
|
43
|
+
renameSync(tempPath, path);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (existsSync(tempPath)) {
|
|
46
|
+
unlinkSync(tempPath);
|
|
47
|
+
}
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
function validatePathSafety(path: string, tasksDir: string): boolean {
|
|
35
53
|
try {
|
|
36
54
|
if (existsSync(path)) {
|
|
@@ -50,17 +68,45 @@ function validatePathSafety(path: string, tasksDir: string): boolean {
|
|
|
50
68
|
}
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
71
|
+
const STATUS_ORDER: Record<Status, number> = { active: 0, open: 1, done: 2 };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compare two tasks for sorting:
|
|
75
|
+
* 1. Status (active > open > done)
|
|
76
|
+
* 2. If active/open: overdue > priority (1-4, 0) > due date > created_at
|
|
77
|
+
* 3. If done: completed_at (newest first)
|
|
78
|
+
*/
|
|
79
|
+
export function compareTasks(a: Task, b: Task): number {
|
|
80
|
+
const sA = STATUS_ORDER[a.status] ?? 99;
|
|
81
|
+
const sB = STATUS_ORDER[b.status] ?? 99;
|
|
82
|
+
if (sA !== sB) return sA - sB;
|
|
83
|
+
|
|
84
|
+
if (a.status !== "done") {
|
|
85
|
+
// Overdue hoist
|
|
86
|
+
const overdueA = isTaskOverdue(a.due_date, a.status) ? 0 : 1;
|
|
87
|
+
const overdueB = isTaskOverdue(b.due_date, b.status) ? 0 : 1;
|
|
88
|
+
if (overdueA !== overdueB) return overdueA - overdueB;
|
|
89
|
+
|
|
90
|
+
// Priority (1-4, then 0/none)
|
|
91
|
+
const pA = a.priority === 0 ? 5 : a.priority;
|
|
92
|
+
const pB = b.priority === 0 ? 5 : b.priority;
|
|
93
|
+
if (pA !== pB) return pA - pB;
|
|
94
|
+
|
|
95
|
+
// Due date (soonest first, nulls last)
|
|
96
|
+
if (a.due_date !== b.due_date) {
|
|
97
|
+
if (!a.due_date) return 1;
|
|
98
|
+
if (!b.due_date) return -1;
|
|
99
|
+
return a.due_date.localeCompare(b.due_date);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Created at (newest first)
|
|
103
|
+
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Done tasks: newest completion first
|
|
107
|
+
const timeA = a.completed_at ? new Date(a.completed_at).getTime() : 0;
|
|
108
|
+
const timeB = b.completed_at ? new Date(b.completed_at).getTime() : 0;
|
|
109
|
+
return timeB - timeA;
|
|
64
110
|
}
|
|
65
111
|
|
|
66
112
|
// --- Config Operations ---
|
|
@@ -82,7 +128,7 @@ export function getConfig(): Config {
|
|
|
82
128
|
export function saveConfig(config: Config): void {
|
|
83
129
|
const tasksDir = ensureTasksDir();
|
|
84
130
|
const configPath = getConfigPath(tasksDir);
|
|
85
|
-
|
|
131
|
+
atomicWrite(configPath, JSON.stringify(config, null, 2));
|
|
86
132
|
}
|
|
87
133
|
|
|
88
134
|
export function updateConfig(updates: Partial<Config>): Config {
|
|
@@ -112,7 +158,7 @@ export function renameProject(oldProject: string, newProject: string): RenameRes
|
|
|
112
158
|
// Find tasks to rename
|
|
113
159
|
const toRename = allTasks.filter((t) => t.project === oldProject);
|
|
114
160
|
if (toRename.length === 0) {
|
|
115
|
-
throw new Error(`No tasks found with project "${oldProject}"
|
|
161
|
+
throw new Error(`No tasks found with project "${oldProject}". Run 'tk ls' to see projects.`);
|
|
116
162
|
}
|
|
117
163
|
|
|
118
164
|
// Check for collisions
|
|
@@ -120,7 +166,7 @@ export function renameProject(oldProject: string, newProject: string): RenameRes
|
|
|
120
166
|
for (const task of toRename) {
|
|
121
167
|
const newId = `${newProject}-${task.ref}`;
|
|
122
168
|
if (existingIds.has(newId)) {
|
|
123
|
-
throw new Error(`Cannot rename: "${newId}" already exists
|
|
169
|
+
throw new Error(`Cannot rename: "${newId}" already exists. Choose a different project name.`);
|
|
124
170
|
}
|
|
125
171
|
}
|
|
126
172
|
|
|
@@ -167,12 +213,12 @@ export function renameProject(oldProject: string, newProject: string): RenameRes
|
|
|
167
213
|
const newPath = getTaskPath(tasksDir, taskId(task));
|
|
168
214
|
|
|
169
215
|
// Write to new path, delete old
|
|
170
|
-
|
|
216
|
+
atomicWrite(newPath, JSON.stringify(task, null, 2));
|
|
171
217
|
unlinkSync(oldPath);
|
|
172
218
|
} else if (modified) {
|
|
173
219
|
// Just update references in non-renamed task
|
|
174
220
|
const path = getTaskPath(tasksDir, taskId(task));
|
|
175
|
-
|
|
221
|
+
atomicWrite(path, JSON.stringify(task, null, 2));
|
|
176
222
|
}
|
|
177
223
|
}
|
|
178
224
|
|
|
@@ -221,7 +267,7 @@ function readTaskFile(path: string, tasksDir?: string): Task | null {
|
|
|
221
267
|
}
|
|
222
268
|
|
|
223
269
|
function writeTaskFile(path: string, task: Task): void {
|
|
224
|
-
|
|
270
|
+
atomicWrite(path, JSON.stringify(task, null, 2));
|
|
225
271
|
}
|
|
226
272
|
|
|
227
273
|
/**
|
|
@@ -323,7 +369,7 @@ function cleanTaskOrphans(
|
|
|
323
369
|
// Write back if modified
|
|
324
370
|
if (modified) {
|
|
325
371
|
task.updated_at = new Date().toISOString();
|
|
326
|
-
|
|
372
|
+
atomicWrite(path, JSON.stringify(task, null, 2));
|
|
327
373
|
}
|
|
328
374
|
|
|
329
375
|
return cleanup;
|
|
@@ -440,6 +486,36 @@ export function validateParent(
|
|
|
440
486
|
return { ok: true };
|
|
441
487
|
}
|
|
442
488
|
|
|
489
|
+
// --- Task Enrichment ---
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Enriches a Task with computed metadata.
|
|
493
|
+
* Can optionally take a status map for efficient bulk processing of blockers.
|
|
494
|
+
*/
|
|
495
|
+
export function enrichTask(task: Task, statusMap?: Map<string, Status>): TaskWithMeta {
|
|
496
|
+
const tasksDir = getTasksDir();
|
|
497
|
+
let blockedByIncomplete = false;
|
|
498
|
+
|
|
499
|
+
for (const blockerId of task.blocked_by) {
|
|
500
|
+
let status = statusMap?.get(blockerId);
|
|
501
|
+
if (status === undefined) {
|
|
502
|
+
status = readTaskFile(getTaskPath(tasksDir, blockerId), tasksDir)?.status;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (status && status !== "done") {
|
|
506
|
+
blockedByIncomplete = true;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
...task,
|
|
513
|
+
id: taskId(task),
|
|
514
|
+
is_overdue: isTaskOverdue(task.due_date, task.status),
|
|
515
|
+
blocked_by_incomplete: blockedByIncomplete,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
443
519
|
// --- Task CRUD ---
|
|
444
520
|
|
|
445
521
|
export interface CreateTaskOptions {
|
|
@@ -488,7 +564,7 @@ export function createTask(options: CreateTaskOptions): Task & { id: string } {
|
|
|
488
564
|
|
|
489
565
|
const path = getTaskPath(tasksDir, id);
|
|
490
566
|
if (writeTaskFileExclusive(path, task)) {
|
|
491
|
-
return
|
|
567
|
+
return enrichTask(task);
|
|
492
568
|
}
|
|
493
569
|
// Collision (extremely rare with 4 chars) - retry with new ref
|
|
494
570
|
}
|
|
@@ -497,7 +573,7 @@ export function createTask(options: CreateTaskOptions): Task & { id: string } {
|
|
|
497
573
|
}
|
|
498
574
|
|
|
499
575
|
export interface TaskResult {
|
|
500
|
-
task:
|
|
576
|
+
task: TaskWithMeta;
|
|
501
577
|
cleanup: CleanupInfo | null;
|
|
502
578
|
}
|
|
503
579
|
|
|
@@ -513,48 +589,13 @@ export function getTask(id: string): TaskResult | null {
|
|
|
513
589
|
const cleanup = cleanTaskOrphans(task, path, id, tasksDir);
|
|
514
590
|
|
|
515
591
|
return {
|
|
516
|
-
task:
|
|
592
|
+
task: enrichTask(task),
|
|
517
593
|
cleanup: hasCleanup(cleanup) ? cleanup : null,
|
|
518
594
|
};
|
|
519
595
|
}
|
|
520
596
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
cleanup: CleanupInfo | null;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
export function getTaskWithMeta(id: string): TaskWithMetaResult | null {
|
|
527
|
-
const tasksDir = getTasksDir();
|
|
528
|
-
if (!existsSync(tasksDir)) return null;
|
|
529
|
-
|
|
530
|
-
const path = getTaskPath(tasksDir, id);
|
|
531
|
-
const task = readTaskFile(path, tasksDir);
|
|
532
|
-
if (!task) return null;
|
|
533
|
-
|
|
534
|
-
// Auto-clean orphaned references
|
|
535
|
-
const cleanup = cleanTaskOrphans(task, path, id, tasksDir);
|
|
536
|
-
|
|
537
|
-
// Check if any blockers are incomplete (after cleanup, all blockers exist)
|
|
538
|
-
let blockedByIncomplete = false;
|
|
539
|
-
for (const blockerId of task.blocked_by) {
|
|
540
|
-
const blockerTask = readTaskFile(getTaskPath(tasksDir, blockerId), tasksDir);
|
|
541
|
-
if (!blockerTask) continue;
|
|
542
|
-
if (blockerTask.status !== "done") {
|
|
543
|
-
blockedByIncomplete = true;
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return {
|
|
549
|
-
task: {
|
|
550
|
-
...task,
|
|
551
|
-
id: taskId(task),
|
|
552
|
-
blocked_by_incomplete: blockedByIncomplete,
|
|
553
|
-
is_overdue: isTaskOverdue(task.due_date, task.status),
|
|
554
|
-
},
|
|
555
|
-
cleanup: hasCleanup(cleanup) ? cleanup : null,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
597
|
+
// Deprecated alias for getTask
|
|
598
|
+
export const getTaskWithMeta = getTask;
|
|
558
599
|
|
|
559
600
|
export interface ListOptions {
|
|
560
601
|
status?: Status;
|
|
@@ -568,82 +609,63 @@ export interface ListOptions {
|
|
|
568
609
|
limit?: number;
|
|
569
610
|
}
|
|
570
611
|
|
|
571
|
-
export function listTasks(options?: ListOptions):
|
|
612
|
+
export function listTasks(options?: ListOptions): TaskWithMeta[] {
|
|
572
613
|
const tasksDir = getTasksDir();
|
|
573
614
|
if (!existsSync(tasksDir)) return [];
|
|
574
615
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
tasks = tasks.filter((t) => t.parent === null);
|
|
598
|
-
}
|
|
599
|
-
if (options?.overdue) {
|
|
600
|
-
tasks = tasks.filter((t) => isTaskOverdue(t.due_date, t.status));
|
|
616
|
+
const allTasks = getAllTasks(tasksDir);
|
|
617
|
+
// Build status map from all tasks once for efficient blocker lookups during enrichment
|
|
618
|
+
const statusMap = new Map<string, Status>(allTasks.map((t) => [taskId(t), t.status]));
|
|
619
|
+
|
|
620
|
+
let tasks = allTasks;
|
|
621
|
+
|
|
622
|
+
// Apply filters declaratively
|
|
623
|
+
if (options) {
|
|
624
|
+
const filters: ((t: Task) => boolean)[] = [];
|
|
625
|
+
|
|
626
|
+
if (options.status) filters.push((t) => t.status === options.status);
|
|
627
|
+
if (options.priority !== undefined) filters.push((t) => t.priority === options.priority);
|
|
628
|
+
if (options.project) filters.push((t) => t.project === options.project);
|
|
629
|
+
if (options.label) filters.push((t) => t.labels.includes(options.label!));
|
|
630
|
+
if (options.assignee) filters.push((t) => t.assignees.includes(options.assignee!));
|
|
631
|
+
if (options.parent) filters.push((t) => t.parent === options.parent);
|
|
632
|
+
if (options.roots) filters.push((t) => t.parent === null);
|
|
633
|
+
if (options.overdue) filters.push((t) => isTaskOverdue(t.due_date, t.status));
|
|
634
|
+
|
|
635
|
+
if (filters.length > 0) {
|
|
636
|
+
tasks = tasks.filter((t) => filters.every((f) => f(t)));
|
|
637
|
+
}
|
|
601
638
|
}
|
|
602
639
|
|
|
603
|
-
|
|
604
|
-
tasks.sort((a, b) => {
|
|
605
|
-
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
606
|
-
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
607
|
-
});
|
|
640
|
+
tasks.sort(compareTasks);
|
|
608
641
|
|
|
609
642
|
if (options?.limit) {
|
|
610
643
|
tasks = tasks.slice(0, options.limit);
|
|
611
644
|
}
|
|
612
645
|
|
|
613
|
-
return tasks.map((t) => (
|
|
646
|
+
return tasks.map((t) => enrichTask(t, statusMap));
|
|
614
647
|
}
|
|
615
648
|
|
|
616
|
-
export function listReadyTasks():
|
|
649
|
+
export function listReadyTasks(): TaskWithMeta[] {
|
|
617
650
|
const tasksDir = getTasksDir();
|
|
618
651
|
if (!existsSync(tasksDir)) return [];
|
|
619
652
|
|
|
620
|
-
// Get all tasks once and build status map for O(1) blocker lookups
|
|
621
653
|
const allTasks = getAllTasks(tasksDir);
|
|
622
|
-
const statusMap = new Map<string, Status>();
|
|
623
|
-
for (const task of allTasks) {
|
|
624
|
-
statusMap.set(taskId(task), task.status);
|
|
625
|
-
}
|
|
654
|
+
const statusMap = new Map<string, Status>(allTasks.map((t) => [taskId(t), t.status]));
|
|
626
655
|
|
|
627
|
-
// Filter to open tasks that have no incomplete blockers
|
|
628
656
|
const readyTasks = allTasks
|
|
629
|
-
.filter((task) => task.status === "open")
|
|
657
|
+
.filter((task) => task.status === "active" || task.status === "open")
|
|
630
658
|
.filter((task) => {
|
|
631
659
|
for (const blockerId of task.blocked_by) {
|
|
632
660
|
const blockerStatus = statusMap.get(blockerId);
|
|
633
|
-
|
|
634
|
-
if (!blockerStatus) continue;
|
|
635
|
-
if (blockerStatus !== "done") return false;
|
|
661
|
+
if (blockerStatus && blockerStatus !== "done") return false;
|
|
636
662
|
}
|
|
637
663
|
return true;
|
|
638
664
|
});
|
|
639
665
|
|
|
640
|
-
|
|
641
|
-
readyTasks.sort((a, b) => {
|
|
642
|
-
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
643
|
-
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
644
|
-
});
|
|
666
|
+
readyTasks.sort(compareTasks);
|
|
645
667
|
|
|
646
|
-
return readyTasks.map((t) => (
|
|
668
|
+
return readyTasks.map((t) => enrichTask(t, statusMap));
|
|
647
669
|
}
|
|
648
670
|
|
|
649
671
|
export function updateTaskStatus(id: string, status: Status): (Task & { id: string }) | null {
|
|
@@ -658,7 +680,7 @@ export function updateTaskStatus(id: string, status: Status): (Task & { id: stri
|
|
|
658
680
|
task.completed_at = status === "done" ? now : null;
|
|
659
681
|
|
|
660
682
|
writeTaskFile(path, task);
|
|
661
|
-
return
|
|
683
|
+
return enrichTask(task);
|
|
662
684
|
}
|
|
663
685
|
|
|
664
686
|
export interface UpdateTaskOptions {
|
|
@@ -690,7 +712,7 @@ export function updateTask(id: string, updates: UpdateTaskOptions): (Task & { id
|
|
|
690
712
|
task.updated_at = now;
|
|
691
713
|
|
|
692
714
|
writeTaskFile(path, task);
|
|
693
|
-
return
|
|
715
|
+
return enrichTask(task);
|
|
694
716
|
}
|
|
695
717
|
|
|
696
718
|
export function deleteTask(id: string): boolean {
|
|
@@ -705,7 +727,7 @@ export function deleteTask(id: string): boolean {
|
|
|
705
727
|
for (const task of allTasks) {
|
|
706
728
|
const modified = removeRefsFromTask(task, (ref) => ref === id);
|
|
707
729
|
if (modified) {
|
|
708
|
-
|
|
730
|
+
atomicWrite(getTaskPath(tasksDir, taskId(task)), JSON.stringify(task, null, 2));
|
|
709
731
|
}
|
|
710
732
|
}
|
|
711
733
|
|
|
@@ -749,7 +771,7 @@ export function cleanTasks(options: CleanOptions): number {
|
|
|
749
771
|
for (const task of remaining) {
|
|
750
772
|
const modified = removeRefsFromTask(task, (id) => toDeleteSet.has(id));
|
|
751
773
|
if (modified) {
|
|
752
|
-
|
|
774
|
+
atomicWrite(getTaskPath(tasksDir, taskId(task)), JSON.stringify(task, null, 2));
|
|
753
775
|
}
|
|
754
776
|
}
|
|
755
777
|
}
|
|
@@ -775,7 +797,7 @@ export function addLogEntry(taskIdStr: string, message: string): LogEntry {
|
|
|
775
797
|
|
|
776
798
|
task.logs.push(entry);
|
|
777
799
|
task.updated_at = now;
|
|
778
|
-
|
|
800
|
+
atomicWrite(path, JSON.stringify(task, null, 2));
|
|
779
801
|
|
|
780
802
|
return entry;
|
|
781
803
|
}
|
|
@@ -806,7 +828,7 @@ export function addBlock(taskIdStr: string, blockedBy: string): { ok: boolean; e
|
|
|
806
828
|
|
|
807
829
|
task.blocked_by.push(blockedBy);
|
|
808
830
|
task.updated_at = new Date().toISOString();
|
|
809
|
-
|
|
831
|
+
atomicWrite(taskPath, JSON.stringify(task, null, 2));
|
|
810
832
|
|
|
811
833
|
return { ok: true };
|
|
812
834
|
}
|
|
@@ -822,7 +844,7 @@ export function removeBlock(taskIdStr: string, blockedBy: string): boolean {
|
|
|
822
844
|
|
|
823
845
|
task.blocked_by.splice(idx, 1);
|
|
824
846
|
task.updated_at = new Date().toISOString();
|
|
825
|
-
|
|
847
|
+
atomicWrite(taskPath, JSON.stringify(task, null, 2));
|
|
826
848
|
|
|
827
849
|
return true;
|
|
828
850
|
}
|
package/src/lib/completions.ts
CHANGED
|
@@ -130,7 +130,7 @@ _tk() {
|
|
|
130
130
|
'add:Create task'
|
|
131
131
|
'ls:List tasks'
|
|
132
132
|
'list:List tasks'
|
|
133
|
-
'ready:List ready tasks (open + unblocked)'
|
|
133
|
+
'ready:List ready tasks (active/open + unblocked)'
|
|
134
134
|
'show:Show task details'
|
|
135
135
|
'start:Start working on task'
|
|
136
136
|
'done:Complete task'
|
|
@@ -164,97 +164,97 @@ _tk() {
|
|
|
164
164
|
local -a shells
|
|
165
165
|
shells=('bash' 'zsh' 'fish')
|
|
166
166
|
|
|
167
|
-
_arguments -C
|
|
168
|
-
|
|
169
|
-
'1:command:->command'
|
|
167
|
+
_arguments -C \
|
|
168
|
+
$global_opts \
|
|
169
|
+
'1:command:->command' \
|
|
170
170
|
'*::arg:->args'
|
|
171
171
|
|
|
172
|
-
case
|
|
172
|
+
case $state in
|
|
173
173
|
command)
|
|
174
174
|
_describe -t commands 'tk command' commands
|
|
175
175
|
;;
|
|
176
176
|
args)
|
|
177
|
-
case
|
|
177
|
+
case $words[1] in
|
|
178
178
|
add)
|
|
179
|
-
_arguments
|
|
180
|
-
'(-p --priority)'{-p,--priority}'[Priority]:priority:(
|
|
181
|
-
'(-P --project)'{-P,--project}'[Project]:project:'
|
|
182
|
-
'(-d --description)'{-d,--description}'[Description]:description:'
|
|
183
|
-
'(-l --labels)'{-l,--labels}'[Labels]:labels:'
|
|
184
|
-
'(-A --assignees)'{-A,--assignees}'[Assignees]:assignees:'
|
|
185
|
-
'--parent[Parent task]:parent:_tk_task_ids'
|
|
186
|
-
'--estimate[Estimate]:estimate:'
|
|
187
|
-
'--due[Due date]:due:'
|
|
188
|
-
'--json[Output as JSON]'
|
|
179
|
+
_arguments \
|
|
180
|
+
'(-p --priority)'{-p,--priority}'[Priority]:priority:($priorities)' \
|
|
181
|
+
'(-P --project)'{-P,--project}'[Project]:project:' \
|
|
182
|
+
'(-d --description)'{-d,--description}'[Description]:description:' \
|
|
183
|
+
'(-l --labels)'{-l,--labels}'[Labels]:labels:' \
|
|
184
|
+
'(-A --assignees)'{-A,--assignees}'[Assignees]:assignees:' \
|
|
185
|
+
'--parent[Parent task]:parent:_tk_task_ids' \
|
|
186
|
+
'--estimate[Estimate]:estimate:' \
|
|
187
|
+
'--due[Due date]:due:' \
|
|
188
|
+
'--json[Output as JSON]' \
|
|
189
189
|
'*:title:'
|
|
190
190
|
;;
|
|
191
191
|
ls|list)
|
|
192
|
-
_arguments
|
|
193
|
-
'(-s --status)'{-s,--status}'[Status]:status:(
|
|
194
|
-
'(-p --priority)'{-p,--priority}'[Priority]:priority:(
|
|
195
|
-
'(-P --project)'{-P,--project}'[Project]:project:'
|
|
196
|
-
'(-l --label)'{-l,--label}'[Label]:label:'
|
|
197
|
-
'--assignee[Assignee]:assignee:'
|
|
198
|
-
'--parent[Parent]:parent:_tk_task_ids'
|
|
199
|
-
'--roots[Root tasks only]'
|
|
200
|
-
'--overdue[Overdue only]'
|
|
201
|
-
'(-n --limit)'{-n,--limit}'[Limit]:limit:'
|
|
202
|
-
'(-a --all)'{-a,--all}'[Show all]'
|
|
192
|
+
_arguments \
|
|
193
|
+
'(-s --status)'{-s,--status}'[Status]:status:($statuses)' \
|
|
194
|
+
'(-p --priority)'{-p,--priority}'[Priority]:priority:($priorities)' \
|
|
195
|
+
'(-P --project)'{-P,--project}'[Project]:project:' \
|
|
196
|
+
'(-l --label)'{-l,--label}'[Label]:label:' \
|
|
197
|
+
'--assignee[Assignee]:assignee:' \
|
|
198
|
+
'--parent[Parent]:parent:_tk_task_ids' \
|
|
199
|
+
'--roots[Root tasks only]' \
|
|
200
|
+
'--overdue[Overdue only]' \
|
|
201
|
+
'(-n --limit)'{-n,--limit}'[Limit]:limit:' \
|
|
202
|
+
'(-a --all)'{-a,--all}'[Show all]' \
|
|
203
203
|
'--json[Output as JSON]'
|
|
204
204
|
;;
|
|
205
205
|
show|start|done|reopen|rm|remove)
|
|
206
|
-
_arguments
|
|
207
|
-
'--json[Output as JSON]'
|
|
206
|
+
_arguments \
|
|
207
|
+
'--json[Output as JSON]' \
|
|
208
208
|
'1:task id:_tk_task_ids'
|
|
209
209
|
;;
|
|
210
210
|
edit)
|
|
211
|
-
_arguments
|
|
212
|
-
'(-t --title)'{-t,--title}'[Title]:title:'
|
|
213
|
-
'(-d --description)'{-d,--description}'[Description]:description:'
|
|
214
|
-
'(-p --priority)'{-p,--priority}'[Priority]:priority:(
|
|
215
|
-
'(-l --labels)'{-l,--labels}'[Labels]:labels:'
|
|
216
|
-
'(-A --assignees)'{-A,--assignees}'[Assignees]:assignees:'
|
|
217
|
-
'--parent[Parent task]:parent:'
|
|
218
|
-
'--estimate[Estimate]:estimate:'
|
|
219
|
-
'--due[Due date]:due:'
|
|
220
|
-
'--json[Output as JSON]'
|
|
211
|
+
_arguments \
|
|
212
|
+
'(-t --title)'{-t,--title}'[Title]:title:' \
|
|
213
|
+
'(-d --description)'{-d,--description}'[Description]:description:' \
|
|
214
|
+
'(-p --priority)'{-p,--priority}'[Priority]:priority:($priorities)' \
|
|
215
|
+
'(-l --labels)'{-l,--labels}'[Labels]:labels:' \
|
|
216
|
+
'(-A --assignees)'{-A,--assignees}'[Assignees]:assignees:' \
|
|
217
|
+
'--parent[Parent task]:parent:' \
|
|
218
|
+
'--estimate[Estimate]:estimate:' \
|
|
219
|
+
'--due[Due date]:due:' \
|
|
220
|
+
'--json[Output as JSON]' \
|
|
221
221
|
'1:task id:_tk_task_ids'
|
|
222
222
|
;;
|
|
223
223
|
log)
|
|
224
|
-
_arguments
|
|
225
|
-
'--json[Output as JSON]'
|
|
226
|
-
'1:task id:_tk_task_ids'
|
|
224
|
+
_arguments \
|
|
225
|
+
'--json[Output as JSON]' \
|
|
226
|
+
'1:task id:_tk_task_ids' \
|
|
227
227
|
'*:message:'
|
|
228
228
|
;;
|
|
229
229
|
block|unblock)
|
|
230
|
-
_arguments
|
|
231
|
-
'--json[Output as JSON]'
|
|
232
|
-
'1:task id:_tk_task_ids'
|
|
230
|
+
_arguments \
|
|
231
|
+
'--json[Output as JSON]' \
|
|
232
|
+
'1:task id:_tk_task_ids' \
|
|
233
233
|
'2:blocker id:_tk_task_ids'
|
|
234
234
|
;;
|
|
235
235
|
clean)
|
|
236
|
-
_arguments
|
|
237
|
-
'--older-than[Days]:days:'
|
|
238
|
-
'(-f --force)'{-f,--force}'[Force clean even if disabled]'
|
|
236
|
+
_arguments \
|
|
237
|
+
'--older-than[Days]:days:' \
|
|
238
|
+
'(-f --force)'{-f,--force}'[Force clean even if disabled]' \
|
|
239
239
|
'--json[Output as JSON]'
|
|
240
240
|
;;
|
|
241
241
|
check)
|
|
242
|
-
_arguments
|
|
242
|
+
_arguments \
|
|
243
243
|
'--json[Output as JSON]'
|
|
244
244
|
;;
|
|
245
245
|
config)
|
|
246
|
-
_arguments
|
|
247
|
-
'--rm[Remove alias]'
|
|
246
|
+
_arguments \
|
|
247
|
+
'--rm[Remove alias]' \
|
|
248
248
|
'1:subcommand:(project alias)'
|
|
249
249
|
;;
|
|
250
250
|
init)
|
|
251
|
-
_arguments
|
|
252
|
-
'(-P --project)'{-P,--project}'[Project]:project:'
|
|
251
|
+
_arguments \
|
|
252
|
+
'(-P --project)'{-P,--project}'[Project]:project:' \
|
|
253
253
|
'--json[Output as JSON]'
|
|
254
254
|
;;
|
|
255
255
|
completions)
|
|
256
|
-
_arguments
|
|
257
|
-
'1:shell:(
|
|
256
|
+
_arguments \
|
|
257
|
+
'1:shell:($shells)'
|
|
258
258
|
;;
|
|
259
259
|
ready|help)
|
|
260
260
|
_arguments \\
|
|
@@ -323,7 +323,7 @@ complete -c tk -n __tk_needs_command -f -a init -d 'Initialize .tasks/ directory
|
|
|
323
323
|
complete -c tk -n __tk_needs_command -f -a add -d 'Create task'
|
|
324
324
|
complete -c tk -n __tk_needs_command -f -a ls -d 'List tasks'
|
|
325
325
|
complete -c tk -n __tk_needs_command -f -a list -d 'List tasks'
|
|
326
|
-
complete -c tk -n __tk_needs_command -f -a ready -d 'List ready tasks (open + unblocked)'
|
|
326
|
+
complete -c tk -n __tk_needs_command -f -a ready -d 'List ready tasks (active/open + unblocked)'
|
|
327
327
|
complete -c tk -n __tk_needs_command -f -a show -d 'Show task details'
|
|
328
328
|
complete -c tk -n __tk_needs_command -f -a start -d 'Start working on task'
|
|
329
329
|
complete -c tk -n __tk_needs_command -f -a done -d 'Complete task'
|