@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/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";