@bretwardjames/tw-bridge 0.2.0 → 0.4.0
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/dist/cli.js +582 -50
- package/dist/extensions/bridge.js +219 -0
- package/dist/hooks/on-modify.js +565 -26
- package/package.json +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/extensions/bridge.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
function parseInput() {
|
|
7
|
+
const raw = fs.readFileSync("/dev/stdin", "utf-8");
|
|
8
|
+
const lines = raw.split("\n");
|
|
9
|
+
let i = 0;
|
|
10
|
+
while (i < lines.length && lines[i].trim() !== "") {
|
|
11
|
+
i++;
|
|
12
|
+
}
|
|
13
|
+
i++;
|
|
14
|
+
const json = lines.slice(i).join("\n").trim();
|
|
15
|
+
if (!json) return [];
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(json);
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseTimewDate(s) {
|
|
23
|
+
return new Date(
|
|
24
|
+
s.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, "$1-$2-$3T$4:$5:$6Z")
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function intervalDuration(iv) {
|
|
28
|
+
const start = parseTimewDate(iv.start).getTime();
|
|
29
|
+
const end = iv.end ? parseTimewDate(iv.end).getTime() : Date.now();
|
|
30
|
+
return end - start;
|
|
31
|
+
}
|
|
32
|
+
function formatDuration(ms) {
|
|
33
|
+
const totalSecs = Math.round(ms / 1e3);
|
|
34
|
+
const h = Math.floor(totalSecs / 3600);
|
|
35
|
+
const m = Math.floor(totalSecs % 3600 / 60);
|
|
36
|
+
const s = totalSecs % 60;
|
|
37
|
+
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
38
|
+
}
|
|
39
|
+
function formatTime(d) {
|
|
40
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
41
|
+
}
|
|
42
|
+
function lookupDescriptions(taskIds) {
|
|
43
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
44
|
+
if (taskIds.length === 0) return descriptions;
|
|
45
|
+
const filter = taskIds.map((id) => `backend_id:${id}`).join(" or ");
|
|
46
|
+
const result = spawnSync("task", ["rc.verbose=nothing", filter, "export"], {
|
|
47
|
+
encoding: "utf-8",
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
49
|
+
});
|
|
50
|
+
if (result.status !== 0 || !result.stdout?.trim()) return descriptions;
|
|
51
|
+
try {
|
|
52
|
+
const tasks = JSON.parse(result.stdout);
|
|
53
|
+
for (const t of tasks) {
|
|
54
|
+
if (t.backend_id) {
|
|
55
|
+
descriptions.set(t.backend_id, t.description ?? "");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
return descriptions;
|
|
61
|
+
}
|
|
62
|
+
function computeTaskTime(intervals) {
|
|
63
|
+
const taskMap = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const iv of intervals) {
|
|
65
|
+
const duration = intervalDuration(iv);
|
|
66
|
+
const idTag = iv.tags?.find((t) => t.startsWith("#"));
|
|
67
|
+
const id = idTag?.slice(1) ?? "";
|
|
68
|
+
const key = id || iv.tags?.join(" ") || "untagged";
|
|
69
|
+
const project = iv.tags?.find((t) => !t.startsWith("#")) ?? "";
|
|
70
|
+
const existing = taskMap.get(key);
|
|
71
|
+
if (existing) {
|
|
72
|
+
existing.duration += duration;
|
|
73
|
+
} else {
|
|
74
|
+
taskMap.set(key, { project, duration });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const ids = [...taskMap.keys()].filter((k) => k !== "untagged" && /^\d+$/.test(k));
|
|
78
|
+
const descriptions = lookupDescriptions(ids);
|
|
79
|
+
const tasks = [];
|
|
80
|
+
for (const [id, info] of taskMap) {
|
|
81
|
+
tasks.push({
|
|
82
|
+
id,
|
|
83
|
+
project: info.project,
|
|
84
|
+
description: descriptions.get(id) ?? "",
|
|
85
|
+
duration: info.duration
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return tasks.sort((a, b) => b.duration - a.duration);
|
|
89
|
+
}
|
|
90
|
+
function computeWallTime(intervals) {
|
|
91
|
+
if (intervals.length === 0) return 0;
|
|
92
|
+
const events = [];
|
|
93
|
+
for (const iv of intervals) {
|
|
94
|
+
const start = parseTimewDate(iv.start).getTime();
|
|
95
|
+
const end = iv.end ? parseTimewDate(iv.end).getTime() : Date.now();
|
|
96
|
+
events.push({ time: start, type: "start" });
|
|
97
|
+
events.push({ time: end, type: "end" });
|
|
98
|
+
}
|
|
99
|
+
events.sort((a, b) => a.time - b.time || (a.type === "end" ? -1 : 1));
|
|
100
|
+
let wallTime = 0;
|
|
101
|
+
let depth = 0;
|
|
102
|
+
let segmentStart = 0;
|
|
103
|
+
for (const event of events) {
|
|
104
|
+
if (event.type === "start") {
|
|
105
|
+
if (depth === 0) segmentStart = event.time;
|
|
106
|
+
depth++;
|
|
107
|
+
} else {
|
|
108
|
+
depth--;
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
wallTime += event.time - segmentStart;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return wallTime;
|
|
115
|
+
}
|
|
116
|
+
function computeTimeline(intervals) {
|
|
117
|
+
if (intervals.length === 0) return [];
|
|
118
|
+
const points = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const iv of intervals) {
|
|
120
|
+
points.add(parseTimewDate(iv.start).getTime());
|
|
121
|
+
points.add(iv.end ? parseTimewDate(iv.end).getTime() : Date.now());
|
|
122
|
+
}
|
|
123
|
+
const sorted = [...points].sort((a, b) => a - b);
|
|
124
|
+
const segments = [];
|
|
125
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
126
|
+
const segStart = sorted[i];
|
|
127
|
+
const segEnd = sorted[i + 1];
|
|
128
|
+
const midpoint = (segStart + segEnd) / 2;
|
|
129
|
+
const activeTags = [];
|
|
130
|
+
for (const iv of intervals) {
|
|
131
|
+
const ivStart = parseTimewDate(iv.start).getTime();
|
|
132
|
+
const ivEnd = iv.end ? parseTimewDate(iv.end).getTime() : Date.now();
|
|
133
|
+
if (ivStart <= midpoint && ivEnd > midpoint) {
|
|
134
|
+
for (const tag of iv.tags ?? []) {
|
|
135
|
+
if (!activeTags.includes(tag)) activeTags.push(tag);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (activeTags.length > 0) {
|
|
140
|
+
segments.push({
|
|
141
|
+
start: new Date(segStart),
|
|
142
|
+
end: new Date(segEnd),
|
|
143
|
+
tags: activeTags
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return segments;
|
|
148
|
+
}
|
|
149
|
+
var BOLD = "\x1B[1m";
|
|
150
|
+
var DIM = "\x1B[2m";
|
|
151
|
+
var RESET = "\x1B[0m";
|
|
152
|
+
var LINE = "\u2500".repeat(60);
|
|
153
|
+
function printTaskTime(tasks, totalTask, wallTime) {
|
|
154
|
+
console.log(`${BOLD}Task Time${RESET}`);
|
|
155
|
+
console.log(LINE);
|
|
156
|
+
for (const t of tasks) {
|
|
157
|
+
const desc = t.description ? ` ${t.description}` : "";
|
|
158
|
+
const idLabel = t.id ? `#${t.id}` : t.project;
|
|
159
|
+
console.log(
|
|
160
|
+
` ${t.project.padEnd(14)} ${idLabel.padEnd(8)}${desc.padEnd(28)} ${formatDuration(t.duration).padStart(10)}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
console.log(LINE);
|
|
164
|
+
console.log(` ${"Total task time:".padEnd(50)} ${BOLD}${formatDuration(totalTask).padStart(10)}${RESET}`);
|
|
165
|
+
const overlap = totalTask - wallTime;
|
|
166
|
+
if (overlap > 1e3) {
|
|
167
|
+
console.log(` ${"Wall time:".padEnd(50)} ${formatDuration(wallTime).padStart(10)}`);
|
|
168
|
+
console.log(` ${DIM}${"Overlap:".padEnd(50)} ${formatDuration(overlap).padStart(10)}${RESET}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function printWallTime(intervals) {
|
|
172
|
+
const timeline = computeTimeline(intervals);
|
|
173
|
+
const wallTime = computeWallTime(intervals);
|
|
174
|
+
console.log(`${BOLD}Wall Time${RESET}`);
|
|
175
|
+
console.log(LINE);
|
|
176
|
+
for (const seg of timeline) {
|
|
177
|
+
const start = formatTime(seg.start);
|
|
178
|
+
const end = formatTime(seg.end);
|
|
179
|
+
const tags = seg.tags.join(", ");
|
|
180
|
+
const duration = seg.end.getTime() - seg.start.getTime();
|
|
181
|
+
console.log(
|
|
182
|
+
` ${start}\u2013${end} ${tags.padEnd(36)} ${DIM}${formatDuration(duration).padStart(10)}${RESET}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
console.log(LINE);
|
|
186
|
+
console.log(` ${"Total wall time:".padEnd(50)} ${BOLD}${formatDuration(wallTime).padStart(10)}${RESET}`);
|
|
187
|
+
}
|
|
188
|
+
function main() {
|
|
189
|
+
const args = process.argv.slice(2);
|
|
190
|
+
const intervals = parseInput();
|
|
191
|
+
let mode = "both";
|
|
192
|
+
const filters = [];
|
|
193
|
+
for (const arg of args) {
|
|
194
|
+
if (arg === "task-time") mode = "task-time";
|
|
195
|
+
else if (arg === "wall-time") mode = "wall-time";
|
|
196
|
+
else if (!arg.startsWith(":")) filters.push(arg);
|
|
197
|
+
}
|
|
198
|
+
let filtered = intervals;
|
|
199
|
+
if (filters.length > 0) {
|
|
200
|
+
filtered = intervals.filter(
|
|
201
|
+
(iv) => filters.some((f) => iv.tags?.includes(f))
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (filtered.length === 0) {
|
|
205
|
+
console.log("No intervals found.");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (mode === "both" || mode === "task-time") {
|
|
209
|
+
const tasks = computeTaskTime(filtered);
|
|
210
|
+
const totalTask = tasks.reduce((a, b) => a + b.duration, 0);
|
|
211
|
+
const wallTime = computeWallTime(filtered);
|
|
212
|
+
printTaskTime(tasks, totalTask, wallTime);
|
|
213
|
+
}
|
|
214
|
+
if (mode === "both") console.log("");
|
|
215
|
+
if (mode === "both" || mode === "wall-time") {
|
|
216
|
+
printWallTime(filtered);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
main();
|