@nijaru/tk 0.0.5 → 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 -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/time.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { Status } from "../types";
|
|
2
|
-
|
|
3
|
-
export const DUE_SOON_THRESHOLD = 7;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Determines if a task is overdue.
|
|
7
|
-
* Done tasks are never overdue.
|
|
8
|
-
*/
|
|
9
|
-
export function isTaskOverdue(dueDate: string | null, status: Status): boolean {
|
|
10
|
-
if (!dueDate || status === "done") return false;
|
|
11
|
-
const today = new Date();
|
|
12
|
-
today.setHours(0, 0, 0, 0);
|
|
13
|
-
const parts = dueDate.split("-").map(Number);
|
|
14
|
-
const year = parts[0];
|
|
15
|
-
const month = parts[1];
|
|
16
|
-
const day = parts[2];
|
|
17
|
-
if (year === undefined || !month || !day) return false;
|
|
18
|
-
const due = new Date(year, month - 1, day);
|
|
19
|
-
return due < today;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Returns the number of days until a task is due (0 = today), or null if:
|
|
24
|
-
* - No due date
|
|
25
|
-
* - Task is done
|
|
26
|
-
* - Task is already overdue
|
|
27
|
-
*/
|
|
28
|
-
export function daysUntilDue(dueDate: string | null, status: Status): number | null {
|
|
29
|
-
if (!dueDate || status === "done") return null;
|
|
30
|
-
const today = new Date();
|
|
31
|
-
today.setHours(0, 0, 0, 0);
|
|
32
|
-
const parts = dueDate.split("-").map(Number);
|
|
33
|
-
const year = parts[0];
|
|
34
|
-
const month = parts[1];
|
|
35
|
-
const day = parts[2];
|
|
36
|
-
if (year === undefined || !month || !day) return null;
|
|
37
|
-
const due = new Date(year, month - 1, day);
|
|
38
|
-
const diffMs = due.getTime() - today.getTime();
|
|
39
|
-
if (diffMs < 0) return null; // already overdue
|
|
40
|
-
return Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Format a timestamp into a human-readable relative time (e.g., "2d", "5h").
|
|
45
|
-
*/
|
|
46
|
-
export function formatRelativeTime(timestamp: string): string {
|
|
47
|
-
const now = new Date();
|
|
48
|
-
const date = new Date(timestamp);
|
|
49
|
-
const diffMs = now.getTime() - date.getTime();
|
|
50
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
51
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
52
|
-
const diffHour = Math.floor(diffMin / 60);
|
|
53
|
-
const diffDay = Math.floor(diffHour / 24);
|
|
54
|
-
|
|
55
|
-
if (diffDay > 0) return `${diffDay}d`;
|
|
56
|
-
if (diffHour > 0) return `${diffHour}h`;
|
|
57
|
-
if (diffMin > 0) return `${diffMin}m`;
|
|
58
|
-
return "now";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Format a timestamp into a localized date string.
|
|
63
|
-
*/
|
|
64
|
-
export function formatDate(timestamp: string): string {
|
|
65
|
-
return new Date(timestamp).toLocaleString();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Format a Date object into YYYY-MM-DD.
|
|
70
|
-
*/
|
|
71
|
-
export function formatLocalDate(date: Date): string {
|
|
72
|
-
const year = date.getFullYear();
|
|
73
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
74
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
75
|
-
return `${year}-${month}-${day}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Parse a due date input string. Supports YYYY-MM-DD and relative formats like +7d.
|
|
80
|
-
*/
|
|
81
|
-
export function parseDueDate(input: string | undefined): string | undefined {
|
|
82
|
-
if (!input) return undefined;
|
|
83
|
-
if (input === "-") return undefined;
|
|
84
|
-
|
|
85
|
-
// Handle relative dates like +7d
|
|
86
|
-
if (input.startsWith("+")) {
|
|
87
|
-
const match = input.match(/^\+(\d+)([dwmh])$/);
|
|
88
|
-
if (match && match[1] && match[2]) {
|
|
89
|
-
const num = match[1];
|
|
90
|
-
const unit = match[2] as "h" | "d" | "w" | "m";
|
|
91
|
-
const n = Number(num);
|
|
92
|
-
const now = new Date();
|
|
93
|
-
now.setSeconds(0, 0); // Normalize to start of minute for predictability
|
|
94
|
-
switch (unit) {
|
|
95
|
-
case "h":
|
|
96
|
-
now.setHours(now.getHours() + n);
|
|
97
|
-
break;
|
|
98
|
-
case "d":
|
|
99
|
-
now.setDate(now.getDate() + n);
|
|
100
|
-
break;
|
|
101
|
-
case "w":
|
|
102
|
-
now.setDate(now.getDate() + n * 7);
|
|
103
|
-
break;
|
|
104
|
-
case "m":
|
|
105
|
-
now.setMonth(now.getMonth() + n);
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
return formatLocalDate(now);
|
|
109
|
-
}
|
|
110
|
-
throw new Error(`Invalid relative date: ${input}. Use format like +7d, +2w, +1m`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Validate YYYY-MM-DD format
|
|
114
|
-
const dateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
115
|
-
if (dateMatch) {
|
|
116
|
-
const [, year, month, day] = dateMatch;
|
|
117
|
-
const y = parseInt(year!, 10);
|
|
118
|
-
const m = parseInt(month!, 10);
|
|
119
|
-
const d = parseInt(day!, 10);
|
|
120
|
-
const date = new Date(y, m - 1, d);
|
|
121
|
-
if (date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d) {
|
|
122
|
-
return input;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw new Error(`Invalid date format: ${input}. Use YYYY-MM-DD or relative like +7d`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Parse an estimate string (number of minutes/hours/etc. depending on convention).
|
|
131
|
-
*/
|
|
132
|
-
export function parseEstimate(input: string | undefined): number | undefined {
|
|
133
|
-
if (!input) return undefined;
|
|
134
|
-
if (!/^\d+$/.test(input)) {
|
|
135
|
-
throw new Error(`Invalid estimate: ${input}. Must be a non-negative number.`);
|
|
136
|
-
}
|
|
137
|
-
return Number(input);
|
|
138
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
export type Status = "open" | "active" | "done";
|
|
2
|
-
export type Priority = 0 | 1 | 2 | 3 | 4;
|
|
3
|
-
|
|
4
|
-
export interface LogEntry {
|
|
5
|
-
ts: string; // ISO 8601 timestamp
|
|
6
|
-
msg: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ExternalLink {
|
|
10
|
-
number?: number;
|
|
11
|
-
repo?: string;
|
|
12
|
-
synced_at?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Task {
|
|
16
|
-
project: string;
|
|
17
|
-
ref: string; // random 4-char alphanumeric
|
|
18
|
-
title: string;
|
|
19
|
-
description: string | null;
|
|
20
|
-
status: Status;
|
|
21
|
-
priority: Priority;
|
|
22
|
-
labels: string[];
|
|
23
|
-
assignees: string[];
|
|
24
|
-
parent: string | null; // full ID like "api-a7b3"
|
|
25
|
-
blocked_by: string[];
|
|
26
|
-
estimate: number | null;
|
|
27
|
-
due_date: string | null; // ISO date string "2026-01-15"
|
|
28
|
-
logs: LogEntry[];
|
|
29
|
-
created_at: string; // ISO 8601
|
|
30
|
-
updated_at: string; // ISO 8601
|
|
31
|
-
completed_at: string | null; // ISO 8601
|
|
32
|
-
external: {
|
|
33
|
-
github?: ExternalLink;
|
|
34
|
-
linear?: ExternalLink;
|
|
35
|
-
jira?: ExternalLink;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Computed ID from project + ref
|
|
40
|
-
export function taskId(task: { project: string; ref: string }): string {
|
|
41
|
-
return `${task.project}-${task.ref}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Parse "api-a7b3" into {project: "api", ref: "a7b3"}
|
|
45
|
-
export function parseId(id: string): { project: string; ref: string } | null {
|
|
46
|
-
const match = id.match(/^([a-z][a-z0-9]*)-([a-z0-9]+)$/);
|
|
47
|
-
if (!match || !match[1] || !match[2]) return null;
|
|
48
|
-
return { project: match[1], ref: match[2] };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Generate random 4-char ref (a-z0-9)
|
|
52
|
-
export function generateRef(): string {
|
|
53
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; // 36 chars
|
|
54
|
-
const charsetLen = chars.length; // 36
|
|
55
|
-
const maxValid = charsetLen * Math.floor(256 / charsetLen); // 252: reject >= maxValid to avoid modulo bias
|
|
56
|
-
const result: string[] = [];
|
|
57
|
-
|
|
58
|
-
while (result.length < 4) {
|
|
59
|
-
const bytes = new Uint8Array(4);
|
|
60
|
-
crypto.getRandomValues(bytes);
|
|
61
|
-
for (const b of bytes) {
|
|
62
|
-
if (b < maxValid && result.length < 4) {
|
|
63
|
-
result.push(chars[b % charsetLen]!);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return result.join("");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface TaskWithMeta extends Task {
|
|
71
|
-
id: string; // computed from project-ref
|
|
72
|
-
blocked_by_incomplete: boolean;
|
|
73
|
-
is_overdue: boolean;
|
|
74
|
-
days_until_due: number | null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface Config {
|
|
78
|
-
version: number;
|
|
79
|
-
project: string; // default project
|
|
80
|
-
defaults: {
|
|
81
|
-
priority: Priority;
|
|
82
|
-
labels: string[];
|
|
83
|
-
assignees: string[];
|
|
84
|
-
};
|
|
85
|
-
clean_after: number | false; // days to keep done tasks, or false to disable
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export const DEFAULT_CONFIG: Config = {
|
|
89
|
-
version: 1,
|
|
90
|
-
project: "tk",
|
|
91
|
-
defaults: {
|
|
92
|
-
priority: 3,
|
|
93
|
-
labels: [],
|
|
94
|
-
assignees: [],
|
|
95
|
-
},
|
|
96
|
-
clean_after: 14,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const PRIORITY_LABELS: Record<Priority, string> = {
|
|
100
|
-
0: "none",
|
|
101
|
-
1: "urgent",
|
|
102
|
-
2: "high",
|
|
103
|
-
3: "medium",
|
|
104
|
-
4: "low",
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export const PRIORITY_FROM_NAME: Record<string, Priority> = {
|
|
108
|
-
none: 0,
|
|
109
|
-
urgent: 1,
|
|
110
|
-
high: 2,
|
|
111
|
-
medium: 3,
|
|
112
|
-
low: 4,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export const STATUS_COLORS: Record<Status, string> = {
|
|
116
|
-
open: "\x1b[33m", // yellow
|
|
117
|
-
active: "\x1b[36m", // cyan
|
|
118
|
-
done: "\x1b[32m", // green
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export const PRIORITY_COLORS: Record<Priority, string> = {
|
|
122
|
-
0: "\x1b[90m", // gray (none)
|
|
123
|
-
1: "\x1b[31m", // red (urgent)
|
|
124
|
-
2: "\x1b[33m", // yellow (high)
|
|
125
|
-
3: "\x1b[0m", // default (medium)
|
|
126
|
-
4: "\x1b[90m", // gray (low)
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export const OVERDUE_COLOR = "\x1b[31m"; // red
|
|
130
|
-
export const RESET = "\x1b[0m";
|