@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/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
- function isTaskOverdue(dueDate: string | null, status: Status): boolean {
54
- if (!dueDate || status === "done") return false;
55
- const today = new Date();
56
- today.setHours(0, 0, 0, 0);
57
- const parts = dueDate.split("-").map(Number);
58
- const year = parts[0];
59
- const month = parts[1];
60
- const day = parts[2];
61
- if (!year || !month || !day) return false;
62
- const due = new Date(year, month - 1, day);
63
- return due < today;
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
- writeFileSync(configPath, JSON.stringify(config, null, 2));
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
- writeFileSync(newPath, JSON.stringify(task, null, 2));
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
- writeFileSync(path, JSON.stringify(task, null, 2));
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
- writeFileSync(path, JSON.stringify(task, null, 2));
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
- writeTaskFile(path, task);
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 { ...task, id };
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: Task & { id: string };
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: { ...task, id: taskId(task) },
592
+ task: enrichTask(task),
517
593
  cleanup: hasCleanup(cleanup) ? cleanup : null,
518
594
  };
519
595
  }
520
596
 
521
- export interface TaskWithMetaResult {
522
- task: TaskWithMeta;
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): (Task & { id: string })[] {
612
+ export function listTasks(options?: ListOptions): TaskWithMeta[] {
572
613
  const tasksDir = getTasksDir();
573
614
  if (!existsSync(tasksDir)) return [];
574
615
 
575
- let tasks = getAllTasks(tasksDir);
576
-
577
- // Apply filters
578
- if (options?.status) {
579
- tasks = tasks.filter((t) => t.status === options.status);
580
- }
581
- if (options?.priority !== undefined) {
582
- tasks = tasks.filter((t) => t.priority === options.priority);
583
- }
584
- if (options?.project) {
585
- tasks = tasks.filter((t) => t.project === options.project);
586
- }
587
- if (options?.label) {
588
- tasks = tasks.filter((t) => t.labels.includes(options.label!));
589
- }
590
- if (options?.assignee) {
591
- tasks = tasks.filter((t) => t.assignees.includes(options.assignee!));
592
- }
593
- if (options?.parent) {
594
- tasks = tasks.filter((t) => t.parent === options.parent);
595
- }
596
- if (options?.roots) {
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
- // Sort by priority (lower = more urgent), then created_at (newer first)
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) => ({ ...t, id: taskId(t) }));
646
+ return tasks.map((t) => enrichTask(t, statusMap));
614
647
  }
615
648
 
616
- export function listReadyTasks(): (Task & { id: string })[] {
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
- // Missing blocker (orphaned ref) doesn't block
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
- // Sort by priority (lower = more urgent), then created_at (newer first)
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) => ({ ...t, id: taskId(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 { ...task, id: taskId(task) };
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 { ...task, id: taskId(task) };
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
- writeTaskFile(getTaskPath(tasksDir, taskId(task)), task);
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
- writeTaskFile(getTaskPath(tasksDir, taskId(task)), task);
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
- writeTaskFile(path, task);
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
- writeTaskFile(taskPath, task);
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
- writeTaskFile(taskPath, task);
847
+ atomicWrite(taskPath, JSON.stringify(task, null, 2));
826
848
 
827
849
  return true;
828
850
  }
@@ -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
- \$global_opts \\
169
- '1:command:->command' \\
167
+ _arguments -C \
168
+ $global_opts \
169
+ '1:command:->command' \
170
170
  '*::arg:->args'
171
171
 
172
- case \$state in
172
+ case $state in
173
173
  command)
174
174
  _describe -t commands 'tk command' commands
175
175
  ;;
176
176
  args)
177
- case \$words[1] in
177
+ case $words[1] in
178
178
  add)
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]' \\
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:(\$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]' \\
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:(\$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]' \\
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:(\$shells)'
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'