@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/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 cols = hasDue
96
- ? ["ID", "Status", "Priority", "Due", "Category", "Tags", "Title"]
97
- : ["ID", "Status", "Priority", "Category", "Tags", "Title"];
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: ${s.total}`);
229
- console.log(` ${green("Done:")} ${s.done}`);
230
- console.log(` ${yellow("Pending:")} ${s.pending}`);
231
- if (s.overdue > 0) console.log(` ${red("Overdue:")} ${s.overdue}`);
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
- search <keyword> Search title / tags / category / note
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 (default: priority)
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
- store.save(todos);
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;