@nijaru/tk 0.0.4 → 0.1.1
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 -976
- package/src/cli.ts +0 -695
- package/src/db/storage.ts +0 -1014
- package/src/lib/completions.ts +0 -425
- package/src/lib/format.test.ts +0 -361
- package/src/lib/format.ts +0 -188
- package/src/lib/help.ts +0 -249
- 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.ts +0 -115
- package/src/types.ts +0 -130
package/src/lib/help.ts
DELETED
|
@@ -1,249 +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
|
-
rm, remove Delete task
|
|
22
|
-
clean Remove old done tasks
|
|
23
|
-
check Check for data issues
|
|
24
|
-
config Show/set configuration
|
|
25
|
-
completions Output shell completions
|
|
26
|
-
|
|
27
|
-
GLOBAL OPTIONS:
|
|
28
|
-
-C <dir> Run in different directory
|
|
29
|
-
--json Output as JSON
|
|
30
|
-
-h, --help Show help
|
|
31
|
-
-V Show version
|
|
32
|
-
|
|
33
|
-
Run 'tk <command> --help' for command-specific options.
|
|
34
|
-
`;
|
|
35
|
-
|
|
36
|
-
export const COMMAND_HELP: Record<string, string> = {
|
|
37
|
-
init: `tk init - Initialize .tasks/ directory
|
|
38
|
-
|
|
39
|
-
USAGE:
|
|
40
|
-
tk init [options]
|
|
41
|
-
|
|
42
|
-
OPTIONS:
|
|
43
|
-
-P, --project <name> Set default project name
|
|
44
|
-
`,
|
|
45
|
-
add: `tk add - Create a new task
|
|
46
|
-
|
|
47
|
-
USAGE:
|
|
48
|
-
tk add <title> [options]
|
|
49
|
-
|
|
50
|
-
OPTIONS:
|
|
51
|
-
-p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
|
|
52
|
-
-P, --project <name> Project prefix for ID
|
|
53
|
-
-d, --description <text> Description
|
|
54
|
-
-l, --labels <csv> Labels (comma-separated)
|
|
55
|
-
-A, --assignees <csv> Assignees (comma-separated, @me for git user)
|
|
56
|
-
--parent <id> Parent task ID
|
|
57
|
-
--estimate <n> Estimate (user-defined units)
|
|
58
|
-
--due <date> Due date (YYYY-MM-DD or +Nh/+Nd/+Nw/+Nm)
|
|
59
|
-
|
|
60
|
-
EXAMPLES:
|
|
61
|
-
tk add "Fix login bug" -p 1
|
|
62
|
-
tk add "New feature" -P api -l bug,urgent
|
|
63
|
-
tk add "Sprint task" --due +7d
|
|
64
|
-
`,
|
|
65
|
-
ls: `tk ls - List tasks
|
|
66
|
-
|
|
67
|
-
USAGE:
|
|
68
|
-
tk ls [options]
|
|
69
|
-
|
|
70
|
-
OPTIONS:
|
|
71
|
-
-s, --status <status> Filter by status (open, active, done)
|
|
72
|
-
-p, --priority <0-4> Filter by priority
|
|
73
|
-
-P, --project <name> Filter by project
|
|
74
|
-
-l, --label <label> Filter by label
|
|
75
|
-
--assignee <name> Filter by assignee
|
|
76
|
-
--parent <id> Filter by parent
|
|
77
|
-
--roots Show only root tasks (no parent)
|
|
78
|
-
--overdue Show only overdue tasks
|
|
79
|
-
-n, --limit <n> Limit results (default: 20)
|
|
80
|
-
-a, --all Show all (no limit)
|
|
81
|
-
|
|
82
|
-
EXAMPLES:
|
|
83
|
-
tk ls -s open -p 1 # Urgent open tasks
|
|
84
|
-
tk ls --overdue # Overdue tasks
|
|
85
|
-
tk ls -P api --roots # Root tasks in api project
|
|
86
|
-
`,
|
|
87
|
-
list: `tk list - List tasks (alias for 'ls')
|
|
88
|
-
|
|
89
|
-
Run 'tk ls --help' for options.
|
|
90
|
-
`,
|
|
91
|
-
ready: `tk ready - List ready tasks (active/open + unblocked)
|
|
92
|
-
|
|
93
|
-
USAGE:
|
|
94
|
-
tk ready
|
|
95
|
-
|
|
96
|
-
Shows active or open tasks that are not blocked by any incomplete task.
|
|
97
|
-
`,
|
|
98
|
-
show: `tk show - Show task details
|
|
99
|
-
|
|
100
|
-
USAGE:
|
|
101
|
-
tk show <id>
|
|
102
|
-
|
|
103
|
-
EXAMPLES:
|
|
104
|
-
tk show tk-1
|
|
105
|
-
tk show 1 # If unambiguous
|
|
106
|
-
`,
|
|
107
|
-
start: `tk start - Start working on a task
|
|
108
|
-
|
|
109
|
-
USAGE:
|
|
110
|
-
tk start <id>
|
|
111
|
-
|
|
112
|
-
Changes status from open to active.
|
|
113
|
-
`,
|
|
114
|
-
done: `tk done - Complete a task
|
|
115
|
-
|
|
116
|
-
USAGE:
|
|
117
|
-
tk done <id>
|
|
118
|
-
|
|
119
|
-
Changes status to done.
|
|
120
|
-
`,
|
|
121
|
-
reopen: `tk reopen - Reopen a task
|
|
122
|
-
|
|
123
|
-
USAGE:
|
|
124
|
-
tk reopen <id>
|
|
125
|
-
|
|
126
|
-
Changes status back to open.
|
|
127
|
-
`,
|
|
128
|
-
edit: `tk edit - Edit a task
|
|
129
|
-
|
|
130
|
-
USAGE:
|
|
131
|
-
tk edit <id> [options]
|
|
132
|
-
|
|
133
|
-
OPTIONS:
|
|
134
|
-
-t, --title <text> New title
|
|
135
|
-
-d, --description <text> New description
|
|
136
|
-
-p, --priority <0-4> New priority
|
|
137
|
-
-l, --labels <csv> Replace labels (use +tag/-tag to add/remove)
|
|
138
|
-
-A, --assignees <csv> Replace assignees
|
|
139
|
-
--parent <id> Set parent (use - to clear)
|
|
140
|
-
--estimate <n> Set estimate (use - to clear)
|
|
141
|
-
--due <date> Set due date (use - to clear)
|
|
142
|
-
|
|
143
|
-
EXAMPLES:
|
|
144
|
-
tk edit tk-1 -t "New title"
|
|
145
|
-
tk edit tk-1 -l +urgent # Add label
|
|
146
|
-
tk edit tk-1 --due - # Clear due date
|
|
147
|
-
`,
|
|
148
|
-
log: `tk log - Add a log entry to a task
|
|
149
|
-
|
|
150
|
-
USAGE:
|
|
151
|
-
tk log <id> "<message>"
|
|
152
|
-
|
|
153
|
-
Message must be quoted.
|
|
154
|
-
|
|
155
|
-
EXAMPLES:
|
|
156
|
-
tk log tk-1 "Started implementation"
|
|
157
|
-
tk log tk-1 "Blocked on API changes"
|
|
158
|
-
`,
|
|
159
|
-
block: `tk block - Add a blocker dependency
|
|
160
|
-
|
|
161
|
-
USAGE:
|
|
162
|
-
tk block <task> <blocker>
|
|
163
|
-
|
|
164
|
-
The first task becomes blocked by the second.
|
|
165
|
-
|
|
166
|
-
EXAMPLES:
|
|
167
|
-
tk block tk-2 tk-1 # tk-2 is blocked by tk-1
|
|
168
|
-
`,
|
|
169
|
-
unblock: `tk unblock - Remove a blocker dependency
|
|
170
|
-
|
|
171
|
-
USAGE:
|
|
172
|
-
tk unblock <task> <blocker>
|
|
173
|
-
|
|
174
|
-
EXAMPLES:
|
|
175
|
-
tk unblock tk-2 tk-1
|
|
176
|
-
`,
|
|
177
|
-
rm: `tk rm - Delete a task
|
|
178
|
-
|
|
179
|
-
USAGE:
|
|
180
|
-
tk rm <id>
|
|
181
|
-
|
|
182
|
-
EXAMPLES:
|
|
183
|
-
tk rm tk-1
|
|
184
|
-
`,
|
|
185
|
-
remove: `tk remove - Delete a task (alias for 'rm')
|
|
186
|
-
|
|
187
|
-
USAGE:
|
|
188
|
-
tk remove <id>
|
|
189
|
-
`,
|
|
190
|
-
clean: `tk clean - Remove old completed tasks
|
|
191
|
-
|
|
192
|
-
USAGE:
|
|
193
|
-
tk clean [options]
|
|
194
|
-
|
|
195
|
-
OPTIONS:
|
|
196
|
-
--older-than <days> Age threshold in days (default: from config, 14)
|
|
197
|
-
-f, --force Remove all done tasks (ignores age and disabled state)
|
|
198
|
-
|
|
199
|
-
EXAMPLES:
|
|
200
|
-
tk clean # Remove done tasks older than config.clean_after days
|
|
201
|
-
tk clean --older-than 30 # Remove done tasks older than 30 days
|
|
202
|
-
tk clean --force # Remove all done tasks regardless of age
|
|
203
|
-
`,
|
|
204
|
-
check: `tk check - Check for data issues
|
|
205
|
-
|
|
206
|
-
USAGE:
|
|
207
|
-
tk check
|
|
208
|
-
|
|
209
|
-
Scans all tasks for issues. Auto-fixable issues (orphaned references, ID
|
|
210
|
-
mismatches) are fixed automatically. Unfixable issues (corrupted JSON) are
|
|
211
|
-
reported for manual intervention.
|
|
212
|
-
|
|
213
|
-
Note: Auto-fixing also happens during normal task operations (show, done,
|
|
214
|
-
edit, etc.) - this command is for bulk cleanup or diagnostics.
|
|
215
|
-
|
|
216
|
-
EXAMPLES:
|
|
217
|
-
tk check # Scan and fix all tasks
|
|
218
|
-
`,
|
|
219
|
-
config: `tk config - Show or set configuration
|
|
220
|
-
|
|
221
|
-
USAGE:
|
|
222
|
-
tk config Show all config
|
|
223
|
-
tk config project Show default project
|
|
224
|
-
tk config project <name> Set default project
|
|
225
|
-
tk config project <new> --rename <old> Rename project (old-* → new-*)
|
|
226
|
-
tk config alias List aliases
|
|
227
|
-
tk config alias <name> <path> Add alias
|
|
228
|
-
tk config alias --rm <name> Remove alias
|
|
229
|
-
|
|
230
|
-
EXAMPLES:
|
|
231
|
-
tk config project api
|
|
232
|
-
tk config project lsmvec --rename cloudlsmvec
|
|
233
|
-
tk config alias web src/web
|
|
234
|
-
`,
|
|
235
|
-
completions: `tk completions - Output shell completions
|
|
236
|
-
|
|
237
|
-
USAGE:
|
|
238
|
-
tk completions <shell>
|
|
239
|
-
|
|
240
|
-
SHELLS:
|
|
241
|
-
bash Bash completion script
|
|
242
|
-
zsh Zsh completion script
|
|
243
|
-
fish Fish completion script
|
|
244
|
-
|
|
245
|
-
EXAMPLES:
|
|
246
|
-
eval "$(tk completions bash)" # Add to ~/.bashrc
|
|
247
|
-
eval "$(tk completions zsh)" # Add to ~/.zshrc
|
|
248
|
-
`,
|
|
249
|
-
};
|
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.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import type { Status } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Determines if a task is overdue.
|
|
5
|
-
* Done tasks are never overdue.
|
|
6
|
-
*/
|
|
7
|
-
export function isTaskOverdue(dueDate: string | null, status: Status): boolean {
|
|
8
|
-
if (!dueDate || status === "done") return false;
|
|
9
|
-
const today = new Date();
|
|
10
|
-
today.setHours(0, 0, 0, 0);
|
|
11
|
-
const parts = dueDate.split("-").map(Number);
|
|
12
|
-
const year = parts[0];
|
|
13
|
-
const month = parts[1];
|
|
14
|
-
const day = parts[2];
|
|
15
|
-
if (!year || !month || !day) return false;
|
|
16
|
-
const due = new Date(year, month - 1, day);
|
|
17
|
-
return due < today;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Format a timestamp into a human-readable relative time (e.g., "2d", "5h").
|
|
22
|
-
*/
|
|
23
|
-
export function formatRelativeTime(timestamp: string): string {
|
|
24
|
-
const now = new Date();
|
|
25
|
-
const date = new Date(timestamp);
|
|
26
|
-
const diffMs = now.getTime() - date.getTime();
|
|
27
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
28
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
29
|
-
const diffHour = Math.floor(diffMin / 60);
|
|
30
|
-
const diffDay = Math.floor(diffHour / 24);
|
|
31
|
-
|
|
32
|
-
if (diffDay > 0) return `${diffDay}d`;
|
|
33
|
-
if (diffHour > 0) return `${diffHour}h`;
|
|
34
|
-
if (diffMin > 0) return `${diffMin}m`;
|
|
35
|
-
return "now";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Format a timestamp into a localized date string.
|
|
40
|
-
*/
|
|
41
|
-
export function formatDate(timestamp: string): string {
|
|
42
|
-
return new Date(timestamp).toLocaleString();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Format a Date object into YYYY-MM-DD.
|
|
47
|
-
*/
|
|
48
|
-
export function formatLocalDate(date: Date): string {
|
|
49
|
-
const year = date.getFullYear();
|
|
50
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
51
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
52
|
-
return `${year}-${month}-${day}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Parse a due date input string. Supports YYYY-MM-DD and relative formats like +7d.
|
|
57
|
-
*/
|
|
58
|
-
export function parseDueDate(input: string | undefined): string | undefined {
|
|
59
|
-
if (!input) return undefined;
|
|
60
|
-
if (input === "-") return undefined;
|
|
61
|
-
|
|
62
|
-
// Handle relative dates like +7d
|
|
63
|
-
if (input.startsWith("+")) {
|
|
64
|
-
const match = input.match(/^\+(\d+)([dwmh])$/);
|
|
65
|
-
if (match && match[1] && match[2]) {
|
|
66
|
-
const num = match[1];
|
|
67
|
-
const unit = match[2] as "h" | "d" | "w" | "m";
|
|
68
|
-
const n = Number(num);
|
|
69
|
-
const now = new Date();
|
|
70
|
-
now.setSeconds(0, 0); // Normalize to start of minute for predictability
|
|
71
|
-
switch (unit) {
|
|
72
|
-
case "h":
|
|
73
|
-
now.setHours(now.getHours() + n);
|
|
74
|
-
break;
|
|
75
|
-
case "d":
|
|
76
|
-
now.setDate(now.getDate() + n);
|
|
77
|
-
break;
|
|
78
|
-
case "w":
|
|
79
|
-
now.setDate(now.getDate() + n * 7);
|
|
80
|
-
break;
|
|
81
|
-
case "m":
|
|
82
|
-
now.setMonth(now.getMonth() + n);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
return formatLocalDate(now);
|
|
86
|
-
}
|
|
87
|
-
throw new Error(`Invalid relative date: ${input}. Use format like +7d, +2w, +1m`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Validate YYYY-MM-DD format
|
|
91
|
-
const dateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
92
|
-
if (dateMatch) {
|
|
93
|
-
const [, year, month, day] = dateMatch;
|
|
94
|
-
const y = parseInt(year!, 10);
|
|
95
|
-
const m = parseInt(month!, 10);
|
|
96
|
-
const d = parseInt(day!, 10);
|
|
97
|
-
const date = new Date(y, m - 1, d);
|
|
98
|
-
if (date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d) {
|
|
99
|
-
return input;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw new Error(`Invalid date format: ${input}. Use YYYY-MM-DD or relative like +7d`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Parse an estimate string (number of minutes/hours/etc. depending on convention).
|
|
108
|
-
*/
|
|
109
|
-
export function parseEstimate(input: string | undefined): number | undefined {
|
|
110
|
-
if (!input) return undefined;
|
|
111
|
-
if (!/^\d+$/.test(input)) {
|
|
112
|
-
throw new Error(`Invalid estimate: ${input}. Must be a non-negative number.`);
|
|
113
|
-
}
|
|
114
|
-
return Number(input);
|
|
115
|
-
}
|