@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/lib/format.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
formatTaskDetail,
|
|
7
7
|
formatJson,
|
|
8
8
|
} from "./format";
|
|
9
|
-
import type {
|
|
9
|
+
import type { TaskWithMeta, LogEntry } from "../types";
|
|
10
10
|
|
|
11
11
|
describe("shouldUseColor", () => {
|
|
12
12
|
const originalEnv = process.env.NO_COLOR;
|
|
@@ -38,7 +38,7 @@ describe("shouldUseColor", () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
describe("formatTaskRow", () => {
|
|
41
|
-
const task:
|
|
41
|
+
const task: TaskWithMeta = {
|
|
42
42
|
id: "tk-a1b2",
|
|
43
43
|
project: "tk",
|
|
44
44
|
ref: "a1b2",
|
|
@@ -57,6 +57,8 @@ describe("formatTaskRow", () => {
|
|
|
57
57
|
updated_at: new Date().toISOString(),
|
|
58
58
|
completed_at: null,
|
|
59
59
|
external: {},
|
|
60
|
+
blocked_by_incomplete: false,
|
|
61
|
+
is_overdue: false,
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
test("formats task without color", () => {
|
|
@@ -75,7 +77,7 @@ describe("formatTaskRow", () => {
|
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
test("truncates long titles to 50 chars", () => {
|
|
78
|
-
const longTask:
|
|
80
|
+
const longTask: TaskWithMeta = {
|
|
79
81
|
...task,
|
|
80
82
|
title: "A".repeat(100),
|
|
81
83
|
};
|
|
@@ -90,13 +92,13 @@ describe("formatTaskRow", () => {
|
|
|
90
92
|
expect(parts.length).toBe(4); // ID | PRIO | STATUS | TITLE
|
|
91
93
|
});
|
|
92
94
|
|
|
93
|
-
test("truncates long project names
|
|
94
|
-
const longProjectTask:
|
|
95
|
+
test("truncates long project names with ellipsis", () => {
|
|
96
|
+
const longProjectTask: TaskWithMeta = {
|
|
95
97
|
...task,
|
|
96
98
|
id: "mylongproject-a1b2",
|
|
97
99
|
};
|
|
98
100
|
const result = formatTaskRow(longProjectTask, false);
|
|
99
|
-
expect(result).toContain("
|
|
101
|
+
expect(result).toContain("mylon…-a1b2");
|
|
100
102
|
expect(result).not.toContain("mylongproject");
|
|
101
103
|
});
|
|
102
104
|
|
|
@@ -106,9 +108,10 @@ describe("formatTaskRow", () => {
|
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
test("shows overdue marker for overdue tasks", () => {
|
|
109
|
-
const overdueTask:
|
|
111
|
+
const overdueTask: TaskWithMeta = {
|
|
110
112
|
...task,
|
|
111
113
|
due_date: "2020-01-01",
|
|
114
|
+
is_overdue: true,
|
|
112
115
|
};
|
|
113
116
|
const result = formatTaskRow(overdueTask, false);
|
|
114
117
|
expect(result).toContain("[OVERDUE]");
|
|
@@ -116,9 +119,10 @@ describe("formatTaskRow", () => {
|
|
|
116
119
|
|
|
117
120
|
test("does NOT show overdue marker for tasks due today", () => {
|
|
118
121
|
const today = new Date().toISOString().split("T")[0] ?? null;
|
|
119
|
-
const todayTask:
|
|
122
|
+
const todayTask: TaskWithMeta = {
|
|
120
123
|
...task,
|
|
121
124
|
due_date: today,
|
|
125
|
+
is_overdue: false,
|
|
122
126
|
};
|
|
123
127
|
const result = formatTaskRow(todayTask, false);
|
|
124
128
|
expect(result).not.toContain("[OVERDUE]");
|
|
@@ -126,12 +130,16 @@ describe("formatTaskRow", () => {
|
|
|
126
130
|
});
|
|
127
131
|
|
|
128
132
|
describe("formatTaskList", () => {
|
|
129
|
-
test("returns message for empty list", () => {
|
|
130
|
-
expect(formatTaskList([])).toBe("No tasks found.");
|
|
133
|
+
test("returns helpful message for empty list", () => {
|
|
134
|
+
expect(formatTaskList([])).toBe("No tasks found. Run 'tk add \"title\"' to create one.");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("allows custom empty hint", () => {
|
|
138
|
+
expect(formatTaskList([], undefined, "Custom hint")).toBe("Custom hint");
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
test("includes header and divider", () => {
|
|
134
|
-
const tasks:
|
|
142
|
+
const tasks: TaskWithMeta[] = [
|
|
135
143
|
{
|
|
136
144
|
id: "tk-a1b2",
|
|
137
145
|
project: "tk",
|
|
@@ -151,6 +159,8 @@ describe("formatTaskList", () => {
|
|
|
151
159
|
updated_at: new Date().toISOString(),
|
|
152
160
|
completed_at: null,
|
|
153
161
|
external: {},
|
|
162
|
+
blocked_by_incomplete: false,
|
|
163
|
+
is_overdue: false,
|
|
154
164
|
},
|
|
155
165
|
];
|
|
156
166
|
const result = formatTaskList(tasks, false);
|
|
@@ -162,7 +172,7 @@ describe("formatTaskList", () => {
|
|
|
162
172
|
});
|
|
163
173
|
|
|
164
174
|
test("formats multiple tasks", () => {
|
|
165
|
-
const tasks:
|
|
175
|
+
const tasks: TaskWithMeta[] = [
|
|
166
176
|
{
|
|
167
177
|
id: "tk-a1b2",
|
|
168
178
|
project: "tk",
|
|
@@ -182,6 +192,8 @@ describe("formatTaskList", () => {
|
|
|
182
192
|
updated_at: new Date().toISOString(),
|
|
183
193
|
completed_at: null,
|
|
184
194
|
external: {},
|
|
195
|
+
blocked_by_incomplete: false,
|
|
196
|
+
is_overdue: false,
|
|
185
197
|
},
|
|
186
198
|
{
|
|
187
199
|
id: "tk-c3d4",
|
|
@@ -202,6 +214,8 @@ describe("formatTaskList", () => {
|
|
|
202
214
|
updated_at: new Date().toISOString(),
|
|
203
215
|
completed_at: null,
|
|
204
216
|
external: {},
|
|
217
|
+
blocked_by_incomplete: false,
|
|
218
|
+
is_overdue: false,
|
|
205
219
|
},
|
|
206
220
|
];
|
|
207
221
|
const result = formatTaskList(tasks, false);
|
package/src/lib/format.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TaskWithMeta, LogEntry } from "../types";
|
|
2
2
|
import { PRIORITY_COLORS, STATUS_COLORS, OVERDUE_COLOR, RESET } from "../types";
|
|
3
3
|
import { formatPriority } from "./priority";
|
|
4
|
+
import { formatDate, formatRelativeTime } from "./time";
|
|
4
5
|
|
|
5
6
|
// Message colors
|
|
6
7
|
const GREEN = "\x1b[32m";
|
|
@@ -42,56 +43,59 @@ export function dim(msg: string): string {
|
|
|
42
43
|
return shouldUseColor() ? `${DIM}${msg}${RESET}` : msg;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
function truncate(text: string, maxLen: number): string {
|
|
47
|
+
return text.length <= maxLen ? text : text.slice(0, maxLen - 1) + "…";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatId(id: string, maxLen = 11): string {
|
|
51
|
+
// Truncate project prefix to fit maxLen, keep full 4-char ref
|
|
52
|
+
// "myproject-a1b2" -> "mypr…-a1b2"
|
|
48
53
|
const dash = id.lastIndexOf("-");
|
|
49
|
-
if (dash === -1) return id
|
|
54
|
+
if (dash === -1) return truncate(id, maxLen);
|
|
50
55
|
const project = id.slice(0, dash);
|
|
51
56
|
const ref = id.slice(dash + 1);
|
|
52
|
-
const
|
|
53
|
-
return
|
|
57
|
+
const maxProjectLen = maxLen - ref.length - 1; // -1 for dash
|
|
58
|
+
return project.length > maxProjectLen
|
|
59
|
+
? `${truncate(project, maxProjectLen)}-${ref}`
|
|
60
|
+
: `${project}-${ref}`;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
export function formatTaskRow(task:
|
|
63
|
+
export function formatTaskRow(task: TaskWithMeta, useColor?: boolean): string {
|
|
57
64
|
const color = useColor ?? shouldUseColor();
|
|
58
65
|
const id = formatId(task.id).padEnd(11);
|
|
59
66
|
const priority = formatPriority(task.priority).padEnd(4);
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const today = new Date();
|
|
67
|
-
today.setHours(0, 0, 0, 0);
|
|
68
|
-
// Parse YYYY-MM-DD as local date (not UTC)
|
|
69
|
-
const parts = task.due_date.split("-").map(Number);
|
|
70
|
-
const year = parts[0];
|
|
71
|
-
const month = parts[1];
|
|
72
|
-
const day = parts[2];
|
|
73
|
-
if (year && month && day) {
|
|
74
|
-
const dueDate = new Date(year, month - 1, day);
|
|
75
|
-
isOverdue = dueDate < today;
|
|
76
|
-
}
|
|
67
|
+
const title = truncate(task.title, 50);
|
|
68
|
+
const isOverdue = task.is_overdue;
|
|
69
|
+
|
|
70
|
+
let statusText: string = task.status;
|
|
71
|
+
if (task.status === "done" && task.completed_at) {
|
|
72
|
+
statusText = `done ${formatRelativeTime(task.completed_at)}`;
|
|
77
73
|
}
|
|
74
|
+
const status = statusText.padEnd(12);
|
|
78
75
|
|
|
79
76
|
if (color) {
|
|
80
77
|
const pc = PRIORITY_COLORS[task.priority];
|
|
81
78
|
const sc = isOverdue ? OVERDUE_COLOR : STATUS_COLORS[task.status];
|
|
82
|
-
|
|
79
|
+
const tc = task.status === "done" ? DIM : "";
|
|
80
|
+
return `${id} | ${pc}${priority}${RESET} | ${sc}${status}${RESET} | ${tc}${title}${RESET}`;
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
const overdueMarker = isOverdue ? " [OVERDUE]" : "";
|
|
86
84
|
return `${id} | ${priority} | ${status} | ${title}${overdueMarker}`;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
export function formatTaskList(
|
|
90
|
-
|
|
87
|
+
export function formatTaskList(
|
|
88
|
+
tasks: TaskWithMeta[],
|
|
89
|
+
useColor?: boolean,
|
|
90
|
+
emptyHint?: string,
|
|
91
|
+
): string {
|
|
92
|
+
if (tasks.length === 0) {
|
|
93
|
+
return emptyHint ?? "No tasks found. Run 'tk add \"title\"' to create one.";
|
|
94
|
+
}
|
|
91
95
|
const color = useColor ?? shouldUseColor();
|
|
92
96
|
|
|
93
|
-
const header = "ID | PRIO | STATUS
|
|
94
|
-
const divider = "-".repeat(
|
|
97
|
+
const header = "ID | PRIO | STATUS | TITLE";
|
|
98
|
+
const divider = "-".repeat(65);
|
|
95
99
|
const rows = tasks.map((t) => formatTaskRow(t, color));
|
|
96
100
|
|
|
97
101
|
return [header, divider, ...rows].join("\n");
|
|
@@ -159,10 +163,6 @@ export function formatTaskDetail(task: TaskWithMeta, logs: LogEntry[], useColor?
|
|
|
159
163
|
return lines.join("\n");
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
function formatDate(timestamp: string): string {
|
|
163
|
-
return new Date(timestamp).toLocaleString();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
166
|
export function formatJson(data: unknown): string {
|
|
167
167
|
return JSON.stringify(data, null, 2);
|
|
168
168
|
}
|
package/src/lib/help.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
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/time.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
}
|