@walknpm/todo-cli 1.2.0 → 2.0.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/README.md +200 -144
- package/README.zh-CN.md +150 -153
- package/package.json +19 -6
- package/src/cli.js +396 -24
- package/src/server.js +422 -0
- package/src/todo.js +544 -35
- package/web/css/style.css +804 -0
- package/web/index.html +216 -0
- package/web/js/app.js +934 -0
package/src/cli.js
CHANGED
|
@@ -23,6 +23,37 @@ import {
|
|
|
23
23
|
exportTodos,
|
|
24
24
|
dueStatus,
|
|
25
25
|
PRIORITIES,
|
|
26
|
+
migrateAll,
|
|
27
|
+
getTodo,
|
|
28
|
+
// Subtasks
|
|
29
|
+
addSubtask,
|
|
30
|
+
toggleSubtask,
|
|
31
|
+
deleteSubtask,
|
|
32
|
+
editSubtask,
|
|
33
|
+
// Recurring
|
|
34
|
+
setRecurring,
|
|
35
|
+
removeRecurring,
|
|
36
|
+
// Dependencies
|
|
37
|
+
addDependency,
|
|
38
|
+
removeDependency,
|
|
39
|
+
getBlockedBy,
|
|
40
|
+
// Time tracking
|
|
41
|
+
startTimer,
|
|
42
|
+
stopTimer,
|
|
43
|
+
logTime,
|
|
44
|
+
getActiveTimer,
|
|
45
|
+
// Pinning
|
|
46
|
+
togglePin,
|
|
47
|
+
// Duplicate
|
|
48
|
+
duplicateTodo,
|
|
49
|
+
// Bulk
|
|
50
|
+
bulkComplete,
|
|
51
|
+
bulkDelete,
|
|
52
|
+
bulkPriority,
|
|
53
|
+
bulkCategory,
|
|
54
|
+
bulkAssign,
|
|
55
|
+
// Utility
|
|
56
|
+
formatTime,
|
|
26
57
|
} from "./todo.js";
|
|
27
58
|
|
|
28
59
|
// --- version & repo (read from package.json so they stay in sync) -----------
|
|
@@ -42,17 +73,14 @@ const green = (s) => c("32", s);
|
|
|
42
73
|
const red = (s) => c("31", s);
|
|
43
74
|
const yellow = (s) => c("33", s);
|
|
44
75
|
const cyan = (s) => c("36", s);
|
|
76
|
+
const magenta = (s) => c("35", s);
|
|
77
|
+
const blue = (s) => c("34", s);
|
|
45
78
|
const bold = (s) => c("1", s);
|
|
46
79
|
const dim = (s) => c("2", s);
|
|
47
80
|
|
|
48
81
|
const PRIORITY_COLOR = { high: red, medium: yellow, low: green };
|
|
49
82
|
|
|
50
83
|
// --- argument parsing ------------------------------------------------------
|
|
51
|
-
/**
|
|
52
|
-
* Split argv into a positional list and a flags object.
|
|
53
|
-
* Flags: -p/--priority, -t/--tags, -c/--category, -d/--due, --note,
|
|
54
|
-
* --title, --status, --tag, --sort, --all, --format
|
|
55
|
-
*/
|
|
56
84
|
function parseArgs(argv) {
|
|
57
85
|
const positional = [];
|
|
58
86
|
const flags = {};
|
|
@@ -68,8 +96,14 @@ function parseArgs(argv) {
|
|
|
68
96
|
else if (a === "--tag") flags.tag = argv[++i];
|
|
69
97
|
else if (a === "--sort") flags.sort = argv[++i];
|
|
70
98
|
else if (a === "--format") flags.format = argv[++i];
|
|
99
|
+
else if (a === "--assignee") flags.assignee = argv[++i];
|
|
100
|
+
else if (a === "--estimate") flags.estimate = argv[++i];
|
|
101
|
+
else if (a === "--color") flags.color = argv[++i];
|
|
71
102
|
else if (a === "--all") flags.all = true;
|
|
72
103
|
else if (a === "--check") flags.check = true;
|
|
104
|
+
else if (a === "--pinned") flags.pinned = true;
|
|
105
|
+
else if (a === "--overdue") flags.overdue = true;
|
|
106
|
+
else if (a === "--due-today") flags.dueToday = true;
|
|
73
107
|
else if (a === "-h" || a === "--help") flags.help = true;
|
|
74
108
|
else if (a.startsWith("-")) {
|
|
75
109
|
// unknown flag — ignore but don't swallow a value
|
|
@@ -92,9 +126,12 @@ function renderTable(todos) {
|
|
|
92
126
|
return;
|
|
93
127
|
}
|
|
94
128
|
const hasDue = todos.some((t) => t.due);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
const hasAssignee = todos.some((t) => t.assignee);
|
|
130
|
+
const cols = ["ID", "Status", "Priority"];
|
|
131
|
+
if (hasDue) cols.push("Due");
|
|
132
|
+
cols.push("Category", "Tags");
|
|
133
|
+
if (hasAssignee) cols.push("Assignee");
|
|
134
|
+
cols.push("Title");
|
|
98
135
|
|
|
99
136
|
function dueCell(t) {
|
|
100
137
|
if (!t.due) return "-";
|
|
@@ -115,9 +152,21 @@ function renderTable(todos) {
|
|
|
115
152
|
if (hasDue) base.push(dueCell(t));
|
|
116
153
|
base.push(
|
|
117
154
|
t.category || "-",
|
|
118
|
-
t.tags.length ? cyan(t.tags.join(", ")) : "-"
|
|
119
|
-
t.done ? dim(t.title) : t.title
|
|
155
|
+
t.tags.length ? cyan(t.tags.join(", ")) : "-"
|
|
120
156
|
);
|
|
157
|
+
if (hasAssignee) base.push(t.assignee || "-");
|
|
158
|
+
const titleParts = [];
|
|
159
|
+
if (t.pinned) titleParts.push(magenta("📌"));
|
|
160
|
+
if (t.recurring) titleParts.push(blue("⟳"));
|
|
161
|
+
if (t.timerStartedAt) titleParts.push(green("⏱"));
|
|
162
|
+
if (t.subtasks && t.subtasks.length) {
|
|
163
|
+
const done = t.subtasks.filter((s) => s.done).length;
|
|
164
|
+
titleParts.push(dim(`[${done}/${t.subtasks.length}]`));
|
|
165
|
+
}
|
|
166
|
+
const blocked = getBlockedBy(todos, t.id);
|
|
167
|
+
if (blocked.length) titleParts.push(red(`⛔${blocked.length}`));
|
|
168
|
+
titleParts.push(t.done ? dim(t.title) : t.title);
|
|
169
|
+
base.push(titleParts.join(" "));
|
|
121
170
|
return base;
|
|
122
171
|
});
|
|
123
172
|
|
|
@@ -134,6 +183,47 @@ function renderTable(todos) {
|
|
|
134
183
|
console.log(dim(`\n ${todos.length} item(s)`));
|
|
135
184
|
}
|
|
136
185
|
|
|
186
|
+
function showTodoDetail(todos, t) {
|
|
187
|
+
console.log(bold(` #${t.id} ${t.title}`));
|
|
188
|
+
console.log(dim(" " + "─".repeat(50)));
|
|
189
|
+
console.log(` Status: ${t.done ? green("✓ done") : yellow("○ pending")}`);
|
|
190
|
+
console.log(` Priority: ${PRIORITY_COLOR[t.priority](t.priority)}`);
|
|
191
|
+
if (t.category) console.log(` Category: ${t.category}`);
|
|
192
|
+
if (t.tags.length) console.log(` Tags: ${cyan(t.tags.join(", "))}`);
|
|
193
|
+
if (t.due) {
|
|
194
|
+
const ds = dueStatus(t.due);
|
|
195
|
+
const label = ds === "overdue" ? red(" (overdue)") : ds === "today" ? yellow(" (today)") : ds === "soon" ? yellow(" (soon)") : "";
|
|
196
|
+
console.log(` Due: ${t.due}${label}`);
|
|
197
|
+
}
|
|
198
|
+
if (t.note) console.log(` Note: ${t.note}`);
|
|
199
|
+
if (t.assignee) console.log(` Assignee: ${t.assignee}`);
|
|
200
|
+
if (t.estimate) console.log(` Estimate: ${formatTime(t.estimate)}`);
|
|
201
|
+
if (t.timeLogged) console.log(` Time logged:${formatTime(t.timeLogged)}`);
|
|
202
|
+
if (t.pinned) console.log(` Pinned: ${magenta("yes")}`);
|
|
203
|
+
if (t.recurring) console.log(` Recurring: ${blue(`every ${t.recurring.interval} ${t.recurring.freq}(s)`)}`);
|
|
204
|
+
if (t.timerStartedAt) {
|
|
205
|
+
const elapsed = Math.round((Date.now() - new Date(t.timerStartedAt).getTime()) / 60000);
|
|
206
|
+
console.log(` Timer: ${green("running")} (${formatTime(elapsed)} elapsed)`);
|
|
207
|
+
}
|
|
208
|
+
if (t.subtasks && t.subtasks.length) {
|
|
209
|
+
console.log(" Subtasks:");
|
|
210
|
+
for (const s of t.subtasks) {
|
|
211
|
+
console.log(` ${s.done ? green("✓") : dim("○")} #${s.id} ${s.done ? dim(s.title) : s.title}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (t.dependsOn && t.dependsOn.length) {
|
|
215
|
+
console.log(" Depends on:");
|
|
216
|
+
for (const d of t.dependsOn) {
|
|
217
|
+
const dep = getTodo(todos, d);
|
|
218
|
+
const status = dep ? (dep.done ? green("✓") : red("✗")) : dim("?");
|
|
219
|
+
console.log(` ${status} #${d} ${dep ? dep.title : "(deleted)"}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.log(dim(` Created: ${t.createdAt}`));
|
|
223
|
+
if (t.updatedAt) console.log(dim(` Updated: ${t.updatedAt}`));
|
|
224
|
+
if (t.completedAt) console.log(dim(` Completed: ${t.completedAt}`));
|
|
225
|
+
}
|
|
226
|
+
|
|
137
227
|
// --- commands --------------------------------------------------------------
|
|
138
228
|
function cmdAdd(todos, { positional, flags }) {
|
|
139
229
|
const title = positional.join(" ");
|
|
@@ -144,6 +234,10 @@ function cmdAdd(todos, { positional, flags }) {
|
|
|
144
234
|
category: flags.category,
|
|
145
235
|
due: flags.due,
|
|
146
236
|
note: flags.note,
|
|
237
|
+
estimate: flags.estimate,
|
|
238
|
+
assignee: flags.assignee,
|
|
239
|
+
color: flags.color,
|
|
240
|
+
pinned: flags.pinned,
|
|
147
241
|
});
|
|
148
242
|
let msg = green("✓ Added:") + ` #${todo.id} ${todo.title}`;
|
|
149
243
|
if (todo.due) msg += dim(` (due ${todo.due})`);
|
|
@@ -156,11 +250,22 @@ function cmdList(todos, { flags }) {
|
|
|
156
250
|
category: flags.category,
|
|
157
251
|
tag: flags.tag,
|
|
158
252
|
priority: flags.priority,
|
|
253
|
+
assignee: flags.assignee,
|
|
159
254
|
sort: flags.sort,
|
|
255
|
+
overdue: flags.overdue,
|
|
256
|
+
dueToday: flags.dueToday,
|
|
257
|
+
pinnedFirst: true,
|
|
160
258
|
});
|
|
161
259
|
renderTable(result);
|
|
162
260
|
}
|
|
163
261
|
|
|
262
|
+
function cmdShow(todos, { positional }) {
|
|
263
|
+
const id = positional[0];
|
|
264
|
+
const t = getTodo(todos, id);
|
|
265
|
+
if (!t) { console.log(red(`✗ No todo with id ${id}`)); return; }
|
|
266
|
+
showTodoDetail(todos, t);
|
|
267
|
+
}
|
|
268
|
+
|
|
164
269
|
function cmdDone(todos, { positional, flags }) {
|
|
165
270
|
if (flags.all) {
|
|
166
271
|
const n = completeAll(todos);
|
|
@@ -197,6 +302,9 @@ function cmdEdit(todos, { positional, flags }) {
|
|
|
197
302
|
category: flags.category,
|
|
198
303
|
due: flags.due,
|
|
199
304
|
note: flags.note,
|
|
305
|
+
estimate: flags.estimate,
|
|
306
|
+
assignee: flags.assignee,
|
|
307
|
+
color: flags.color,
|
|
200
308
|
});
|
|
201
309
|
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
202
310
|
else console.log(green("✓ Edited:") + ` #${todo.id} ${todo.title}`);
|
|
@@ -225,10 +333,14 @@ function cmdStats(todos) {
|
|
|
225
333
|
const s = getStats(todos);
|
|
226
334
|
console.log(bold("Todo Statistics"));
|
|
227
335
|
console.log(dim("─────────────────"));
|
|
228
|
-
console.log(` Total:
|
|
229
|
-
console.log(` ${green("Done:")}
|
|
230
|
-
console.log(` ${yellow("Pending:")}
|
|
231
|
-
|
|
336
|
+
console.log(` Total: ${s.total}`);
|
|
337
|
+
console.log(` ${green("Done:")} ${s.done}`);
|
|
338
|
+
console.log(` ${yellow("Pending:")} ${s.pending}`);
|
|
339
|
+
console.log(` Completion: ${s.completionRate}%`);
|
|
340
|
+
if (s.overdue > 0) console.log(` ${red("Overdue:")} ${s.overdue}`);
|
|
341
|
+
if (s.dueToday > 0) console.log(` ${yellow("Due today:")} ${s.dueToday}`);
|
|
342
|
+
if (s.pinned > 0) console.log(` ${magenta("Pinned:")} ${s.pinned}`);
|
|
343
|
+
if (s.totalTimeLogged > 0) console.log(` Time logged: ${formatTime(s.totalTimeLogged)}`);
|
|
232
344
|
console.log(dim("\n By priority:"));
|
|
233
345
|
console.log(` ${red("high")}: ${s.byPriority.high} ${yellow("medium")}: ${s.byPriority.medium} ${green("low")}: ${s.byPriority.low}`);
|
|
234
346
|
const cats = Object.keys(s.byCategory);
|
|
@@ -236,6 +348,11 @@ function cmdStats(todos) {
|
|
|
236
348
|
console.log(dim(" By category:"));
|
|
237
349
|
for (const c of cats.sort()) console.log(` ${c}: ${s.byCategory[c]}`);
|
|
238
350
|
}
|
|
351
|
+
const assignees = Object.keys(s.byAssignee);
|
|
352
|
+
if (assignees.length) {
|
|
353
|
+
console.log(dim(" By assignee:"));
|
|
354
|
+
for (const a of assignees.sort()) console.log(` ${a}: ${s.byAssignee[a]}`);
|
|
355
|
+
}
|
|
239
356
|
}
|
|
240
357
|
|
|
241
358
|
function cmdExport(todos, { positional, flags }) {
|
|
@@ -249,6 +366,200 @@ function cmdArchive(todos) {
|
|
|
249
366
|
console.log(green(`✓ Archived (removed) ${n} completed item(s)`));
|
|
250
367
|
}
|
|
251
368
|
|
|
369
|
+
// --- Subtask commands ------------------------------------------------------
|
|
370
|
+
function cmdSubtask(todos, { positional }) {
|
|
371
|
+
const subcmd = positional[0];
|
|
372
|
+
const id = positional[1];
|
|
373
|
+
if (!subcmd || !id) {
|
|
374
|
+
console.log(red("✗ Usage: todo subtask <add|done|delete|edit> <todoId> [args]"));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
switch (subcmd) {
|
|
378
|
+
case "add": {
|
|
379
|
+
const title = positional.slice(2).join(" ");
|
|
380
|
+
const st = addSubtask(todos, id, title);
|
|
381
|
+
if (!st) console.log(red(`✗ No todo with id ${id}`));
|
|
382
|
+
else console.log(green("✓ Subtask added:") + ` #${st.id} ${st.title}`);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case "done":
|
|
386
|
+
case "toggle": {
|
|
387
|
+
const st = toggleSubtask(todos, id, positional[2]);
|
|
388
|
+
if (!st) console.log(red(`✗ Subtask not found`));
|
|
389
|
+
else console.log(green("✓ Subtask:") + ` #${st.id} ${st.done ? "done" : "pending"}`);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "delete":
|
|
393
|
+
case "rm": {
|
|
394
|
+
const ok = deleteSubtask(todos, id, positional[2]);
|
|
395
|
+
if (!ok) console.log(red("✗ Subtask not found"));
|
|
396
|
+
else console.log(green("✓ Subtask deleted"));
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case "edit": {
|
|
400
|
+
const title = positional.slice(3).join(" ");
|
|
401
|
+
const st = editSubtask(todos, id, positional[2], title);
|
|
402
|
+
if (!st) console.log(red("✗ Subtask not found"));
|
|
403
|
+
else console.log(green("✓ Subtask edited:") + ` #${st.id} ${st.title}`);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
default:
|
|
407
|
+
console.log(red(`✗ Unknown subtask command: ${subcmd}`));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// --- Pin command -----------------------------------------------------------
|
|
412
|
+
function cmdPin(todos, { positional }) {
|
|
413
|
+
const todo = togglePin(todos, positional[0]);
|
|
414
|
+
if (!todo) console.log(red(`✗ No todo with id ${positional[0]}`));
|
|
415
|
+
else console.log(green("✓") + ` #${todo.id} ${todo.pinned ? "pinned" : "unpinned"}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// --- Duplicate command -----------------------------------------------------
|
|
419
|
+
function cmdDuplicate(todos, { positional }) {
|
|
420
|
+
const todo = duplicateTodo(todos, positional[0]);
|
|
421
|
+
if (!todo) console.log(red(`✗ No todo with id ${positional[0]}`));
|
|
422
|
+
else console.log(green("✓ Duplicated:") + ` #${todo.id} ${todo.title}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// --- Timer commands --------------------------------------------------------
|
|
426
|
+
function cmdTimer(todos, { positional }) {
|
|
427
|
+
const subcmd = positional[0];
|
|
428
|
+
const id = positional[1];
|
|
429
|
+
if (!subcmd) {
|
|
430
|
+
const active = getActiveTimer(todos);
|
|
431
|
+
if (active) {
|
|
432
|
+
const elapsed = Math.round((Date.now() - new Date(active.timerStartedAt).getTime()) / 60000);
|
|
433
|
+
console.log(green("⏱ Active timer:") + ` #${active.id} ${active.title} — ${formatTime(elapsed)} elapsed`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(dim(" No active timer"));
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
switch (subcmd) {
|
|
440
|
+
case "start": {
|
|
441
|
+
const todo = startTimer(todos, id);
|
|
442
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
443
|
+
else console.log(green("⏱ Timer started:") + ` #${todo.id} ${todo.title}`);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case "stop": {
|
|
447
|
+
const todo = stopTimer(todos, id);
|
|
448
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
449
|
+
else console.log(green("⏱ Timer stopped:") + ` #${todo.id} — ${formatTime(todo.timeLogged)} total`);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "log": {
|
|
453
|
+
const minutes = positional[2];
|
|
454
|
+
const todo = logTime(todos, id, minutes);
|
|
455
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
456
|
+
else console.log(green("✓ Time logged:") + ` #${todo.id} — ${formatTime(todo.timeLogged)} total`);
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
default:
|
|
460
|
+
console.log(red(`✗ Usage: todo timer [start|stop|log] <id> [minutes]`));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// --- Dependency commands ---------------------------------------------------
|
|
465
|
+
function cmdDep(todos, { positional }) {
|
|
466
|
+
const subcmd = positional[0];
|
|
467
|
+
const id = positional[1];
|
|
468
|
+
if (!subcmd || !id) {
|
|
469
|
+
console.log(red("✗ Usage: todo dep <add|remove> <todoId> <depId>"));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
switch (subcmd) {
|
|
473
|
+
case "add": {
|
|
474
|
+
const todo = addDependency(todos, id, positional[2]);
|
|
475
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
476
|
+
else console.log(green("✓ Dependency added:") + ` #${todo.id} depends on #${positional[2]}`);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case "remove":
|
|
480
|
+
case "rm": {
|
|
481
|
+
const todo = removeDependency(todos, id, positional[2]);
|
|
482
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
483
|
+
else console.log(green("✓ Dependency removed:") + ` #${todo.id} no longer depends on #${positional[2]}`);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
default:
|
|
487
|
+
console.log(red(`✗ Unknown dep command: ${subcmd}`));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// --- Recurring commands ----------------------------------------------------
|
|
492
|
+
function cmdRecur(todos, { positional }) {
|
|
493
|
+
const id = positional[0];
|
|
494
|
+
const freq = positional[1];
|
|
495
|
+
if (!id) {
|
|
496
|
+
console.log(red("✗ Usage: todo recur <id> <daily|weekly|monthly|yearly> [interval] (or: todo recur <id> off)"));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!freq || freq === "off" || freq === "none") {
|
|
500
|
+
const todo = removeRecurring(todos, id);
|
|
501
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
502
|
+
else console.log(green("✓ Recurrence removed:") + ` #${todo.id}`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const interval = positional[2] ? Number(positional[2]) : 1;
|
|
506
|
+
const todo = setRecurring(todos, id, freq, interval);
|
|
507
|
+
if (!todo) console.log(red(`✗ No todo with id ${id}`));
|
|
508
|
+
else console.log(green("✓ Recurrence set:") + ` #${todo.id} every ${todo.recurring.interval} ${todo.recurring.freq}(s)`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// --- Bulk commands ---------------------------------------------------------
|
|
512
|
+
function cmdBulk(todos, { positional, flags }) {
|
|
513
|
+
const subcmd = positional[0];
|
|
514
|
+
const ids = positional.slice(1).map(Number).filter((n) => !isNaN(n));
|
|
515
|
+
if (!subcmd || ids.length === 0) {
|
|
516
|
+
console.log(red("✗ Usage: todo bulk <done|delete|priority|category|assign> <ids...> [value]"));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
switch (subcmd) {
|
|
520
|
+
case "done":
|
|
521
|
+
case "complete": {
|
|
522
|
+
const n = bulkComplete(todos, ids);
|
|
523
|
+
console.log(green(`✓ Completed ${n} item(s)`));
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
case "delete":
|
|
527
|
+
case "rm": {
|
|
528
|
+
const n = bulkDelete(todos, ids);
|
|
529
|
+
console.log(green(`✓ Deleted ${n} item(s)`));
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case "priority":
|
|
533
|
+
case "p": {
|
|
534
|
+
const p = flags.priority || ids.shift();
|
|
535
|
+
const n = bulkPriority(todos, ids, p);
|
|
536
|
+
console.log(green(`✓ Updated priority on ${n} item(s)`));
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case "category":
|
|
540
|
+
case "c": {
|
|
541
|
+
const c = flags.category;
|
|
542
|
+
const n = bulkCategory(todos, ids, c);
|
|
543
|
+
console.log(green(`✓ Updated category on ${n} item(s)`));
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case "assign": {
|
|
547
|
+
const a = flags.assignee;
|
|
548
|
+
const n = bulkAssign(todos, ids, a);
|
|
549
|
+
console.log(green(`✓ Updated assignee on ${n} item(s)`));
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
default:
|
|
553
|
+
console.log(red(`✗ Unknown bulk command: ${subcmd}`));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// --- Web server command ----------------------------------------------------
|
|
558
|
+
function cmdWeb(todos, { positional }) {
|
|
559
|
+
const port = Number(positional[0]) || 3456;
|
|
560
|
+
import("./server.js").then(({ startServer }) => startServer(port));
|
|
561
|
+
}
|
|
562
|
+
|
|
252
563
|
function cmdUpdate(_todos, { flags }) {
|
|
253
564
|
const tmpDir = path.join(os.tmpdir(), "todo-cli-update");
|
|
254
565
|
const run = (cmd) => execSync(cmd, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
@@ -257,7 +568,6 @@ function cmdUpdate(_todos, { flags }) {
|
|
|
257
568
|
|
|
258
569
|
let latestVersion;
|
|
259
570
|
try {
|
|
260
|
-
// Clone or pull the repo into a temp dir.
|
|
261
571
|
if (fs.existsSync(path.join(tmpDir, ".git"))) {
|
|
262
572
|
console.log(dim(" pulling latest..."));
|
|
263
573
|
run(`git -C ${tmpDir} pull origin main`);
|
|
@@ -311,18 +621,49 @@ ${bold("USAGE")}
|
|
|
311
621
|
${bold("COMMANDS")}
|
|
312
622
|
add <title> Add a new todo
|
|
313
623
|
list List todos (default command)
|
|
624
|
+
show <id> Show detailed info for a todo
|
|
314
625
|
done <id> Mark a todo as done (done --all = complete every pending)
|
|
315
626
|
toggle <id> Toggle done / pending
|
|
316
627
|
edit <id> Edit a todo (use --title, -p, -t, -c, -d, --note)
|
|
317
628
|
delete <id> Delete a todo (alias: rm, remove)
|
|
318
|
-
|
|
629
|
+
duplicate <id> Clone a todo (alias: dup)
|
|
630
|
+
pin <id> Toggle pin status
|
|
631
|
+
search <keyword> Search title / tags / category / note / assignee
|
|
319
632
|
stats Show summary statistics
|
|
320
633
|
export [json|csv|md] Export all todos to stdout
|
|
321
634
|
archive Remove all completed todos
|
|
322
|
-
update Update todo-cli to the latest version (--check = check only)
|
|
323
635
|
clear Remove ALL todos
|
|
636
|
+
web [port] Start the web UI server (default port: 3456)
|
|
637
|
+
update Update todo-cli to the latest version (--check = check only)
|
|
324
638
|
help Show this help
|
|
325
639
|
|
|
640
|
+
${bold("SUBTASKS")}
|
|
641
|
+
subtask add <id> <title> Add a subtask
|
|
642
|
+
subtask done <id> <sid> Toggle subtask done
|
|
643
|
+
subtask edit <id> <sid> <title> Edit subtask title
|
|
644
|
+
subtask delete <id> <sid> Delete a subtask
|
|
645
|
+
|
|
646
|
+
${bold("RECURRING")}
|
|
647
|
+
recur <id> <daily|weekly|monthly|yearly> [interval] Set recurrence
|
|
648
|
+
recur <id> off Remove recurrence
|
|
649
|
+
|
|
650
|
+
${bold("DEPENDENCIES")}
|
|
651
|
+
dep add <id> <depId> Add dependency (blocks completion)
|
|
652
|
+
dep remove <id> <depId> Remove dependency
|
|
653
|
+
|
|
654
|
+
${bold("TIME TRACKING")}
|
|
655
|
+
timer start <id> Start timer (stops any other running timer)
|
|
656
|
+
timer stop <id> Stop timer and log elapsed time
|
|
657
|
+
timer log <id> <minutes> Manually log time
|
|
658
|
+
timer Show active timer
|
|
659
|
+
|
|
660
|
+
${bold("BULK OPERATIONS")}
|
|
661
|
+
bulk done <ids...> Complete multiple todos
|
|
662
|
+
bulk delete <ids...> Delete multiple todos
|
|
663
|
+
bulk priority <ids...> -p <val> Set priority on multiple todos
|
|
664
|
+
bulk category <ids...> -c <val> Set category on multiple todos
|
|
665
|
+
bulk assign <ids...> --assignee <val> Set assignee on multiple todos
|
|
666
|
+
|
|
326
667
|
${bold("GLOBAL FLAGS")}
|
|
327
668
|
-h, --help Show help (works with any command)
|
|
328
669
|
-v, --version Show version
|
|
@@ -336,25 +677,44 @@ ${bold("FLAGS")}
|
|
|
336
677
|
--title <v> New title (for edit)
|
|
337
678
|
--status <v> Filter: all | done | pending (list only)
|
|
338
679
|
--tag <v> Filter by tag (list only)
|
|
339
|
-
--sort <v> Sort: priority | created | due | id
|
|
680
|
+
--sort <v> Sort: priority | created | due | id | title | updated | order
|
|
681
|
+
--assignee <v> Filter/set assignee
|
|
682
|
+
--estimate <v> Estimated minutes to complete
|
|
683
|
+
--color <v> Custom color label
|
|
340
684
|
--all Complete all pending (done only)
|
|
685
|
+
--pinned Pin the new todo (add only)
|
|
686
|
+
--overdue Show only overdue items (list only)
|
|
687
|
+
--due-today Show only items due today (list only)
|
|
341
688
|
--check Check for update without installing (update only)
|
|
342
689
|
--format <v> Export format json|csv|md (export only)
|
|
343
690
|
|
|
344
691
|
${bold("EXAMPLES")}
|
|
345
692
|
todo add "Buy groceries" -p high -t shopping,weekend -c personal -d 2025-01-15
|
|
346
|
-
todo add "Write report" --note "Focus on Q4 metrics" -p high -c work
|
|
693
|
+
todo add "Write report" --note "Focus on Q4 metrics" -p high -c work --estimate 120
|
|
694
|
+
todo add "Team meeting" --assignee alice -p medium
|
|
347
695
|
todo list --status pending --sort due
|
|
696
|
+
todo list --overdue
|
|
697
|
+
todo show 3
|
|
348
698
|
todo search report
|
|
349
699
|
todo done 3
|
|
350
700
|
todo done --all
|
|
351
701
|
todo edit 2 --title "Updated" -p low -d 2025-02-01
|
|
702
|
+
todo pin 1
|
|
703
|
+
todo duplicate 3
|
|
704
|
+
todo subtask add 1 "Research options"
|
|
705
|
+
todo subtask done 1 1
|
|
706
|
+
todo recur 1 weekly 2
|
|
707
|
+
todo dep add 3 1
|
|
708
|
+
todo timer start 2
|
|
709
|
+
todo timer stop 2
|
|
710
|
+
todo timer log 2 30
|
|
711
|
+
todo bulk done 1 2 3
|
|
712
|
+
todo bulk priority 1 2 3 -p high
|
|
352
713
|
todo stats
|
|
353
714
|
todo export csv
|
|
354
715
|
todo archive
|
|
716
|
+
todo web 8080
|
|
355
717
|
todo update --check
|
|
356
|
-
todo update
|
|
357
|
-
todo delete 5
|
|
358
718
|
`
|
|
359
719
|
);
|
|
360
720
|
}
|
|
@@ -364,30 +724,38 @@ const COMMANDS = {
|
|
|
364
724
|
add: cmdAdd,
|
|
365
725
|
list: cmdList,
|
|
366
726
|
ls: cmdList,
|
|
727
|
+
show: cmdShow,
|
|
367
728
|
done: cmdDone,
|
|
368
729
|
toggle: cmdToggle,
|
|
369
730
|
edit: cmdEdit,
|
|
370
731
|
delete: cmdDelete,
|
|
371
732
|
rm: cmdDelete,
|
|
372
733
|
remove: cmdDelete,
|
|
734
|
+
duplicate: cmdDuplicate,
|
|
735
|
+
dup: cmdDuplicate,
|
|
736
|
+
pin: cmdPin,
|
|
373
737
|
search: cmdSearch,
|
|
374
738
|
stats: cmdStats,
|
|
375
739
|
export: cmdExport,
|
|
376
740
|
archive: cmdArchive,
|
|
377
741
|
update: cmdUpdate,
|
|
378
742
|
clear: cmdClear,
|
|
743
|
+
subtask: cmdSubtask,
|
|
744
|
+
recur: cmdRecur,
|
|
745
|
+
dep: cmdDep,
|
|
746
|
+
timer: cmdTimer,
|
|
747
|
+
bulk: cmdBulk,
|
|
748
|
+
web: cmdWeb,
|
|
379
749
|
help: cmdHelp,
|
|
380
750
|
};
|
|
381
751
|
|
|
382
752
|
/** Main entry point. argv should be process.argv.slice(2). */
|
|
383
753
|
export function run(argv) {
|
|
384
|
-
// -h / --help anywhere (including bare `todo --help`) shows help and exits.
|
|
385
754
|
if (argv.some((a) => a === "-h" || a === "--help")) {
|
|
386
755
|
cmdHelp();
|
|
387
756
|
return;
|
|
388
757
|
}
|
|
389
758
|
|
|
390
|
-
// -v / --version prints the version (read from package.json) and exits.
|
|
391
759
|
if (argv.some((a) => a === "-v" || a === "--version")) {
|
|
392
760
|
console.log(`todo v${VERSION}`);
|
|
393
761
|
return;
|
|
@@ -395,6 +763,7 @@ export function run(argv) {
|
|
|
395
763
|
|
|
396
764
|
const store = createStore();
|
|
397
765
|
const todos = store.load();
|
|
766
|
+
migrateAll(todos);
|
|
398
767
|
|
|
399
768
|
const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
|
|
400
769
|
const rest = command === "list" && argv[0] && !argv[0].startsWith("-")
|
|
@@ -411,7 +780,10 @@ export function run(argv) {
|
|
|
411
780
|
|
|
412
781
|
try {
|
|
413
782
|
handler(todos, parsed);
|
|
414
|
-
|
|
783
|
+
// Don't save if the server is running (it manages its own saves).
|
|
784
|
+
if (command !== "web" && command !== "update") {
|
|
785
|
+
store.save(todos);
|
|
786
|
+
}
|
|
415
787
|
} catch (err) {
|
|
416
788
|
console.error(red(`✗ ${err.message}`));
|
|
417
789
|
process.exitCode = 1;
|