@nijaru/tk 0.0.5 → 0.1.3
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/bin/tk.js +30 -0
- package/package.json +22 -45
- package/LICENSE +0 -21
- package/README.md +0 -242
- package/src/cli.test.ts +0 -1172
- package/src/cli.ts +0 -671
- package/src/db/storage.ts +0 -1050
- package/src/lib/completions.ts +0 -440
- package/src/lib/format.test.ts +0 -433
- package/src/lib/format.ts +0 -189
- package/src/lib/help.ts +0 -266
- package/src/lib/priority.test.ts +0 -105
- package/src/lib/priority.ts +0 -40
- package/src/lib/root.ts +0 -79
- package/src/lib/time.test.ts +0 -222
- package/src/lib/time.ts +0 -138
- package/src/types.ts +0 -130
package/src/lib/help.ts
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { version } from "../../package.json";
|
|
2
|
-
|
|
3
|
-
export const MAIN_HELP = `tk v${version} - Task tracker for AI agents
|
|
4
|
-
|
|
5
|
-
USAGE:
|
|
6
|
-
tk <command> [options]
|
|
7
|
-
|
|
8
|
-
COMMANDS:
|
|
9
|
-
init Initialize .tasks/ directory
|
|
10
|
-
add Create task
|
|
11
|
-
ls, list List tasks
|
|
12
|
-
ready List ready tasks (active/open + unblocked)
|
|
13
|
-
show Show task details
|
|
14
|
-
start Start working (open -> active)
|
|
15
|
-
done Complete task
|
|
16
|
-
reopen Reopen task
|
|
17
|
-
edit Edit task
|
|
18
|
-
log Add log entry
|
|
19
|
-
block Add blocker
|
|
20
|
-
unblock Remove blocker
|
|
21
|
-
mv, move Move task to different project
|
|
22
|
-
rm, remove Delete task
|
|
23
|
-
clean Remove old done tasks
|
|
24
|
-
check Check for data issues
|
|
25
|
-
config Show/set configuration
|
|
26
|
-
completions Output shell completions
|
|
27
|
-
|
|
28
|
-
GLOBAL OPTIONS:
|
|
29
|
-
-C <dir> Run in different directory
|
|
30
|
-
--json Output as JSON
|
|
31
|
-
-h, --help Show help
|
|
32
|
-
-V Show version
|
|
33
|
-
|
|
34
|
-
Run 'tk <command> --help' for command-specific options.
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
export const COMMAND_HELP: Record<string, string> = {
|
|
38
|
-
init: `tk init - Initialize .tasks/ directory
|
|
39
|
-
|
|
40
|
-
USAGE:
|
|
41
|
-
tk init [options]
|
|
42
|
-
|
|
43
|
-
OPTIONS:
|
|
44
|
-
-P, --project <name> Set default project name
|
|
45
|
-
`,
|
|
46
|
-
add: `tk add - Create a new task
|
|
47
|
-
|
|
48
|
-
USAGE:
|
|
49
|
-
tk add <title> [options]
|
|
50
|
-
|
|
51
|
-
OPTIONS:
|
|
52
|
-
-p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
|
|
53
|
-
-P, --project <name> Project prefix for ID
|
|
54
|
-
-d, --description <text> Description
|
|
55
|
-
-l, --labels <csv> Labels (comma-separated)
|
|
56
|
-
-A, --assignees <csv> Assignees (comma-separated, @me for git user)
|
|
57
|
-
--parent <id> Parent task ID
|
|
58
|
-
--estimate <n> Estimate (user-defined units)
|
|
59
|
-
--due <date> Due date (YYYY-MM-DD or +Nh/+Nd/+Nw/+Nm)
|
|
60
|
-
|
|
61
|
-
EXAMPLES:
|
|
62
|
-
tk add "Fix login bug" -p 1
|
|
63
|
-
tk add "New feature" -P api -l bug,urgent
|
|
64
|
-
tk add "Sprint task" --due +7d
|
|
65
|
-
`,
|
|
66
|
-
ls: `tk ls - List tasks
|
|
67
|
-
|
|
68
|
-
USAGE:
|
|
69
|
-
tk ls [options]
|
|
70
|
-
|
|
71
|
-
OPTIONS:
|
|
72
|
-
-s, --status <status> Filter by status (open, active, done)
|
|
73
|
-
-p, --priority <0-4> Filter by priority
|
|
74
|
-
-P, --project <name> Filter by project
|
|
75
|
-
-l, --label <label> Filter by label
|
|
76
|
-
--assignee <name> Filter by assignee
|
|
77
|
-
--parent <id> Filter by parent
|
|
78
|
-
--roots Show only root tasks (no parent)
|
|
79
|
-
--overdue Show only overdue tasks
|
|
80
|
-
-n, --limit <n> Limit results (default: 20)
|
|
81
|
-
-a, --all Show all (no limit)
|
|
82
|
-
|
|
83
|
-
EXAMPLES:
|
|
84
|
-
tk ls -s open -p 1 # Urgent open tasks
|
|
85
|
-
tk ls --overdue # Overdue tasks
|
|
86
|
-
tk ls -P api --roots # Root tasks in api project
|
|
87
|
-
`,
|
|
88
|
-
list: `tk list - List tasks (alias for 'ls')
|
|
89
|
-
|
|
90
|
-
Run 'tk ls --help' for options.
|
|
91
|
-
`,
|
|
92
|
-
ready: `tk ready - List ready tasks (active/open + unblocked)
|
|
93
|
-
|
|
94
|
-
USAGE:
|
|
95
|
-
tk ready
|
|
96
|
-
|
|
97
|
-
Shows active or open tasks that are not blocked by any incomplete task.
|
|
98
|
-
`,
|
|
99
|
-
show: `tk show - Show task details
|
|
100
|
-
|
|
101
|
-
USAGE:
|
|
102
|
-
tk show <id>
|
|
103
|
-
|
|
104
|
-
EXAMPLES:
|
|
105
|
-
tk show tk-1
|
|
106
|
-
tk show 1 # If unambiguous
|
|
107
|
-
`,
|
|
108
|
-
start: `tk start - Start working on a task
|
|
109
|
-
|
|
110
|
-
USAGE:
|
|
111
|
-
tk start <id>
|
|
112
|
-
|
|
113
|
-
Changes status from open to active.
|
|
114
|
-
`,
|
|
115
|
-
done: `tk done - Complete a task
|
|
116
|
-
|
|
117
|
-
USAGE:
|
|
118
|
-
tk done <id>
|
|
119
|
-
|
|
120
|
-
Changes status to done.
|
|
121
|
-
`,
|
|
122
|
-
reopen: `tk reopen - Reopen a task
|
|
123
|
-
|
|
124
|
-
USAGE:
|
|
125
|
-
tk reopen <id>
|
|
126
|
-
|
|
127
|
-
Changes status back to open.
|
|
128
|
-
`,
|
|
129
|
-
edit: `tk edit - Edit a task
|
|
130
|
-
|
|
131
|
-
USAGE:
|
|
132
|
-
tk edit <id> [options]
|
|
133
|
-
|
|
134
|
-
OPTIONS:
|
|
135
|
-
-t, --title <text> New title
|
|
136
|
-
-d, --description <text> New description
|
|
137
|
-
-p, --priority <0-4> New priority
|
|
138
|
-
-l, --labels <csv> Replace labels (use +tag/-tag to add/remove)
|
|
139
|
-
-A, --assignees <csv> Replace assignees
|
|
140
|
-
--parent <id> Set parent (use - to clear)
|
|
141
|
-
--estimate <n> Set estimate (use - to clear)
|
|
142
|
-
--due <date> Set due date (use - to clear)
|
|
143
|
-
|
|
144
|
-
EXAMPLES:
|
|
145
|
-
tk edit tk-1 -t "New title"
|
|
146
|
-
tk edit tk-1 -l +urgent # Add label
|
|
147
|
-
tk edit tk-1 --due - # Clear due date
|
|
148
|
-
`,
|
|
149
|
-
log: `tk log - Add a log entry to a task
|
|
150
|
-
|
|
151
|
-
USAGE:
|
|
152
|
-
tk log <id> "<message>"
|
|
153
|
-
|
|
154
|
-
Message must be quoted.
|
|
155
|
-
|
|
156
|
-
EXAMPLES:
|
|
157
|
-
tk log tk-1 "Started implementation"
|
|
158
|
-
tk log tk-1 "Blocked on API changes"
|
|
159
|
-
`,
|
|
160
|
-
block: `tk block - Add a blocker dependency
|
|
161
|
-
|
|
162
|
-
USAGE:
|
|
163
|
-
tk block <task> <blocker>
|
|
164
|
-
|
|
165
|
-
The first task becomes blocked by the second.
|
|
166
|
-
|
|
167
|
-
EXAMPLES:
|
|
168
|
-
tk block tk-2 tk-1 # tk-2 is blocked by tk-1
|
|
169
|
-
`,
|
|
170
|
-
unblock: `tk unblock - Remove a blocker dependency
|
|
171
|
-
|
|
172
|
-
USAGE:
|
|
173
|
-
tk unblock <task> <blocker>
|
|
174
|
-
|
|
175
|
-
EXAMPLES:
|
|
176
|
-
tk unblock tk-2 tk-1
|
|
177
|
-
`,
|
|
178
|
-
mv: `tk mv - Move a task to a different project
|
|
179
|
-
|
|
180
|
-
USAGE:
|
|
181
|
-
tk mv <id> <project>
|
|
182
|
-
|
|
183
|
-
Moves a task to a new project, keeping the same ref.
|
|
184
|
-
Errors if the new ID already exists.
|
|
185
|
-
Updates all blocked_by and parent references.
|
|
186
|
-
|
|
187
|
-
EXAMPLES:
|
|
188
|
-
tk mv api-a1b2 web # Moves api-a1b2 → web-a1b2
|
|
189
|
-
tk mv a1b2 archive # Partial ID resolution
|
|
190
|
-
`,
|
|
191
|
-
move: `tk move - Move a task to a different project (alias for 'mv')
|
|
192
|
-
|
|
193
|
-
USAGE:
|
|
194
|
-
tk move <id> <project>
|
|
195
|
-
|
|
196
|
-
Run 'tk mv --help' for options.
|
|
197
|
-
`,
|
|
198
|
-
rm: `tk rm - Delete a task
|
|
199
|
-
|
|
200
|
-
USAGE:
|
|
201
|
-
tk rm <id>
|
|
202
|
-
|
|
203
|
-
EXAMPLES:
|
|
204
|
-
tk rm tk-1
|
|
205
|
-
`,
|
|
206
|
-
remove: `tk remove - Delete a task (alias for 'rm')
|
|
207
|
-
|
|
208
|
-
USAGE:
|
|
209
|
-
tk remove <id>
|
|
210
|
-
`,
|
|
211
|
-
clean: `tk clean - Remove old completed tasks
|
|
212
|
-
|
|
213
|
-
USAGE:
|
|
214
|
-
tk clean [options]
|
|
215
|
-
|
|
216
|
-
OPTIONS:
|
|
217
|
-
--older-than <days> Age threshold in days (default: from config, 14)
|
|
218
|
-
-f, --force Remove all done tasks (ignores age and disabled state)
|
|
219
|
-
|
|
220
|
-
EXAMPLES:
|
|
221
|
-
tk clean # Remove done tasks older than config.clean_after days
|
|
222
|
-
tk clean --older-than 30 # Remove done tasks older than 30 days
|
|
223
|
-
tk clean --force # Remove all done tasks regardless of age
|
|
224
|
-
`,
|
|
225
|
-
check: `tk check - Check for data issues
|
|
226
|
-
|
|
227
|
-
USAGE:
|
|
228
|
-
tk check
|
|
229
|
-
|
|
230
|
-
Scans all tasks for issues. Auto-fixable issues (orphaned references, ID
|
|
231
|
-
mismatches) are fixed automatically. Unfixable issues (corrupted JSON) are
|
|
232
|
-
reported for manual intervention.
|
|
233
|
-
|
|
234
|
-
Note: Auto-fixing also happens during normal task operations (show, done,
|
|
235
|
-
edit, etc.) - this command is for bulk cleanup or diagnostics.
|
|
236
|
-
|
|
237
|
-
EXAMPLES:
|
|
238
|
-
tk check # Scan and fix all tasks
|
|
239
|
-
`,
|
|
240
|
-
config: `tk config - Show or set configuration
|
|
241
|
-
|
|
242
|
-
USAGE:
|
|
243
|
-
tk config Show all config
|
|
244
|
-
tk config project Show default project
|
|
245
|
-
tk config project <name> Set default project
|
|
246
|
-
tk config project <new> --rename <old> Rename project (old-* → new-*)
|
|
247
|
-
|
|
248
|
-
EXAMPLES:
|
|
249
|
-
tk config project api
|
|
250
|
-
tk config project lsmvec --rename cloudlsmvec
|
|
251
|
-
`,
|
|
252
|
-
completions: `tk completions - Output shell completions
|
|
253
|
-
|
|
254
|
-
USAGE:
|
|
255
|
-
tk completions <shell>
|
|
256
|
-
|
|
257
|
-
SHELLS:
|
|
258
|
-
bash Bash completion script
|
|
259
|
-
zsh Zsh completion script
|
|
260
|
-
fish Fish completion script
|
|
261
|
-
|
|
262
|
-
EXAMPLES:
|
|
263
|
-
eval "$(tk completions bash)" # Add to ~/.bashrc
|
|
264
|
-
eval "$(tk completions zsh)" # Add to ~/.zshrc
|
|
265
|
-
`,
|
|
266
|
-
};
|
package/src/lib/priority.test.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from "bun:test";
|
|
2
|
-
import { parsePriority, formatPriority, formatPriorityName } from "./priority";
|
|
3
|
-
|
|
4
|
-
describe("parsePriority", () => {
|
|
5
|
-
describe("valid inputs", () => {
|
|
6
|
-
test("returns 3 (medium) for undefined", () => {
|
|
7
|
-
expect(parsePriority(undefined)).toBe(3);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("parses numeric strings 0-4", () => {
|
|
11
|
-
expect(parsePriority("0")).toBe(0);
|
|
12
|
-
expect(parsePriority("1")).toBe(1);
|
|
13
|
-
expect(parsePriority("2")).toBe(2);
|
|
14
|
-
expect(parsePriority("3")).toBe(3);
|
|
15
|
-
expect(parsePriority("4")).toBe(4);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("parses numbers 0-4", () => {
|
|
19
|
-
expect(parsePriority(0)).toBe(0);
|
|
20
|
-
expect(parsePriority(1)).toBe(1);
|
|
21
|
-
expect(parsePriority(2)).toBe(2);
|
|
22
|
-
expect(parsePriority(3)).toBe(3);
|
|
23
|
-
expect(parsePriority(4)).toBe(4);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("parses p0-p4 format (lowercase)", () => {
|
|
27
|
-
expect(parsePriority("p0")).toBe(0);
|
|
28
|
-
expect(parsePriority("p1")).toBe(1);
|
|
29
|
-
expect(parsePriority("p2")).toBe(2);
|
|
30
|
-
expect(parsePriority("p3")).toBe(3);
|
|
31
|
-
expect(parsePriority("p4")).toBe(4);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("parses P0-P4 format (uppercase)", () => {
|
|
35
|
-
expect(parsePriority("P0")).toBe(0);
|
|
36
|
-
expect(parsePriority("P1")).toBe(1);
|
|
37
|
-
expect(parsePriority("P2")).toBe(2);
|
|
38
|
-
expect(parsePriority("P3")).toBe(3);
|
|
39
|
-
expect(parsePriority("P4")).toBe(4);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("parses named priorities", () => {
|
|
43
|
-
expect(parsePriority("none")).toBe(0);
|
|
44
|
-
expect(parsePriority("urgent")).toBe(1);
|
|
45
|
-
expect(parsePriority("high")).toBe(2);
|
|
46
|
-
expect(parsePriority("medium")).toBe(3);
|
|
47
|
-
expect(parsePriority("low")).toBe(4);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("parses named priorities case-insensitively", () => {
|
|
51
|
-
expect(parsePriority("URGENT")).toBe(1);
|
|
52
|
-
expect(parsePriority("High")).toBe(2);
|
|
53
|
-
expect(parsePriority("MEDIUM")).toBe(3);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("invalid inputs", () => {
|
|
58
|
-
test("throws for out of range numbers", () => {
|
|
59
|
-
expect(() => parsePriority("5")).toThrow("Invalid priority");
|
|
60
|
-
expect(() => parsePriority("-1")).toThrow("Invalid priority");
|
|
61
|
-
expect(() => parsePriority("99")).toThrow("Invalid priority");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("throws for out of range pX format", () => {
|
|
65
|
-
expect(() => parsePriority("p5")).toThrow("Invalid priority");
|
|
66
|
-
expect(() => parsePriority("p6")).toThrow("Invalid priority");
|
|
67
|
-
expect(() => parsePriority("p-1")).toThrow("Invalid priority");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("throws for invalid named priorities", () => {
|
|
71
|
-
expect(() => parsePriority("critical")).toThrow("Invalid priority");
|
|
72
|
-
expect(() => parsePriority("abc")).toThrow("Invalid priority");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("throws for empty string", () => {
|
|
76
|
-
expect(() => parsePriority("")).toThrow("Invalid priority");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("error message includes valid formats", () => {
|
|
80
|
-
expect(() => parsePriority("invalid")).toThrow(
|
|
81
|
-
"Use 0-4, p0-p4, or none/urgent/high/medium/low",
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("formatPriority", () => {
|
|
88
|
-
test("formats priorities as pX", () => {
|
|
89
|
-
expect(formatPriority(0)).toBe("p0");
|
|
90
|
-
expect(formatPriority(1)).toBe("p1");
|
|
91
|
-
expect(formatPriority(2)).toBe("p2");
|
|
92
|
-
expect(formatPriority(3)).toBe("p3");
|
|
93
|
-
expect(formatPriority(4)).toBe("p4");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("formatPriorityName", () => {
|
|
98
|
-
test("formats priorities as names", () => {
|
|
99
|
-
expect(formatPriorityName(0)).toBe("none");
|
|
100
|
-
expect(formatPriorityName(1)).toBe("urgent");
|
|
101
|
-
expect(formatPriorityName(2)).toBe("high");
|
|
102
|
-
expect(formatPriorityName(3)).toBe("medium");
|
|
103
|
-
expect(formatPriorityName(4)).toBe("low");
|
|
104
|
-
});
|
|
105
|
-
});
|
package/src/lib/priority.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { Priority } from "../types";
|
|
2
|
-
import { PRIORITY_FROM_NAME, PRIORITY_LABELS } from "../types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Parse priority from various formats:
|
|
6
|
-
* - Number: 0-4
|
|
7
|
-
* - Prefixed: p0-p4
|
|
8
|
-
* - Named: none, urgent, high, medium, low
|
|
9
|
-
*/
|
|
10
|
-
export function parsePriority(input: string | number | undefined): Priority {
|
|
11
|
-
if (input === undefined) return 3; // default: medium
|
|
12
|
-
|
|
13
|
-
const str = String(input).toLowerCase();
|
|
14
|
-
|
|
15
|
-
// Handle named format
|
|
16
|
-
const named = PRIORITY_FROM_NAME[str];
|
|
17
|
-
if (named !== undefined) {
|
|
18
|
-
return named;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Handle pX format
|
|
22
|
-
if (str.startsWith("p")) {
|
|
23
|
-
const num = parseInt(str.slice(1), 10);
|
|
24
|
-
if (num >= 0 && num <= 4) return num as Priority;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Handle numeric format
|
|
28
|
-
const num = parseInt(str, 10);
|
|
29
|
-
if (num >= 0 && num <= 4) return num as Priority;
|
|
30
|
-
|
|
31
|
-
throw new Error(`Invalid priority: ${input}. Use 0-4, p0-p4, or none/urgent/high/medium/low.`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function formatPriority(p: Priority): string {
|
|
35
|
-
return `p${p}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function formatPriorityName(p: Priority): string {
|
|
39
|
-
return PRIORITY_LABELS[p];
|
|
40
|
-
}
|
package/src/lib/root.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { join, dirname, resolve } from "path";
|
|
3
|
-
|
|
4
|
-
const TASKS_DIR = ".tasks";
|
|
5
|
-
const GIT_DIR = ".git";
|
|
6
|
-
|
|
7
|
-
// Global override for working directory (set via -C flag)
|
|
8
|
-
let workingDir: string | null = null;
|
|
9
|
-
|
|
10
|
-
export function setWorkingDir(dir: string | null): void {
|
|
11
|
-
workingDir = dir ? resolve(dir) : null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getWorkingDir(): string {
|
|
15
|
-
return workingDir ?? process.cwd();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface RootInfo {
|
|
19
|
-
root: string;
|
|
20
|
-
tasksDir: string;
|
|
21
|
-
exists: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Find the project root by walking up directories.
|
|
26
|
-
* Looks for existing .tasks/ or .git/ directory.
|
|
27
|
-
* Returns cwd if neither found.
|
|
28
|
-
*/
|
|
29
|
-
export function findRoot(): RootInfo {
|
|
30
|
-
let current = getWorkingDir();
|
|
31
|
-
|
|
32
|
-
while (true) {
|
|
33
|
-
const tasksPath = join(current, TASKS_DIR);
|
|
34
|
-
if (existsSync(tasksPath)) {
|
|
35
|
-
return { root: current, tasksDir: tasksPath, exists: true };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const gitPath = join(current, GIT_DIR);
|
|
39
|
-
if (existsSync(gitPath)) {
|
|
40
|
-
return {
|
|
41
|
-
root: current,
|
|
42
|
-
tasksDir: join(current, TASKS_DIR),
|
|
43
|
-
exists: false,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const parent = dirname(current);
|
|
48
|
-
if (parent === current) break; // Reached filesystem root
|
|
49
|
-
current = parent;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// No .git or .tasks found, use working dir
|
|
53
|
-
const wd = getWorkingDir();
|
|
54
|
-
return {
|
|
55
|
-
root: wd,
|
|
56
|
-
tasksDir: join(wd, TASKS_DIR),
|
|
57
|
-
exists: false,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get the tasks directory path, optionally requiring it to exist.
|
|
63
|
-
*/
|
|
64
|
-
export function getTasksDir(requireExists = false): string {
|
|
65
|
-
const info = findRoot();
|
|
66
|
-
if (requireExists && !info.exists) {
|
|
67
|
-
throw new TasksNotFoundError(info.root);
|
|
68
|
-
}
|
|
69
|
-
return info.tasksDir;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export class TasksNotFoundError extends Error {
|
|
73
|
-
constructor(searchedFrom: string) {
|
|
74
|
-
super(
|
|
75
|
-
`No .tasks/ directory found. Run 'tk add' to create one, or initialize with 'tk init'.\nSearched from: ${searchedFrom}`,
|
|
76
|
-
);
|
|
77
|
-
this.name = "TasksNotFoundError";
|
|
78
|
-
}
|
|
79
|
-
}
|
package/src/lib/time.test.ts
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
isTaskOverdue,
|
|
4
|
-
daysUntilDue,
|
|
5
|
-
formatRelativeTime,
|
|
6
|
-
formatDate,
|
|
7
|
-
formatLocalDate,
|
|
8
|
-
parseDueDate,
|
|
9
|
-
parseEstimate,
|
|
10
|
-
DUE_SOON_THRESHOLD,
|
|
11
|
-
} from "./time";
|
|
12
|
-
|
|
13
|
-
describe("isTaskOverdue", () => {
|
|
14
|
-
test("returns false when no due date", () => {
|
|
15
|
-
expect(isTaskOverdue(null, "open")).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("returns false for done tasks regardless of date", () => {
|
|
19
|
-
expect(isTaskOverdue("2020-01-01", "done")).toBe(false);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("returns false for active done tasks", () => {
|
|
23
|
-
expect(isTaskOverdue("2020-01-01", "done")).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("returns true for past dates (open)", () => {
|
|
27
|
-
expect(isTaskOverdue("2020-01-01", "open")).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("returns true for past dates (active)", () => {
|
|
31
|
-
expect(isTaskOverdue("2020-06-15", "active")).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("returns false for today", () => {
|
|
35
|
-
const today = new Date().toISOString().split("T")[0]!;
|
|
36
|
-
expect(isTaskOverdue(today, "open")).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("returns false for future dates", () => {
|
|
40
|
-
const future = new Date();
|
|
41
|
-
future.setDate(future.getDate() + 7);
|
|
42
|
-
const dateStr = future.toISOString().split("T")[0]!;
|
|
43
|
-
expect(isTaskOverdue(dateStr, "open")).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("returns false for invalid date string", () => {
|
|
47
|
-
expect(isTaskOverdue("not-a-date", "open")).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("daysUntilDue", () => {
|
|
52
|
-
test("returns null when no due date", () => {
|
|
53
|
-
expect(daysUntilDue(null, "open")).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("returns null when no due date (active)", () => {
|
|
57
|
-
expect(daysUntilDue(null, "active")).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("returns null for done tasks", () => {
|
|
61
|
-
const future = new Date();
|
|
62
|
-
future.setDate(future.getDate() + 3);
|
|
63
|
-
const dateStr = future.toISOString().split("T")[0]!;
|
|
64
|
-
expect(daysUntilDue(dateStr, "done")).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("returns 0 for due today (open)", () => {
|
|
68
|
-
const today = new Date().toISOString().split("T")[0]!;
|
|
69
|
-
expect(daysUntilDue(today, "open")).toBe(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("returns 0 for due today (active)", () => {
|
|
73
|
-
const today = new Date().toISOString().split("T")[0]!;
|
|
74
|
-
expect(daysUntilDue(today, "active")).toBe(0);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("returns N for due in N days", () => {
|
|
78
|
-
const future = new Date();
|
|
79
|
-
future.setDate(future.getDate() + 3);
|
|
80
|
-
const dateStr = future.toISOString().split("T")[0]!;
|
|
81
|
-
expect(daysUntilDue(dateStr, "open")).toBe(3);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("returns null for past dates (already overdue)", () => {
|
|
85
|
-
expect(daysUntilDue("2020-01-01", "open")).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("returns null for invalid date string", () => {
|
|
89
|
-
expect(daysUntilDue("not-a-date", "open")).toBeNull();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("DUE_SOON_THRESHOLD is 7", () => {
|
|
93
|
-
expect(DUE_SOON_THRESHOLD).toBe(7);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("formatRelativeTime", () => {
|
|
98
|
-
test("returns 'now' for very recent timestamps", () => {
|
|
99
|
-
const ts = new Date().toISOString();
|
|
100
|
-
expect(formatRelativeTime(ts)).toBe("now");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("returns minutes for timestamps minutes ago", () => {
|
|
104
|
-
const ts = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
105
|
-
expect(formatRelativeTime(ts)).toBe("5m");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("returns hours for timestamps hours ago", () => {
|
|
109
|
-
const ts = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
|
|
110
|
-
expect(formatRelativeTime(ts)).toBe("3h");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("returns days for timestamps days ago", () => {
|
|
114
|
-
const ts = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
115
|
-
expect(formatRelativeTime(ts)).toBe("2d");
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("formatDate", () => {
|
|
120
|
-
test("returns a non-empty string for a valid ISO timestamp", () => {
|
|
121
|
-
const result = formatDate("2024-01-15T10:30:00.000Z");
|
|
122
|
-
expect(typeof result).toBe("string");
|
|
123
|
-
expect(result.length).toBeGreaterThan(0);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("formatLocalDate", () => {
|
|
128
|
-
test("formats date as YYYY-MM-DD", () => {
|
|
129
|
-
const date = new Date(2026, 0, 15); // Jan 15 2026
|
|
130
|
-
expect(formatLocalDate(date)).toBe("2026-01-15");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("zero-pads month and day", () => {
|
|
134
|
-
const date = new Date(2026, 1, 5); // Feb 5 2026
|
|
135
|
-
expect(formatLocalDate(date)).toBe("2026-02-05");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("parseDueDate", () => {
|
|
140
|
-
test("returns undefined for undefined input", () => {
|
|
141
|
-
expect(parseDueDate(undefined)).toBeUndefined();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("returns undefined for '-' (clear sentinel)", () => {
|
|
145
|
-
expect(parseDueDate("-")).toBeUndefined();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("returns valid YYYY-MM-DD unchanged", () => {
|
|
149
|
-
expect(parseDueDate("2026-06-15")).toBe("2026-06-15");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("parses +Nd relative date", () => {
|
|
153
|
-
const result = parseDueDate("+7d");
|
|
154
|
-
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
155
|
-
const resultDate = new Date(result!);
|
|
156
|
-
const today = new Date();
|
|
157
|
-
const diffDays = Math.round((resultDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
158
|
-
expect(diffDays).toBeGreaterThanOrEqual(6);
|
|
159
|
-
expect(diffDays).toBeLessThanOrEqual(8);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("parses +Nw relative date", () => {
|
|
163
|
-
const result = parseDueDate("+2w");
|
|
164
|
-
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("parses +Nm relative date", () => {
|
|
168
|
-
const result = parseDueDate("+1m");
|
|
169
|
-
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("parses +Nh relative date", () => {
|
|
173
|
-
const result = parseDueDate("+3h");
|
|
174
|
-
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("throws for invalid relative format", () => {
|
|
178
|
-
expect(() => parseDueDate("+7x")).toThrow("Invalid relative date");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("throws for bare + with no content", () => {
|
|
182
|
-
expect(() => parseDueDate("+")).toThrow("Invalid relative date");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("throws for non-date string", () => {
|
|
186
|
-
expect(() => parseDueDate("not-a-date")).toThrow("Invalid date format");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("throws for invalid calendar date (month 13)", () => {
|
|
190
|
-
expect(() => parseDueDate("2026-13-01")).toThrow("Invalid date format");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("throws for invalid calendar date (day 32)", () => {
|
|
194
|
-
expect(() => parseDueDate("2026-01-32")).toThrow("Invalid date format");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("parseEstimate", () => {
|
|
199
|
-
test("returns undefined for undefined input", () => {
|
|
200
|
-
expect(parseEstimate(undefined)).toBeUndefined();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("parses valid integer string", () => {
|
|
204
|
-
expect(parseEstimate("42")).toBe(42);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("parses zero", () => {
|
|
208
|
-
expect(parseEstimate("0")).toBe(0);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("throws for non-numeric string", () => {
|
|
212
|
-
expect(() => parseEstimate("abc")).toThrow("Invalid estimate");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("throws for decimal", () => {
|
|
216
|
-
expect(() => parseEstimate("3.5")).toThrow("Invalid estimate");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("throws for negative number", () => {
|
|
220
|
-
expect(() => parseEstimate("-1")).toThrow("Invalid estimate");
|
|
221
|
-
});
|
|
222
|
-
});
|