@estruyf/worklog-cli 0.2.0 → 0.2.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/README.md +18 -2
- package/dist/worklog.js +395 -231
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,11 +34,25 @@ Each command refuses options it doesn't have, so a to-do's due date can never en
|
|
|
34
34
|
| `--id` | Log on this exact task id instead. |
|
|
35
35
|
| `--note` | Progress note added under the task's `### Notes`. |
|
|
36
36
|
| `--date` | The day worked, `YYYY-MM-DD`. Defaults to today. |
|
|
37
|
+
| `--time` | Time spent on the task that day: `1h`, `45m`, `1h30m`, or `none` to clear it. Sets the day's time, the same as the app's Time field. |
|
|
38
|
+
|
|
39
|
+
`--time` is a record on the task only. The hours you bill a client for stay with [`hours`](#hours--time-worked).
|
|
37
40
|
|
|
38
41
|
The task is matched by `--id`, then `--link`, then an **open task of the same title** for that
|
|
39
42
|
client — so a task you typed into the app yourself is picked up rather than duplicated. With no match
|
|
40
43
|
it is created.
|
|
41
44
|
|
|
45
|
+
## `unlog` — take a log back off
|
|
46
|
+
|
|
47
|
+
For a log that went on the wrong task or day. Name the task like `update`, then pass what to remove:
|
|
48
|
+
|
|
49
|
+
| Option | Meaning |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `--date` | Take this day worked off the task, with its time. |
|
|
52
|
+
| `--note` | Remove the note with exactly this text. |
|
|
53
|
+
|
|
54
|
+
Each option removes only what it names. The task itself and its status stay as they are.
|
|
55
|
+
|
|
42
56
|
## `add` — a to-do
|
|
43
57
|
|
|
44
58
|
Creates an **open task**: nothing is marked worked and no note is written.
|
|
@@ -52,6 +66,7 @@ Creates an **open task**: nothing is marked worked and no note is written.
|
|
|
52
66
|
| `--description` | What the task is about, written under its fields. |
|
|
53
67
|
| `--tag` | A tag. Repeat it for several. |
|
|
54
68
|
| `--parent` | A task id, making the new task its subtask. |
|
|
69
|
+
| `--repeat` | A repeat rule, as the app's `- repeat:` line takes it: `weekly on mon`, `monthly on last`, … Without `--due`, the first occurrence is the due date. |
|
|
55
70
|
| `--link` | Issue or ticket url. |
|
|
56
71
|
|
|
57
72
|
An open task with the same link or title is left as it is rather than duplicated.
|
|
@@ -70,6 +85,7 @@ several clients have one), then pass only what changes:
|
|
|
70
85
|
| `--status` | A status id from `.worklog/config.json`. A closing one closes the task. |
|
|
71
86
|
| `--tag` / `--untag` | Add or remove a tag. Repeatable. |
|
|
72
87
|
| `--parent` | Move it under this task id, or `none` to make it top-level. |
|
|
88
|
+
| `--repeat` | A new repeat rule, or `none` to stop it repeating. Its `repeatFrom` and `repeatUntil` are kept. |
|
|
73
89
|
|
|
74
90
|
A title that matches open tasks of several clients is refused, with their ids, rather than guessed.
|
|
75
91
|
|
|
@@ -87,8 +103,8 @@ due, priority, client, status, title), soonest due first.
|
|
|
87
103
|
| --- | --- |
|
|
88
104
|
| `--client` | Only this client's tasks; `todos` for the general to-do list. |
|
|
89
105
|
| `--due-by` | Only tasks due on or before this day. |
|
|
90
|
-
| `--tag` | Only tasks with this tag. |
|
|
91
|
-
| `--json` | Print JSON instead, with descriptions, tags and
|
|
106
|
+
| `--tag` | Only tasks with this tag. Repeat it for tasks with all of them. |
|
|
107
|
+
| `--json` | Print JSON instead, with descriptions, tags, links, the days worked and the minutes per day. |
|
|
92
108
|
|
|
93
109
|
## `hours` — time worked
|
|
94
110
|
|
package/dist/worklog.js
CHANGED
|
@@ -101,6 +101,216 @@ function isTaskHeading(lines, index) {
|
|
|
101
101
|
return false;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// src/parser/blocks.ts
|
|
105
|
+
var ID_META2 = /^-\s+id\s*:\s*(.+?)\s*$/;
|
|
106
|
+
function findBlockRange(content, taskId) {
|
|
107
|
+
const lines = content.split(/\r?\n/);
|
|
108
|
+
let blockStart = -1;
|
|
109
|
+
let matched = false;
|
|
110
|
+
const ranges = [];
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
if (isTaskHeading(lines, i)) {
|
|
113
|
+
if (blockStart !== -1) {
|
|
114
|
+
ranges.push({ start: blockStart, end: i, matched });
|
|
115
|
+
}
|
|
116
|
+
blockStart = i;
|
|
117
|
+
matched = false;
|
|
118
|
+
} else if (blockStart !== -1) {
|
|
119
|
+
const m = ID_META2.exec(lines[i]);
|
|
120
|
+
if (m && m[1] === taskId) {
|
|
121
|
+
matched = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (blockStart !== -1) {
|
|
126
|
+
ranges.push({ start: blockStart, end: lines.length, matched });
|
|
127
|
+
}
|
|
128
|
+
const hit = ranges.find((r) => r.matched);
|
|
129
|
+
return hit ? { start: hit.start, end: hit.end } : void 0;
|
|
130
|
+
}
|
|
131
|
+
function replaceBlock(content, taskId, newBlock) {
|
|
132
|
+
const range = findBlockRange(content, taskId);
|
|
133
|
+
if (!range) {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
const lines = content.split(/\r?\n/);
|
|
137
|
+
const original = lines.slice(range.start, range.end);
|
|
138
|
+
const hadTrailingBlank = original.length > 0 && original[original.length - 1].trim() === "";
|
|
139
|
+
const replacement = newBlock.split(/\r?\n/);
|
|
140
|
+
if (hadTrailingBlank) {
|
|
141
|
+
replacement.push("");
|
|
142
|
+
}
|
|
143
|
+
const next = [...lines.slice(0, range.start), ...replacement, ...lines.slice(range.end)];
|
|
144
|
+
return next.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
145
|
+
}
|
|
146
|
+
function extractBlock(content, taskId) {
|
|
147
|
+
const range = findBlockRange(content, taskId);
|
|
148
|
+
if (!range) {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
const lines = content.split(/\r?\n/);
|
|
152
|
+
const blockLines = lines.slice(range.start, range.end);
|
|
153
|
+
while (blockLines.length && blockLines[blockLines.length - 1].trim() === "") {
|
|
154
|
+
blockLines.pop();
|
|
155
|
+
}
|
|
156
|
+
const before = lines.slice(0, range.start);
|
|
157
|
+
const after = lines.slice(range.end);
|
|
158
|
+
while (before.length && before[before.length - 1].trim() === "") {
|
|
159
|
+
before.pop();
|
|
160
|
+
}
|
|
161
|
+
const remainderLines = [...before];
|
|
162
|
+
if (before.length && after.some((l) => l.trim() !== "")) {
|
|
163
|
+
remainderLines.push("");
|
|
164
|
+
}
|
|
165
|
+
remainderLines.push(...after);
|
|
166
|
+
let remainder = remainderLines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
|
|
167
|
+
if (remainder) {
|
|
168
|
+
remainder += "\n";
|
|
169
|
+
}
|
|
170
|
+
return { block: blockLines.join("\n"), remainder, startLine: range.start };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/parser/meetingParser.ts
|
|
174
|
+
var H22 = /^##\s+(.*)$/;
|
|
175
|
+
var META2 = /^-\s+([A-Za-z][\w-]*)\s*:\s*(.*)$/;
|
|
176
|
+
var ACTIONS_HEADING = /^###\s+Action items\s*$/i;
|
|
177
|
+
var ACTION_ENTRY = /^-\s+\[([ xX])\]\s*(.*)$/;
|
|
178
|
+
var ACTION_TASK = /^(.*?)\s+→\s+(t_[A-Za-z0-9]+)$/;
|
|
179
|
+
var TIME = /^(\d{1,2}):(\d{2})$/;
|
|
180
|
+
var DURATION = /^(?:(\d+(?:\.\d+)?)\s*h)?\s*(?:(\d+)\s*m(?:in)?)?$/i;
|
|
181
|
+
function parseDuration(value) {
|
|
182
|
+
const text = value.trim();
|
|
183
|
+
if (/^\d+$/.test(text)) {
|
|
184
|
+
const n = Number(text);
|
|
185
|
+
return n > 0 ? n : void 0;
|
|
186
|
+
}
|
|
187
|
+
const m = DURATION.exec(text);
|
|
188
|
+
if (!m || !m[1] && !m[2]) {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
const minutes = Math.round(Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0));
|
|
192
|
+
return minutes > 0 ? minutes : void 0;
|
|
193
|
+
}
|
|
194
|
+
function formatDuration(minutes) {
|
|
195
|
+
const h = Math.floor(minutes / 60);
|
|
196
|
+
const m = minutes % 60;
|
|
197
|
+
if (h === 0) {
|
|
198
|
+
return `${m}m`;
|
|
199
|
+
}
|
|
200
|
+
return m === 0 ? `${h}h` : `${h}h${m}m`;
|
|
201
|
+
}
|
|
202
|
+
function normalizeTime(value) {
|
|
203
|
+
const m = TIME.exec(value.trim());
|
|
204
|
+
if (!m || Number(m[1]) > 23 || Number(m[2]) > 59) {
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
return `${m[1].padStart(2, "0")}:${m[2]}`;
|
|
208
|
+
}
|
|
209
|
+
function parseMeetingFile(content, sourceFile) {
|
|
210
|
+
const lines = content.split(/\r?\n/);
|
|
211
|
+
const starts = [];
|
|
212
|
+
for (let i = 0; i < lines.length; i++) {
|
|
213
|
+
if (isTaskHeading(lines, i)) {
|
|
214
|
+
starts.push(i);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const meetings = [];
|
|
218
|
+
for (let b = 0; b < starts.length; b++) {
|
|
219
|
+
const end = b + 1 < starts.length ? starts[b + 1] : lines.length;
|
|
220
|
+
const meeting = parseBlock(lines.slice(starts[b], end), sourceFile, starts[b]);
|
|
221
|
+
if (meeting.id) {
|
|
222
|
+
meetings.push(meeting);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return meetings;
|
|
226
|
+
}
|
|
227
|
+
function parseBlock(lines, sourceFile, line) {
|
|
228
|
+
const meeting = {
|
|
229
|
+
id: "",
|
|
230
|
+
title: (H22.exec(lines[0])?.[1] ?? "").trim(),
|
|
231
|
+
date: "",
|
|
232
|
+
people: [],
|
|
233
|
+
notes: "",
|
|
234
|
+
actions: [],
|
|
235
|
+
sourceFile,
|
|
236
|
+
sourceLine: line
|
|
237
|
+
};
|
|
238
|
+
let i = 1;
|
|
239
|
+
for (; i < lines.length; i++) {
|
|
240
|
+
const meta = META2.exec(lines[i]);
|
|
241
|
+
if (!meta) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
const value = meta[2].trim();
|
|
245
|
+
switch (meta[1].toLowerCase()) {
|
|
246
|
+
case "id":
|
|
247
|
+
meeting.id = value;
|
|
248
|
+
break;
|
|
249
|
+
case "date":
|
|
250
|
+
meeting.date = value;
|
|
251
|
+
break;
|
|
252
|
+
case "time":
|
|
253
|
+
meeting.time = normalizeTime(value) ?? meeting.time;
|
|
254
|
+
break;
|
|
255
|
+
case "duration":
|
|
256
|
+
meeting.duration = parseDuration(value) ?? meeting.duration;
|
|
257
|
+
break;
|
|
258
|
+
case "client":
|
|
259
|
+
meeting.clientId = value || void 0;
|
|
260
|
+
break;
|
|
261
|
+
case "people":
|
|
262
|
+
meeting.people = splitPeople(value);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const body = trimBlank(lines.slice(i));
|
|
267
|
+
const at = actionsStart(body);
|
|
268
|
+
if (at < 0) {
|
|
269
|
+
meeting.notes = body.join("\n");
|
|
270
|
+
} else {
|
|
271
|
+
meeting.notes = trimBlank(body.slice(0, at)).join("\n");
|
|
272
|
+
meeting.actions = body.slice(at + 1).filter((l) => l.trim() !== "").map(parseAction);
|
|
273
|
+
}
|
|
274
|
+
return meeting;
|
|
275
|
+
}
|
|
276
|
+
function actionsStart(body) {
|
|
277
|
+
for (let i = body.length - 1; i >= 0; i--) {
|
|
278
|
+
if (ACTIONS_HEADING.test(body[i])) {
|
|
279
|
+
return body.slice(i + 1).every((l) => l.trim() === "" || ACTION_ENTRY.test(l)) ? i : -1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return -1;
|
|
283
|
+
}
|
|
284
|
+
function parseAction(line) {
|
|
285
|
+
const m = ACTION_ENTRY.exec(line);
|
|
286
|
+
const done = m[1] !== " ";
|
|
287
|
+
const linked = ACTION_TASK.exec(m[2].trim());
|
|
288
|
+
return linked ? { text: linked[1].trim(), done, taskId: linked[2] } : { text: m[2].trim(), done };
|
|
289
|
+
}
|
|
290
|
+
function splitPeople(value) {
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const raw of value.split(",")) {
|
|
294
|
+
const name = raw.trim();
|
|
295
|
+
if (name && !seen.has(name.toLowerCase())) {
|
|
296
|
+
seen.add(name.toLowerCase());
|
|
297
|
+
out.push(name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function trimBlank(lines) {
|
|
303
|
+
let start = 0;
|
|
304
|
+
let end = lines.length;
|
|
305
|
+
while (start < end && lines[start].trim() === "") {
|
|
306
|
+
start++;
|
|
307
|
+
}
|
|
308
|
+
while (end > start && lines[end - 1].trim() === "") {
|
|
309
|
+
end--;
|
|
310
|
+
}
|
|
311
|
+
return lines.slice(start, end);
|
|
312
|
+
}
|
|
313
|
+
|
|
104
314
|
// src/util/date.ts
|
|
105
315
|
function today() {
|
|
106
316
|
return toISODate(/* @__PURE__ */ new Date());
|
|
@@ -481,8 +691,8 @@ function rollToWeekday(iso, weekdays) {
|
|
|
481
691
|
|
|
482
692
|
// src/parser/taskParser.ts
|
|
483
693
|
var H1 = /^#\s+(.*)$/;
|
|
484
|
-
var
|
|
485
|
-
var
|
|
694
|
+
var H23 = /^##\s+(.*)$/;
|
|
695
|
+
var META3 = /^-\s+([A-Za-z][\w-]*)\s*:\s*(.*)$/;
|
|
486
696
|
var NOTES_HEADING = /^###\s+Notes\s*$/;
|
|
487
697
|
var NOTE_ENTRY = /^-\s+(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2})?)\s+—\s+(.*)$/;
|
|
488
698
|
var PROMPTS_HEADING = /^###\s+Prompts\s*$/;
|
|
@@ -518,6 +728,7 @@ function parseTaskFile(content, sourceFile, clientId) {
|
|
|
518
728
|
lastDone: t.lastDone,
|
|
519
729
|
repeatOf: t.repeatOf,
|
|
520
730
|
workedOn: t.workedOn,
|
|
731
|
+
workedTime: t.workedTime,
|
|
521
732
|
tags: t.tags,
|
|
522
733
|
checklist,
|
|
523
734
|
prompts,
|
|
@@ -529,7 +740,7 @@ function parseTaskFile(content, sourceFile, clientId) {
|
|
|
529
740
|
};
|
|
530
741
|
for (let i = 0; i < lines.length; i++) {
|
|
531
742
|
const line = lines[i];
|
|
532
|
-
const h2 = isTaskHeading(lines, i) ?
|
|
743
|
+
const h2 = isTaskHeading(lines, i) ? H23.exec(line) : null;
|
|
533
744
|
if (h2) {
|
|
534
745
|
flush();
|
|
535
746
|
current = {
|
|
@@ -548,7 +759,7 @@ function parseTaskFile(content, sourceFile, clientId) {
|
|
|
548
759
|
continue;
|
|
549
760
|
}
|
|
550
761
|
if (inMeta) {
|
|
551
|
-
const meta =
|
|
762
|
+
const meta = META3.exec(line);
|
|
552
763
|
if (meta) {
|
|
553
764
|
applyMeta(current.task, meta[1].toLowerCase(), meta[2].trim());
|
|
554
765
|
continue;
|
|
@@ -655,7 +866,16 @@ function applyMeta(task, key, value) {
|
|
|
655
866
|
if (!value) {
|
|
656
867
|
break;
|
|
657
868
|
}
|
|
658
|
-
const dates =
|
|
869
|
+
const dates = [];
|
|
870
|
+
for (const token of value.split(/[\s,]+/).filter(Boolean)) {
|
|
871
|
+
const minutes = /[hm]/i.test(token) ? parseDuration(token) : void 0;
|
|
872
|
+
const date = dates[dates.length - 1];
|
|
873
|
+
if (minutes !== void 0 && date) {
|
|
874
|
+
task.workedTime = { ...task.workedTime, [date]: minutes };
|
|
875
|
+
} else {
|
|
876
|
+
dates.push(token);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
659
879
|
if (dates.length === 0) {
|
|
660
880
|
break;
|
|
661
881
|
}
|
|
@@ -851,7 +1071,8 @@ function serializeTask(task, clientId) {
|
|
|
851
1071
|
out.push(`- repeatOf: ${task.repeatOf}`);
|
|
852
1072
|
}
|
|
853
1073
|
for (const worked of task.workedOn ?? []) {
|
|
854
|
-
|
|
1074
|
+
const minutes = task.workedTime?.[worked];
|
|
1075
|
+
out.push(`- worked: ${worked}${minutes ? ` ${formatDuration(minutes)}` : ""}`);
|
|
855
1076
|
}
|
|
856
1077
|
if (task.tags && task.tags.length) {
|
|
857
1078
|
out.push(`- tags: ${task.tags.join(", ")}`);
|
|
@@ -950,7 +1171,7 @@ function bodyOf(lines) {
|
|
|
950
1171
|
// src/parser/checklistParser.ts
|
|
951
1172
|
var ITEM = /^(\s*)([-*+])\s+\[([ xX])\]\s?(.*)$/;
|
|
952
1173
|
var H12 = /^#[ \t]+(.*)$/;
|
|
953
|
-
var
|
|
1174
|
+
var H24 = /^##[ \t]+(.*)$/;
|
|
954
1175
|
var LAST_RUN = /^-[ \t]+last run:[ \t]*(\d{4}-\d{2}-\d{2})[ \t]*$/;
|
|
955
1176
|
function parseChecklistFile(content, sourceFile, id) {
|
|
956
1177
|
const lines = content.split(/\r?\n/);
|
|
@@ -968,7 +1189,7 @@ function parseChecklistFile(content, sourceFile, id) {
|
|
|
968
1189
|
});
|
|
969
1190
|
continue;
|
|
970
1191
|
}
|
|
971
|
-
const h2 =
|
|
1192
|
+
const h2 = H24.exec(line);
|
|
972
1193
|
if (h2) {
|
|
973
1194
|
sections.push({ title: h2[1].trim(), line: i, items: [] });
|
|
974
1195
|
continue;
|
|
@@ -993,208 +1214,6 @@ function parseChecklistFile(content, sourceFile, id) {
|
|
|
993
1214
|
};
|
|
994
1215
|
}
|
|
995
1216
|
|
|
996
|
-
// src/parser/blocks.ts
|
|
997
|
-
var ID_META2 = /^-\s+id\s*:\s*(.+?)\s*$/;
|
|
998
|
-
function findBlockRange(content, taskId) {
|
|
999
|
-
const lines = content.split(/\r?\n/);
|
|
1000
|
-
let blockStart = -1;
|
|
1001
|
-
let matched = false;
|
|
1002
|
-
const ranges = [];
|
|
1003
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1004
|
-
if (isTaskHeading(lines, i)) {
|
|
1005
|
-
if (blockStart !== -1) {
|
|
1006
|
-
ranges.push({ start: blockStart, end: i, matched });
|
|
1007
|
-
}
|
|
1008
|
-
blockStart = i;
|
|
1009
|
-
matched = false;
|
|
1010
|
-
} else if (blockStart !== -1) {
|
|
1011
|
-
const m = ID_META2.exec(lines[i]);
|
|
1012
|
-
if (m && m[1] === taskId) {
|
|
1013
|
-
matched = true;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
if (blockStart !== -1) {
|
|
1018
|
-
ranges.push({ start: blockStart, end: lines.length, matched });
|
|
1019
|
-
}
|
|
1020
|
-
const hit = ranges.find((r) => r.matched);
|
|
1021
|
-
return hit ? { start: hit.start, end: hit.end } : void 0;
|
|
1022
|
-
}
|
|
1023
|
-
function replaceBlock(content, taskId, newBlock) {
|
|
1024
|
-
const range = findBlockRange(content, taskId);
|
|
1025
|
-
if (!range) {
|
|
1026
|
-
return void 0;
|
|
1027
|
-
}
|
|
1028
|
-
const lines = content.split(/\r?\n/);
|
|
1029
|
-
const original = lines.slice(range.start, range.end);
|
|
1030
|
-
const hadTrailingBlank = original.length > 0 && original[original.length - 1].trim() === "";
|
|
1031
|
-
const replacement = newBlock.split(/\r?\n/);
|
|
1032
|
-
if (hadTrailingBlank) {
|
|
1033
|
-
replacement.push("");
|
|
1034
|
-
}
|
|
1035
|
-
const next = [...lines.slice(0, range.start), ...replacement, ...lines.slice(range.end)];
|
|
1036
|
-
return next.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
1037
|
-
}
|
|
1038
|
-
function extractBlock(content, taskId) {
|
|
1039
|
-
const range = findBlockRange(content, taskId);
|
|
1040
|
-
if (!range) {
|
|
1041
|
-
return void 0;
|
|
1042
|
-
}
|
|
1043
|
-
const lines = content.split(/\r?\n/);
|
|
1044
|
-
const blockLines = lines.slice(range.start, range.end);
|
|
1045
|
-
while (blockLines.length && blockLines[blockLines.length - 1].trim() === "") {
|
|
1046
|
-
blockLines.pop();
|
|
1047
|
-
}
|
|
1048
|
-
const before = lines.slice(0, range.start);
|
|
1049
|
-
const after = lines.slice(range.end);
|
|
1050
|
-
while (before.length && before[before.length - 1].trim() === "") {
|
|
1051
|
-
before.pop();
|
|
1052
|
-
}
|
|
1053
|
-
const remainderLines = [...before];
|
|
1054
|
-
if (before.length && after.some((l) => l.trim() !== "")) {
|
|
1055
|
-
remainderLines.push("");
|
|
1056
|
-
}
|
|
1057
|
-
remainderLines.push(...after);
|
|
1058
|
-
let remainder = remainderLines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
|
|
1059
|
-
if (remainder) {
|
|
1060
|
-
remainder += "\n";
|
|
1061
|
-
}
|
|
1062
|
-
return { block: blockLines.join("\n"), remainder, startLine: range.start };
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// src/parser/meetingParser.ts
|
|
1066
|
-
var H24 = /^##\s+(.*)$/;
|
|
1067
|
-
var META3 = /^-\s+([A-Za-z][\w-]*)\s*:\s*(.*)$/;
|
|
1068
|
-
var ACTIONS_HEADING = /^###\s+Action items\s*$/i;
|
|
1069
|
-
var ACTION_ENTRY = /^-\s+\[([ xX])\]\s*(.*)$/;
|
|
1070
|
-
var ACTION_TASK = /^(.*?)\s+→\s+(t_[A-Za-z0-9]+)$/;
|
|
1071
|
-
var TIME = /^(\d{1,2}):(\d{2})$/;
|
|
1072
|
-
var DURATION = /^(?:(\d+(?:\.\d+)?)\s*h)?\s*(?:(\d+)\s*m(?:in)?)?$/i;
|
|
1073
|
-
function parseDuration(value) {
|
|
1074
|
-
const text = value.trim();
|
|
1075
|
-
if (/^\d+$/.test(text)) {
|
|
1076
|
-
const n = Number(text);
|
|
1077
|
-
return n > 0 ? n : void 0;
|
|
1078
|
-
}
|
|
1079
|
-
const m = DURATION.exec(text);
|
|
1080
|
-
if (!m || !m[1] && !m[2]) {
|
|
1081
|
-
return void 0;
|
|
1082
|
-
}
|
|
1083
|
-
const minutes = Math.round(Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0));
|
|
1084
|
-
return minutes > 0 ? minutes : void 0;
|
|
1085
|
-
}
|
|
1086
|
-
function normalizeTime(value) {
|
|
1087
|
-
const m = TIME.exec(value.trim());
|
|
1088
|
-
if (!m || Number(m[1]) > 23 || Number(m[2]) > 59) {
|
|
1089
|
-
return void 0;
|
|
1090
|
-
}
|
|
1091
|
-
return `${m[1].padStart(2, "0")}:${m[2]}`;
|
|
1092
|
-
}
|
|
1093
|
-
function parseMeetingFile(content, sourceFile) {
|
|
1094
|
-
const lines = content.split(/\r?\n/);
|
|
1095
|
-
const starts = [];
|
|
1096
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1097
|
-
if (isTaskHeading(lines, i)) {
|
|
1098
|
-
starts.push(i);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
const meetings = [];
|
|
1102
|
-
for (let b = 0; b < starts.length; b++) {
|
|
1103
|
-
const end = b + 1 < starts.length ? starts[b + 1] : lines.length;
|
|
1104
|
-
const meeting = parseBlock(lines.slice(starts[b], end), sourceFile, starts[b]);
|
|
1105
|
-
if (meeting.id) {
|
|
1106
|
-
meetings.push(meeting);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return meetings;
|
|
1110
|
-
}
|
|
1111
|
-
function parseBlock(lines, sourceFile, line) {
|
|
1112
|
-
const meeting = {
|
|
1113
|
-
id: "",
|
|
1114
|
-
title: (H24.exec(lines[0])?.[1] ?? "").trim(),
|
|
1115
|
-
date: "",
|
|
1116
|
-
people: [],
|
|
1117
|
-
notes: "",
|
|
1118
|
-
actions: [],
|
|
1119
|
-
sourceFile,
|
|
1120
|
-
sourceLine: line
|
|
1121
|
-
};
|
|
1122
|
-
let i = 1;
|
|
1123
|
-
for (; i < lines.length; i++) {
|
|
1124
|
-
const meta = META3.exec(lines[i]);
|
|
1125
|
-
if (!meta) {
|
|
1126
|
-
break;
|
|
1127
|
-
}
|
|
1128
|
-
const value = meta[2].trim();
|
|
1129
|
-
switch (meta[1].toLowerCase()) {
|
|
1130
|
-
case "id":
|
|
1131
|
-
meeting.id = value;
|
|
1132
|
-
break;
|
|
1133
|
-
case "date":
|
|
1134
|
-
meeting.date = value;
|
|
1135
|
-
break;
|
|
1136
|
-
case "time":
|
|
1137
|
-
meeting.time = normalizeTime(value) ?? meeting.time;
|
|
1138
|
-
break;
|
|
1139
|
-
case "duration":
|
|
1140
|
-
meeting.duration = parseDuration(value) ?? meeting.duration;
|
|
1141
|
-
break;
|
|
1142
|
-
case "client":
|
|
1143
|
-
meeting.clientId = value || void 0;
|
|
1144
|
-
break;
|
|
1145
|
-
case "people":
|
|
1146
|
-
meeting.people = splitPeople(value);
|
|
1147
|
-
break;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
const body = trimBlank(lines.slice(i));
|
|
1151
|
-
const at = actionsStart(body);
|
|
1152
|
-
if (at < 0) {
|
|
1153
|
-
meeting.notes = body.join("\n");
|
|
1154
|
-
} else {
|
|
1155
|
-
meeting.notes = trimBlank(body.slice(0, at)).join("\n");
|
|
1156
|
-
meeting.actions = body.slice(at + 1).filter((l) => l.trim() !== "").map(parseAction);
|
|
1157
|
-
}
|
|
1158
|
-
return meeting;
|
|
1159
|
-
}
|
|
1160
|
-
function actionsStart(body) {
|
|
1161
|
-
for (let i = body.length - 1; i >= 0; i--) {
|
|
1162
|
-
if (ACTIONS_HEADING.test(body[i])) {
|
|
1163
|
-
return body.slice(i + 1).every((l) => l.trim() === "" || ACTION_ENTRY.test(l)) ? i : -1;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
return -1;
|
|
1167
|
-
}
|
|
1168
|
-
function parseAction(line) {
|
|
1169
|
-
const m = ACTION_ENTRY.exec(line);
|
|
1170
|
-
const done = m[1] !== " ";
|
|
1171
|
-
const linked = ACTION_TASK.exec(m[2].trim());
|
|
1172
|
-
return linked ? { text: linked[1].trim(), done, taskId: linked[2] } : { text: m[2].trim(), done };
|
|
1173
|
-
}
|
|
1174
|
-
function splitPeople(value) {
|
|
1175
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1176
|
-
const out = [];
|
|
1177
|
-
for (const raw of value.split(",")) {
|
|
1178
|
-
const name = raw.trim();
|
|
1179
|
-
if (name && !seen.has(name.toLowerCase())) {
|
|
1180
|
-
seen.add(name.toLowerCase());
|
|
1181
|
-
out.push(name);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return out;
|
|
1185
|
-
}
|
|
1186
|
-
function trimBlank(lines) {
|
|
1187
|
-
let start = 0;
|
|
1188
|
-
let end = lines.length;
|
|
1189
|
-
while (start < end && lines[start].trim() === "") {
|
|
1190
|
-
start++;
|
|
1191
|
-
}
|
|
1192
|
-
while (end > start && lines[end - 1].trim() === "") {
|
|
1193
|
-
end--;
|
|
1194
|
-
}
|
|
1195
|
-
return lines.slice(start, end);
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
1217
|
// src/model/status.ts
|
|
1199
1218
|
var DEFAULT_STATUSES = [
|
|
1200
1219
|
{ id: "open", label: "Open" },
|
|
@@ -1905,14 +1924,36 @@ async function toggleTaskWorkedOn(store, taskId, date) {
|
|
|
1905
1924
|
existing.delete(date);
|
|
1906
1925
|
}
|
|
1907
1926
|
const workedOn = [...existing].sort();
|
|
1927
|
+
const workedTime = marking ? task.workedTime : withoutDay(task.workedTime, date);
|
|
1908
1928
|
const wip = marking && !isClosed(task) ? inProgressStatusId(store.getConfig().statuses) : void 0;
|
|
1909
1929
|
await updateInPlace(store, task, (t) => ({
|
|
1910
1930
|
...t,
|
|
1911
1931
|
workedOn,
|
|
1932
|
+
workedTime,
|
|
1912
1933
|
status: wip ?? t.status
|
|
1913
1934
|
}));
|
|
1914
1935
|
await store.rebuild("toggleWorked");
|
|
1915
|
-
return { ...task, workedOn, status: wip ?? task.status };
|
|
1936
|
+
return { ...task, workedOn, workedTime, status: wip ?? task.status };
|
|
1937
|
+
}
|
|
1938
|
+
async function setTaskWorkedTime(store, taskId, date, minutes) {
|
|
1939
|
+
let task = store.db.getTask(taskId);
|
|
1940
|
+
if (!task) {
|
|
1941
|
+
throw new Error(`Task ${taskId} not found.`);
|
|
1942
|
+
}
|
|
1943
|
+
if (minutes && !(task.workedOn ?? []).includes(date)) {
|
|
1944
|
+
task = await toggleTaskWorkedOn(store, taskId, date);
|
|
1945
|
+
}
|
|
1946
|
+
const workedTime = minutes ? { ...task.workedTime, [date]: minutes } : withoutDay(task.workedTime, date);
|
|
1947
|
+
await updateInPlace(store, task, (t) => ({ ...t, workedTime }));
|
|
1948
|
+
await store.rebuild("setWorkedTime");
|
|
1949
|
+
return { ...task, workedTime };
|
|
1950
|
+
}
|
|
1951
|
+
function withoutDay(time, date) {
|
|
1952
|
+
if (!time || !(date in time)) {
|
|
1953
|
+
return time;
|
|
1954
|
+
}
|
|
1955
|
+
const { [date]: _gone, ...rest } = time;
|
|
1956
|
+
return Object.keys(rest).length ? rest : void 0;
|
|
1916
1957
|
}
|
|
1917
1958
|
async function addTaskNote(store, taskId, text) {
|
|
1918
1959
|
const task = store.db.getTask(taskId);
|
|
@@ -1931,6 +1972,21 @@ async function addTaskNote(store, taskId, text) {
|
|
|
1931
1972
|
await store.rebuild("addNote");
|
|
1932
1973
|
return store.db.getTask(taskId) ?? task;
|
|
1933
1974
|
}
|
|
1975
|
+
async function deleteTaskNote(store, taskId, index) {
|
|
1976
|
+
const task = store.db.getTask(taskId);
|
|
1977
|
+
if (!task) {
|
|
1978
|
+
throw new Error(`Task ${taskId} not found.`);
|
|
1979
|
+
}
|
|
1980
|
+
await updateInPlace(store, task, (t) => {
|
|
1981
|
+
const notes = [...t.notes ?? []];
|
|
1982
|
+
if (index >= 0 && index < notes.length) {
|
|
1983
|
+
notes.splice(index, 1);
|
|
1984
|
+
}
|
|
1985
|
+
return { ...t, notes };
|
|
1986
|
+
});
|
|
1987
|
+
await store.rebuild("deleteNote");
|
|
1988
|
+
return store.db.getTask(taskId) ?? task;
|
|
1989
|
+
}
|
|
1934
1990
|
async function closeTaskById(store, taskId, completedDate = today()) {
|
|
1935
1991
|
const terminal = terminalStatusId(store.getConfig().statuses);
|
|
1936
1992
|
return closeCascade(store, taskId, terminal, completedDate);
|
|
@@ -2025,7 +2081,8 @@ async function rollOccurrence(store, args) {
|
|
|
2025
2081
|
// come along, but unticked — the ticks belong to the occurrence that earned
|
|
2026
2082
|
// them and stay on its snapshot.
|
|
2027
2083
|
checklist: untickedChecklist(source.checklist),
|
|
2028
|
-
workedOn: void 0
|
|
2084
|
+
workedOn: void 0,
|
|
2085
|
+
workedTime: void 0
|
|
2029
2086
|
};
|
|
2030
2087
|
const replaced = replaceBlock(
|
|
2031
2088
|
content,
|
|
@@ -2109,11 +2166,32 @@ async function undoOccurrence(store, snapshot, template, statusId) {
|
|
|
2109
2166
|
notes: snapshot.notes,
|
|
2110
2167
|
prompts: snapshot.prompts,
|
|
2111
2168
|
checklist: snapshot.checklist,
|
|
2112
|
-
workedOn: snapshot.workedOn
|
|
2169
|
+
workedOn: snapshot.workedOn,
|
|
2170
|
+
workedTime: snapshot.workedTime
|
|
2113
2171
|
}));
|
|
2114
2172
|
await store.rebuild("undoOccurrence");
|
|
2115
2173
|
return store.db.getTask(template.id) ?? template;
|
|
2116
2174
|
}
|
|
2175
|
+
async function setTaskRecurrence(store, taskId, repeat) {
|
|
2176
|
+
const task = store.db.getTask(taskId);
|
|
2177
|
+
if (!task) {
|
|
2178
|
+
throw new Error(`Task ${taskId} not found.`);
|
|
2179
|
+
}
|
|
2180
|
+
if (task.completed) {
|
|
2181
|
+
throw new Error("Only open tasks can repeat.");
|
|
2182
|
+
}
|
|
2183
|
+
await updateInPlace(
|
|
2184
|
+
store,
|
|
2185
|
+
task,
|
|
2186
|
+
(t) => withSeededDue({
|
|
2187
|
+
...t,
|
|
2188
|
+
repeat,
|
|
2189
|
+
lastDone: repeat ? t.lastDone : void 0
|
|
2190
|
+
})
|
|
2191
|
+
);
|
|
2192
|
+
await store.rebuild("setRecurrence");
|
|
2193
|
+
return store.db.getTask(taskId) ?? task;
|
|
2194
|
+
}
|
|
2117
2195
|
async function updateTask(store, taskId, fields) {
|
|
2118
2196
|
const task = store.db.getTask(taskId);
|
|
2119
2197
|
if (!task) {
|
|
@@ -2248,12 +2326,16 @@ async function logWork(store, input) {
|
|
|
2248
2326
|
if (markedWorked) {
|
|
2249
2327
|
task = await toggleTaskWorkedOn(store, task.id, date);
|
|
2250
2328
|
}
|
|
2329
|
+
const timeSet = input.minutes === null ? !!task.workedTime?.[date] : !!input.minutes && task.workedTime?.[date] !== input.minutes;
|
|
2330
|
+
if (timeSet) {
|
|
2331
|
+
task = await setTaskWorkedTime(store, task.id, date, input.minutes ?? void 0);
|
|
2332
|
+
}
|
|
2251
2333
|
const note = input.note?.trim();
|
|
2252
2334
|
const noteAdded = !!note && !(task.notes ?? []).some((n) => n.timestamp.startsWith(today()) && n.text === note);
|
|
2253
2335
|
if (noteAdded) {
|
|
2254
2336
|
task = await addTaskNote(store, task.id, note);
|
|
2255
2337
|
}
|
|
2256
|
-
return { task: store.db.getTask(task.id) ?? task, created, markedWorked, noteAdded };
|
|
2338
|
+
return { task: store.db.getTask(task.id) ?? task, created, markedWorked, timeSet, noteAdded };
|
|
2257
2339
|
}
|
|
2258
2340
|
function findTask(store, input) {
|
|
2259
2341
|
if (input.taskId) {
|
|
@@ -2348,6 +2430,13 @@ async function editTask(store, ref, edit) {
|
|
|
2348
2430
|
if (Object.keys(fields).length > 0) {
|
|
2349
2431
|
task = await updateTask(store, task.id, fields);
|
|
2350
2432
|
}
|
|
2433
|
+
if (edit.repeat !== void 0) {
|
|
2434
|
+
const next = edit.repeat === null ? void 0 : { ...readRecurrence(edit.repeat), anchor: task.repeat?.anchor ?? "schedule", until: task.repeat?.until };
|
|
2435
|
+
if ((next && formatRecurrence(next)) !== (task.repeat && formatRecurrence(task.repeat))) {
|
|
2436
|
+
task = await setTaskRecurrence(store, task.id, next);
|
|
2437
|
+
changed.push("repeat");
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2351
2440
|
if (edit.status !== void 0 && edit.status !== task.status) {
|
|
2352
2441
|
const statuses = store.getConfig().statuses;
|
|
2353
2442
|
if (!statuses.some((s) => s.id === edit.status)) {
|
|
@@ -2366,6 +2455,28 @@ async function closeTask(store, ref) {
|
|
|
2366
2455
|
const closed = await closeTaskById(store, task.id);
|
|
2367
2456
|
return { task: store.db.getTask(task.id) ?? closed, closed: true };
|
|
2368
2457
|
}
|
|
2458
|
+
async function unlogWork(store, ref, input) {
|
|
2459
|
+
let task = requireTask(store, ref, { includeClosed: true });
|
|
2460
|
+
const unmarked = !!input.date && (task.workedOn ?? []).includes(input.date);
|
|
2461
|
+
if (input.date && unmarked) {
|
|
2462
|
+
task = await toggleTaskWorkedOn(store, task.id, input.date);
|
|
2463
|
+
}
|
|
2464
|
+
const note = input.note?.trim();
|
|
2465
|
+
const index = note ? (task.notes ?? []).map((n) => n.text).lastIndexOf(note) : -1;
|
|
2466
|
+
if (index >= 0) {
|
|
2467
|
+
task = await deleteTaskNote(store, task.id, index);
|
|
2468
|
+
}
|
|
2469
|
+
return { task: store.db.getTask(task.id) ?? task, unmarked, noteRemoved: index >= 0 };
|
|
2470
|
+
}
|
|
2471
|
+
function readRecurrence(text) {
|
|
2472
|
+
const rule = parseRecurrence(text);
|
|
2473
|
+
if (!rule) {
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
`Can't read the repeat "${text}". Use daily, weekdays, every 3 days, weekly on mon,thu, monthly on 15, monthly on last or yearly on 03-14.`
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
return rule;
|
|
2479
|
+
}
|
|
2369
2480
|
function requireParent(store, parentId, childId) {
|
|
2370
2481
|
const parent = store.db.getTask(parentId);
|
|
2371
2482
|
if (!parent) {
|
|
@@ -2388,6 +2499,7 @@ async function addTask(store, input) {
|
|
|
2388
2499
|
if (input.parentId) {
|
|
2389
2500
|
requireParent(store, input.parentId);
|
|
2390
2501
|
}
|
|
2502
|
+
const repeat = input.repeat ? readRecurrence(input.repeat) : void 0;
|
|
2391
2503
|
const task = await createTask(store, {
|
|
2392
2504
|
clientId: input.clientId,
|
|
2393
2505
|
title: input.title,
|
|
@@ -2396,15 +2508,16 @@ async function addTask(store, input) {
|
|
|
2396
2508
|
priority: input.priority,
|
|
2397
2509
|
description: input.description,
|
|
2398
2510
|
tags: input.tags,
|
|
2399
|
-
parentId: input.parentId
|
|
2511
|
+
parentId: input.parentId,
|
|
2512
|
+
repeat
|
|
2400
2513
|
});
|
|
2401
2514
|
return { task: store.db.getTask(task.id) ?? task, created: true };
|
|
2402
2515
|
}
|
|
2403
2516
|
|
|
2404
2517
|
// src/services/listTasks.ts
|
|
2405
2518
|
function listTasks(store, filter = {}) {
|
|
2406
|
-
const
|
|
2407
|
-
return store.db.getAllTasks().filter((t) => !t.completed).filter((t) => !filter.clientId || t.clientIds.includes(filter.clientId)).filter((t) => !filter.dueBy || !!t.due && t.due <= filter.dueBy).filter((t) =>
|
|
2519
|
+
const tags = (filter.tags ?? []).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
2520
|
+
return store.db.getAllTasks().filter((t) => !t.completed).filter((t) => !filter.clientId || t.clientIds.includes(filter.clientId)).filter((t) => !filter.dueBy || !!t.due && t.due <= filter.dueBy).filter((t) => tags.every((tag) => (t.tags ?? []).some((x) => x.toLowerCase() === tag))).sort(
|
|
2408
2521
|
(a, b) => (a.due ?? "\uFFFF").localeCompare(b.due ?? "\uFFFF") || priorityRank(a.priority) - priorityRank(b.priority) || (a.created ?? "").localeCompare(b.created ?? "")
|
|
2409
2522
|
);
|
|
2410
2523
|
}
|
|
@@ -2512,7 +2625,8 @@ var NONE = "none";
|
|
|
2512
2625
|
var USAGE = `Usage: worklog <command> [options]
|
|
2513
2626
|
|
|
2514
2627
|
log Work that was done: marks the day worked on a task (creating the task
|
|
2515
|
-
when it's new) and adds the note.
|
|
2628
|
+
when it's new), records the time spent and adds the note.
|
|
2629
|
+
unlog Undoes a log: takes a day worked off a task, removes a note.
|
|
2516
2630
|
add Something still to do: creates an open task. No day is marked worked.
|
|
2517
2631
|
Without --client it goes on the general to-do list.
|
|
2518
2632
|
update Changes an existing task: due date, priority, description, ...
|
|
@@ -2528,7 +2642,7 @@ Every command:
|
|
|
2528
2642
|
--client <id> client id from .worklog/config.json; "todos" is the
|
|
2529
2643
|
general to-do list
|
|
2530
2644
|
|
|
2531
|
-
Naming a task (log, update, done):
|
|
2645
|
+
Naming a task (log, unlog, update, done):
|
|
2532
2646
|
--id <t_xxxxxx> this exact task
|
|
2533
2647
|
--link <url> the task linked to this issue/ticket url
|
|
2534
2648
|
--title <title> the open task with this title
|
|
@@ -2536,6 +2650,13 @@ Naming a task (log, update, done):
|
|
|
2536
2650
|
log --client <id> --title <title>:
|
|
2537
2651
|
--note <text> progress note added to the task
|
|
2538
2652
|
--date <date> day worked, YYYY-MM-DD (default: today)
|
|
2653
|
+
--time <duration> time spent on the task that day: 1h, 45m, 1h30m, or
|
|
2654
|
+
none to clear it. Sets the day's time, it doesn't add.
|
|
2655
|
+
A record only: billed hours stay with "hours"
|
|
2656
|
+
|
|
2657
|
+
unlog (at least one of):
|
|
2658
|
+
--date <date> take this day worked off the task, with its time
|
|
2659
|
+
--note <text> remove the note with exactly this text
|
|
2539
2660
|
|
|
2540
2661
|
add --title <title>:
|
|
2541
2662
|
--link <url> issue/ticket url
|
|
@@ -2544,6 +2665,8 @@ add --title <title>:
|
|
|
2544
2665
|
--description <text> what the task is about
|
|
2545
2666
|
--tag <tag> repeat for several tags
|
|
2546
2667
|
--parent <t_xxxxxx> make it a subtask of this task
|
|
2668
|
+
--repeat <rule> daily, weekdays, every 3 days, weekly on mon,thu,
|
|
2669
|
+
monthly on 15, monthly on last, yearly on 03-14
|
|
2547
2670
|
|
|
2548
2671
|
update:
|
|
2549
2672
|
--rename <title> new title
|
|
@@ -2554,6 +2677,7 @@ update:
|
|
|
2554
2677
|
--tag <tag> add a tag (repeatable)
|
|
2555
2678
|
--untag <tag> remove a tag (repeatable)
|
|
2556
2679
|
--parent <id|none> move it under this task, or none to make it top-level
|
|
2680
|
+
--repeat <rule|none> a new repeat rule (as for add), or none to stop it
|
|
2557
2681
|
|
|
2558
2682
|
hours --client <id> | --event <type>:
|
|
2559
2683
|
--hours <amount> full, half, or a number of hours; 0 removes the entry.
|
|
@@ -2564,14 +2688,16 @@ hours --client <id> | --event <type>:
|
|
|
2564
2688
|
|
|
2565
2689
|
list:
|
|
2566
2690
|
--due-by <date> only tasks due on or before this day
|
|
2567
|
-
--tag <tag> only tasks with this tag
|
|
2568
|
-
--json print JSON instead of one line per task
|
|
2691
|
+
--tag <tag> only tasks with this tag; repeat it for tasks with all
|
|
2692
|
+
--json print JSON instead of one line per task, with the
|
|
2693
|
+
days worked and the minutes per day
|
|
2569
2694
|
|
|
2570
|
-
log, add, update, done, hours:
|
|
2695
|
+
log, unlog, add, update, done, hours:
|
|
2571
2696
|
--dry-run show the change, commit nothing`;
|
|
2572
2697
|
var OPTIONS = {
|
|
2573
|
-
log: ["client", "id", "link", "title", "note", "date", "dry-run"],
|
|
2574
|
-
|
|
2698
|
+
log: ["client", "id", "link", "title", "note", "date", "time", "dry-run"],
|
|
2699
|
+
unlog: ["client", "id", "link", "title", "date", "note", "dry-run"],
|
|
2700
|
+
add: ["client", "link", "title", "due", "priority", "description", "tag", "parent", "repeat", "dry-run"],
|
|
2575
2701
|
update: [
|
|
2576
2702
|
"client",
|
|
2577
2703
|
"id",
|
|
@@ -2585,6 +2711,7 @@ var OPTIONS = {
|
|
|
2585
2711
|
"tag",
|
|
2586
2712
|
"untag",
|
|
2587
2713
|
"parent",
|
|
2714
|
+
"repeat",
|
|
2588
2715
|
"dry-run"
|
|
2589
2716
|
],
|
|
2590
2717
|
done: ["client", "id", "link", "title", "dry-run"],
|
|
@@ -2593,6 +2720,7 @@ var OPTIONS = {
|
|
|
2593
2720
|
};
|
|
2594
2721
|
var HINTS = {
|
|
2595
2722
|
date: `A to-do's date is --due; --date is the day worked, for "worklog log" and "worklog hours".`,
|
|
2723
|
+
time: `Time on a task is "worklog log --time"; a client's hours for the day are "worklog hours --hours".`,
|
|
2596
2724
|
due: "The day worked is --date; --due is when a to-do is due."
|
|
2597
2725
|
};
|
|
2598
2726
|
async function main() {
|
|
@@ -2605,6 +2733,7 @@ async function main() {
|
|
|
2605
2733
|
id: { type: "string" },
|
|
2606
2734
|
note: { type: "string" },
|
|
2607
2735
|
date: { type: "string" },
|
|
2736
|
+
time: { type: "string" },
|
|
2608
2737
|
due: { type: "string" },
|
|
2609
2738
|
"due-by": { type: "string" },
|
|
2610
2739
|
priority: { type: "string" },
|
|
@@ -2614,6 +2743,7 @@ async function main() {
|
|
|
2614
2743
|
tag: { type: "string", multiple: true },
|
|
2615
2744
|
untag: { type: "string", multiple: true },
|
|
2616
2745
|
parent: { type: "string" },
|
|
2746
|
+
repeat: { type: "string" },
|
|
2617
2747
|
event: { type: "string" },
|
|
2618
2748
|
hours: { type: "string" },
|
|
2619
2749
|
json: { type: "boolean" },
|
|
@@ -2660,6 +2790,10 @@ function validate(values) {
|
|
|
2660
2790
|
throw new Error(`--${option} must be YYYY-MM-DD, got "${value}".`);
|
|
2661
2791
|
}
|
|
2662
2792
|
}
|
|
2793
|
+
const time = values.time;
|
|
2794
|
+
if (typeof time === "string" && time !== NONE && !(/[hm]/i.test(time) && parseDuration(time))) {
|
|
2795
|
+
throw new Error(`--time must be a duration like 1h, 45m or 1h30m, or none, got "${time}".`);
|
|
2796
|
+
}
|
|
2663
2797
|
const priority = values.priority;
|
|
2664
2798
|
if (typeof priority === "string" && !PRIORITIES.some((p) => p.id === priority.trim().toLowerCase())) {
|
|
2665
2799
|
throw new Error(`--priority must be one of ${PRIORITIES.map((p) => p.id).join(", ")}, got "${priority}".`);
|
|
@@ -2679,9 +2813,26 @@ function toEdit(command, values) {
|
|
|
2679
2813
|
|
|
2680
2814
|
${USAGE}`);
|
|
2681
2815
|
}
|
|
2682
|
-
const
|
|
2816
|
+
const time = str("time");
|
|
2817
|
+
const input = {
|
|
2818
|
+
clientId,
|
|
2819
|
+
title,
|
|
2820
|
+
taskId: ref.taskId,
|
|
2821
|
+
link: ref.link,
|
|
2822
|
+
note: str("note"),
|
|
2823
|
+
date: str("date"),
|
|
2824
|
+
minutes: time === NONE ? null : time ? parseDuration(time) : void 0
|
|
2825
|
+
};
|
|
2683
2826
|
return async (store) => describeLog(await logWork(store, input), input);
|
|
2684
2827
|
}
|
|
2828
|
+
case "unlog": {
|
|
2829
|
+
requireRef(ref);
|
|
2830
|
+
const input = { date: str("date"), note: str("note") };
|
|
2831
|
+
if (!input.date && !input.note) {
|
|
2832
|
+
throw new Error("Nothing to take off: pass --date, --note or both.");
|
|
2833
|
+
}
|
|
2834
|
+
return async (store) => describeUnlog(await unlogWork(store, ref, input), input);
|
|
2835
|
+
}
|
|
2685
2836
|
case "add": {
|
|
2686
2837
|
const title = str("title");
|
|
2687
2838
|
if (!title) {
|
|
@@ -2697,7 +2848,8 @@ ${USAGE}`);
|
|
|
2697
2848
|
priority: str("priority"),
|
|
2698
2849
|
description: str("description"),
|
|
2699
2850
|
tags: list2("tag"),
|
|
2700
|
-
parentId: str("parent")
|
|
2851
|
+
parentId: str("parent"),
|
|
2852
|
+
repeat: str("repeat")
|
|
2701
2853
|
};
|
|
2702
2854
|
return async (store) => describeAdd(await addTask(store, input));
|
|
2703
2855
|
}
|
|
@@ -2711,10 +2863,11 @@ ${USAGE}`);
|
|
|
2711
2863
|
status: str("status"),
|
|
2712
2864
|
addTags: list2("tag"),
|
|
2713
2865
|
removeTags: list2("untag"),
|
|
2714
|
-
parentId: orNull(str("parent"))
|
|
2866
|
+
parentId: orNull(str("parent")),
|
|
2867
|
+
repeat: orNull(str("repeat"))
|
|
2715
2868
|
};
|
|
2716
2869
|
if (Object.values(edit).every((v) => v === void 0)) {
|
|
2717
|
-
throw new Error(`Nothing to change: pass at least one of --rename, --due, --priority, --description, --status, --tag, --untag, --parent.`);
|
|
2870
|
+
throw new Error(`Nothing to change: pass at least one of --rename, --due, --priority, --description, --status, --tag, --untag, --parent, --repeat.`);
|
|
2718
2871
|
}
|
|
2719
2872
|
return async (store) => describeUpdate(await editTask(store, ref, edit));
|
|
2720
2873
|
}
|
|
@@ -2797,7 +2950,7 @@ async function list(repo, values) {
|
|
|
2797
2950
|
const tasks = listTasks(store, {
|
|
2798
2951
|
clientId: values.client,
|
|
2799
2952
|
dueBy: values["due-by"],
|
|
2800
|
-
|
|
2953
|
+
tags: values.tag
|
|
2801
2954
|
});
|
|
2802
2955
|
if (values.json) {
|
|
2803
2956
|
console.log(JSON.stringify(tasks.map(toJson), null, 2));
|
|
@@ -2835,6 +2988,8 @@ function toJson(t) {
|
|
|
2835
2988
|
links: t.links.map((l) => l.url),
|
|
2836
2989
|
parent: t.parentId ?? null,
|
|
2837
2990
|
repeats: !!t.repeat,
|
|
2991
|
+
worked: t.workedOn ?? [],
|
|
2992
|
+
minutes: t.workedTime ?? {},
|
|
2838
2993
|
created: t.created ?? null,
|
|
2839
2994
|
description: t.description ?? null,
|
|
2840
2995
|
file: t.sourceFile
|
|
@@ -2873,19 +3028,28 @@ function listFiles(repo) {
|
|
|
2873
3028
|
function label(task) {
|
|
2874
3029
|
return `${task.id} "${task.title}" (${task.sourceFile})`;
|
|
2875
3030
|
}
|
|
2876
|
-
function describeLog({ task, created, markedWorked, noteAdded }, input) {
|
|
3031
|
+
function describeLog({ task, created, markedWorked, timeSet, noteAdded }, input) {
|
|
2877
3032
|
const what = [
|
|
2878
3033
|
created ? "created" : "found",
|
|
2879
3034
|
markedWorked ? `marked worked ${input.date ?? today()}` : void 0,
|
|
3035
|
+
timeSet ? input.minutes ? `time ${formatDuration(input.minutes)}` : "time cleared" : void 0,
|
|
2880
3036
|
noteAdded ? "note added" : void 0
|
|
2881
3037
|
].filter(Boolean);
|
|
2882
3038
|
return `${label(task)}: ${what.join(", ")}`;
|
|
2883
3039
|
}
|
|
3040
|
+
function describeUnlog({ task, unmarked, noteRemoved }, input) {
|
|
3041
|
+
const what = [
|
|
3042
|
+
input.date ? unmarked ? `took ${input.date} off` : `${input.date} wasn't marked worked` : void 0,
|
|
3043
|
+
input.note ? noteRemoved ? "note removed" : "no note with that text" : void 0
|
|
3044
|
+
].filter(Boolean);
|
|
3045
|
+
return `${label(task)}: ${what.join(", ")}`;
|
|
3046
|
+
}
|
|
2884
3047
|
function describeAdd({ task, created }) {
|
|
2885
3048
|
const what = [
|
|
2886
3049
|
created ? "created" : "already exists, left as is",
|
|
2887
3050
|
task.due ? `due ${task.due}` : void 0,
|
|
2888
|
-
task.priority ? `priority ${task.priority}` : void 0
|
|
3051
|
+
task.priority ? `priority ${task.priority}` : void 0,
|
|
3052
|
+
task.repeat ? `repeats ${formatRecurrence(task.repeat)}` : void 0
|
|
2889
3053
|
].filter(Boolean);
|
|
2890
3054
|
return `${label(task)}: ${what.join(", ")}`;
|
|
2891
3055
|
}
|
package/package.json
CHANGED