@nijaru/tk 0.0.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/LICENSE +21 -0
- package/README.md +229 -0
- package/package.json +47 -0
- package/src/cli.test.ts +636 -0
- package/src/cli.ts +871 -0
- package/src/db/storage.ts +777 -0
- package/src/lib/completions.ts +418 -0
- package/src/lib/format.test.ts +347 -0
- package/src/lib/format.ts +162 -0
- package/src/lib/priority.test.ts +105 -0
- package/src/lib/priority.ts +40 -0
- package/src/lib/root.ts +79 -0
- package/src/types.ts +130 -0
package/src/lib/root.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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/types.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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 maxValid = 252; // 36 * 7 - reject >= 252 to avoid modulo bias
|
|
55
|
+
const result: string[] = [];
|
|
56
|
+
|
|
57
|
+
while (result.length < 4) {
|
|
58
|
+
const bytes = new Uint8Array(4);
|
|
59
|
+
crypto.getRandomValues(bytes);
|
|
60
|
+
for (const b of bytes) {
|
|
61
|
+
if (b < maxValid && result.length < 4) {
|
|
62
|
+
result.push(chars[b % 36]!);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result.join("");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TaskWithMeta extends Task {
|
|
70
|
+
id: string; // computed from project-ref
|
|
71
|
+
blocked_by_incomplete: boolean;
|
|
72
|
+
is_overdue: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Config {
|
|
76
|
+
version: number;
|
|
77
|
+
project: string; // default project
|
|
78
|
+
defaults: {
|
|
79
|
+
priority: Priority;
|
|
80
|
+
labels: string[];
|
|
81
|
+
assignees: string[];
|
|
82
|
+
};
|
|
83
|
+
aliases: Record<string, string>; // alias -> path for auto-project detection
|
|
84
|
+
auto_project: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const DEFAULT_CONFIG: Config = {
|
|
88
|
+
version: 1,
|
|
89
|
+
project: "tk",
|
|
90
|
+
defaults: {
|
|
91
|
+
priority: 3,
|
|
92
|
+
labels: [],
|
|
93
|
+
assignees: [],
|
|
94
|
+
},
|
|
95
|
+
aliases: {},
|
|
96
|
+
auto_project: false,
|
|
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";
|