import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createStore } from "./store.js";
import {
addTodo,
editTodo,
deleteTodo,
toggleTodo,
completeTodo,
completeAll,
listTodos,
clearTodos,
searchTodos,
getStats,
archiveCompleted,
exportTodos,
dueStatus,
PRIORITIES,
migrateAll,
getTodo,
addSubtask,
toggleSubtask,
deleteSubtask,
editSubtask,
setRecurring,
removeRecurring,
addDependency,
removeDependency,
getBlockedBy,
startTimer,
stopTimer,
logTime,
getActiveTimer,
togglePin,
duplicateTodo,
bulkComplete,
bulkDelete,
bulkPriority,
bulkCategory,
bulkAssign,
formatTime,
} from "./todo.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const __pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
);
const VERSION = __pkg.version;
const REPO_URL = "https://gitcode.com/hw_aishell_projects/todo_cli.git";
const useColor = process.stdout.isTTY;
function c(code, str) {
return useColor ? `\x1b[${code}m${str}\x1b[0m` : str;
}
const green = (s) => c("32", s);
const red = (s) => c("31", s);
const yellow = (s) => c("33", s);
const cyan = (s) => c("36", s);
const magenta = (s) => c("35", s);
const blue = (s) => c("34", s);
const bold = (s) => c("1", s);
const dim = (s) => c("2", s);
const PRIORITY_COLOR = { high: red, medium: yellow, low: green };
function parseArgs(argv) {
const positional = [];
const flags = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "-p" || a === "--priority") flags.priority = argv[++i];
else if (a === "-t" || a === "--tags") flags.tags = argv[++i];
else if (a === "-c" || a === "--category") flags.category = argv[++i];
else if (a === "-d" || a === "--due") flags.due = argv[++i];
else if (a === "--note") flags.note = argv[++i];
else if (a === "--title") flags.title = argv[++i];
else if (a === "--status") flags.status = argv[++i];
else if (a === "--tag") flags.tag = argv[++i];
else if (a === "--sort") flags.sort = argv[++i];
else if (a === "--format") flags.format = argv[++i];
else if (a === "--assignee") flags.assignee = argv[++i];
else if (a === "--estimate") flags.estimate = argv[++i];
else if (a === "--color") flags.color = argv[++i];
else if (a === "--all") flags.all = true;
else if (a === "--check") flags.check = true;
else if (a === "--pinned") flags.pinned = true;
else if (a === "--overdue") flags.overdue = true;
else if (a === "--due-today") flags.dueToday = true;
else if (a === "-h" || a === "--help") flags.help = true;
else if (a.startsWith("-")) {
} else {
positional.push(a);
}
}
return { positional, flags };
}
function pad(str, width) {
str = String(str);
return str.length >= width ? str : str + " ".repeat(width - str.length);
}
function renderTable(todos) {
if (todos.length === 0) {
console.log(dim(" (no todos)"));
return;
}
const hasDue = todos.some((t) => t.due);
const hasAssignee = todos.some((t) => t.assignee);
const cols = ["ID", "Status", "Priority"];
if (hasDue) cols.push("Due");
cols.push("Category", "Tags");
if (hasAssignee) cols.push("Assignee");
cols.push("Title");
function dueCell(t) {
if (!t.due) return "-";
const ds = dueStatus(t.due);
if (t.done) return dim(t.due);
if (ds === "overdue") return red(t.due);
if (ds === "today") return yellow(t.due);
if (ds === "soon") return yellow(t.due);
return t.due;
}
const rows = todos.map((t) => {
const base = [
String(t.id),
t.done ? green("✓") : dim("○"),
PRIORITY_COLOR[t.priority](t.priority),
];
if (hasDue) base.push(dueCell(t));
base.push(
t.category || "-",
t.tags.length ? cyan(t.tags.join(", ")) : "-"
);
if (hasAssignee) base.push(t.assignee || "-");
const titleParts = [];
if (t.pinned) titleParts.push(magenta("📌"));
if (t.recurring) titleParts.push(blue("⟳"));
if (t.timerStartedAt) titleParts.push(green("⏱"));
if (t.subtasks && t.subtasks.length) {
const done = t.subtasks.filter((s) => s.done).length;
titleParts.push(dim(`[${done}/${t.subtasks.length}]`));
}
const blocked = getBlockedBy(todos, t.id);
if (blocked.length) titleParts.push(red(`⛔${blocked.length}`));
titleParts.push(t.done ? dim(t.title) : t.title);
base.push(titleParts.join(" "));
return base;
});
const widths = cols.map((_, ci) =>
Math.max(cols[ci].length, ...rows.map((r) => r[ci].length))
);
const header = " " + cols.map((h, i) => bold(pad(h, widths[i]))).join(" ");
console.log(header);
console.log(" " + dim("-".repeat(widths.reduce((a, w) => a + w + 2, -2))));
for (const row of rows) {
console.log(" " + row.map((cell, i) => pad(cell, widths[i])).join(" "));
}
console.log(dim(`\n ${todos.length} item(s)`));
}
function showTodoDetail(todos, t) {
console.log(bold(` #${t.id} ${t.title}`));
console.log(dim(" " + "─".repeat(50)));
console.log(` Status: ${t.done ? green("✓ done") : yellow("○ pending")}`);
console.log(` Priority: ${PRIORITY_COLOR[t.priority](t.priority)}`);
if (t.category) console.log(` Category: ${t.category}`);
if (t.tags.length) console.log(` Tags: ${cyan(t.tags.join(", "))}`);
if (t.due) {
const ds = dueStatus(t.due);
const label = ds === "overdue" ? red(" (overdue)") : ds === "today" ? yellow(" (today)") : ds === "soon" ? yellow(" (soon)") : "";
console.log(` Due: ${t.due}${label}`);
}
if (t.note) console.log(` Note: ${t.note}`);
if (t.assignee) console.log(` Assignee: ${t.assignee}`);
if (t.estimate) console.log(` Estimate: ${formatTime(t.estimate)}`);
if (t.timeLogged) console.log(` Time logged:${formatTime(t.timeLogged)}`);
if (t.pinned) console.log(` Pinned: ${magenta("yes")}`);
if (t.recurring) console.log(` Recurring: ${blue(`every ${t.recurring.interval} ${t.recurring.freq}(s)`)}`);
if (t.timerStartedAt) {
const elapsed = Math.round((Date.now() - new Date(t.timerStartedAt).getTime()) / 60000);
console.log(` Timer: ${green("running")} (${formatTime(elapsed)} elapsed)`);
}
if (t.subtasks && t.subtasks.length) {
console.log(" Subtasks:");
for (const s of t.subtasks) {
console.log(` ${s.done ? green("✓") : dim("○")} #${s.id} ${s.done ? dim(s.title) : s.title}`);
}
}
if (t.dependsOn && t.dependsOn.length) {
console.log(" Depends on:");
for (const d of t.dependsOn) {
const dep = getTodo(todos, d);
const status = dep ? (dep.done ? green("✓") : red("✗")) : dim("?");
console.log(` ${status} #${d} ${dep ? dep.title : "(deleted)"}`);
}
}
console.log(dim(` Created: ${t.createdAt}`));
if (t.updatedAt) console.log(dim(` Updated: ${t.updatedAt}`));
if (t.completedAt) console.log(dim(` Completed: ${t.completedAt}`));
}
function cmdAdd(todos, { positional, flags }) {
const title = positional.join(" ");
const todo = addTodo(todos, {
title,
priority: flags.priority,
tags: flags.tags,
category: flags.category,
due: flags.due,
note: flags.note,
estimate: flags.estimate,
assignee: flags.assignee,
color: flags.color,
pinned: flags.pinned,
});
let msg = green("✓ Added:") + ` #${todo.id} ${todo.title}`;
if (todo.due) msg += dim(` (due ${todo.due})`);
console.log(msg);
}
function cmdList(todos, { flags }) {
const result = listTodos(todos, {
status: flags.status,
category: flags.category,
tag: flags.tag,
priority: flags.priority,
assignee: flags.assignee,
sort: flags.sort,
overdue: flags.overdue,
dueToday: flags.dueToday,
pinnedFirst: true,
});
renderTable(result);
}
function cmdShow(todos, { positional }) {
const id = positional[0];
const t = getTodo(todos, id);
if (!t) { console.log(red(`✗ No todo with id ${id}`)); return; }
showTodoDetail(todos, t);
}
function cmdDone(todos, { positional, flags }) {
if (flags.all) {
const n = completeAll(todos);
console.log(green(`✓ Marked ${n} pending item(s) as done`));
return;
}
const id = positional[0];
if (!id) {
console.log(red("✗ Usage: todo done <id> (or todo done --all)"));
return;
}
const todo = completeTodo(todos, id);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Done:") + ` #${todo.id} ${todo.title}`);
}
function cmdToggle(todos, { positional }) {
const id = positional[0];
const todo = toggleTodo(todos, id);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else
console.log(
(todo.done ? green("✓ Done:") : yellow("◌ Reopened:")) +
` #${todo.id} ${todo.title}`
);
}
function cmdEdit(todos, { positional, flags }) {
const id = positional[0];
const todo = editTodo(todos, id, {
title: flags.title,
priority: flags.priority,
tags: flags.tags,
category: flags.category,
due: flags.due,
note: flags.note,
estimate: flags.estimate,
assignee: flags.assignee,
color: flags.color,
});
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Edited:") + ` #${todo.id} ${todo.title}`);
}
function cmdDelete(todos, { positional }) {
const id = positional[0];
const ok = deleteTodo(todos, id);
if (!ok) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Deleted:") + ` #${id}`);
}
function cmdClear(todos) {
const n = clearTodos(todos);
console.log(green(`✓ Cleared ${n} item(s)`));
}
function cmdSearch(todos, { positional }) {
const keyword = positional.join(" ");
const results = searchTodos(todos, keyword);
console.log(bold(`Search:`) + ` "${keyword}" — ${results.length} match(es)`);
renderTable(results);
}
function cmdStats(todos) {
const s = getStats(todos);
console.log(bold("Todo Statistics"));
console.log(dim("─────────────────"));
console.log(` Total: ${s.total}`);
console.log(` ${green("Done:")} ${s.done}`);
console.log(` ${yellow("Pending:")} ${s.pending}`);
console.log(` Completion: ${s.completionRate}%`);
if (s.overdue > 0) console.log(` ${red("Overdue:")} ${s.overdue}`);
if (s.dueToday > 0) console.log(` ${yellow("Due today:")} ${s.dueToday}`);
if (s.pinned > 0) console.log(` ${magenta("Pinned:")} ${s.pinned}`);
if (s.totalTimeLogged > 0) console.log(` Time logged: ${formatTime(s.totalTimeLogged)}`);
console.log(dim("\n By priority:"));
console.log(` ${red("high")}: ${s.byPriority.high} ${yellow("medium")}: ${s.byPriority.medium} ${green("low")}: ${s.byPriority.low}`);
const cats = Object.keys(s.byCategory);
if (cats.length) {
console.log(dim(" By category:"));
for (const c of cats.sort()) console.log(` ${c}: ${s.byCategory[c]}`);
}
const assignees = Object.keys(s.byAssignee);
if (assignees.length) {
console.log(dim(" By assignee:"));
for (const a of assignees.sort()) console.log(` ${a}: ${s.byAssignee[a]}`);
}
}
function cmdExport(todos, { positional, flags }) {
const format = (positional[0] || flags.format || "json").toLowerCase();
const out = exportTodos(todos, format);
console.log(out);
}
function cmdArchive(todos) {
const n = archiveCompleted(todos);
console.log(green(`✓ Archived (removed) ${n} completed item(s)`));
}
function cmdSubtask(todos, { positional }) {
const subcmd = positional[0];
const id = positional[1];
if (!subcmd || !id) {
console.log(red("✗ Usage: todo subtask <add|done|delete|edit> <todoId> [args]"));
return;
}
switch (subcmd) {
case "add": {
const title = positional.slice(2).join(" ");
const st = addSubtask(todos, id, title);
if (!st) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Subtask added:") + ` #${st.id} ${st.title}`);
break;
}
case "done":
case "toggle": {
const st = toggleSubtask(todos, id, positional[2]);
if (!st) console.log(red(`✗ Subtask not found`));
else console.log(green("✓ Subtask:") + ` #${st.id} ${st.done ? "done" : "pending"}`);
break;
}
case "delete":
case "rm": {
const ok = deleteSubtask(todos, id, positional[2]);
if (!ok) console.log(red("✗ Subtask not found"));
else console.log(green("✓ Subtask deleted"));
break;
}
case "edit": {
const title = positional.slice(3).join(" ");
const st = editSubtask(todos, id, positional[2], title);
if (!st) console.log(red("✗ Subtask not found"));
else console.log(green("✓ Subtask edited:") + ` #${st.id} ${st.title}`);
break;
}
default:
console.log(red(`✗ Unknown subtask command: ${subcmd}`));
}
}
function cmdPin(todos, { positional }) {
const todo = togglePin(todos, positional[0]);
if (!todo) console.log(red(`✗ No todo with id ${positional[0]}`));
else console.log(green("✓") + ` #${todo.id} ${todo.pinned ? "pinned" : "unpinned"}`);
}
function cmdDuplicate(todos, { positional }) {
const todo = duplicateTodo(todos, positional[0]);
if (!todo) console.log(red(`✗ No todo with id ${positional[0]}`));
else console.log(green("✓ Duplicated:") + ` #${todo.id} ${todo.title}`);
}
function cmdTimer(todos, { positional }) {
const subcmd = positional[0];
const id = positional[1];
if (!subcmd) {
const active = getActiveTimer(todos);
if (active) {
const elapsed = Math.round((Date.now() - new Date(active.timerStartedAt).getTime()) / 60000);
console.log(green("⏱ Active timer:") + ` #${active.id} ${active.title} — ${formatTime(elapsed)} elapsed`);
} else {
console.log(dim(" No active timer"));
}
return;
}
switch (subcmd) {
case "start": {
const todo = startTimer(todos, id);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("⏱ Timer started:") + ` #${todo.id} ${todo.title}`);
break;
}
case "stop": {
const todo = stopTimer(todos, id);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("⏱ Timer stopped:") + ` #${todo.id} — ${formatTime(todo.timeLogged)} total`);
break;
}
case "log": {
const minutes = positional[2];
const todo = logTime(todos, id, minutes);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Time logged:") + ` #${todo.id} — ${formatTime(todo.timeLogged)} total`);
break;
}
default:
console.log(red(`✗ Usage: todo timer [start|stop|log] <id> [minutes]`));
}
}
function cmdDep(todos, { positional }) {
const subcmd = positional[0];
const id = positional[1];
if (!subcmd || !id) {
console.log(red("✗ Usage: todo dep <add|remove> <todoId> <depId>"));
return;
}
switch (subcmd) {
case "add": {
const todo = addDependency(todos, id, positional[2]);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Dependency added:") + ` #${todo.id} depends on #${positional[2]}`);
break;
}
case "remove":
case "rm": {
const todo = removeDependency(todos, id, positional[2]);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Dependency removed:") + ` #${todo.id} no longer depends on #${positional[2]}`);
break;
}
default:
console.log(red(`✗ Unknown dep command: ${subcmd}`));
}
}
function cmdRecur(todos, { positional }) {
const id = positional[0];
const freq = positional[1];
if (!id) {
console.log(red("✗ Usage: todo recur <id> <daily|weekly|monthly|yearly> [interval] (or: todo recur <id> off)"));
return;
}
if (!freq || freq === "off" || freq === "none") {
const todo = removeRecurring(todos, id);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Recurrence removed:") + ` #${todo.id}`);
return;
}
const interval = positional[2] ? Number(positional[2]) : 1;
const todo = setRecurring(todos, id, freq, interval);
if (!todo) console.log(red(`✗ No todo with id ${id}`));
else console.log(green("✓ Recurrence set:") + ` #${todo.id} every ${todo.recurring.interval} ${todo.recurring.freq}(s)`);
}
function cmdBulk(todos, { positional, flags }) {
const subcmd = positional[0];
const ids = positional.slice(1).map(Number).filter((n) => !isNaN(n));
if (!subcmd || ids.length === 0) {
console.log(red("✗ Usage: todo bulk <done|delete|priority|category|assign> <ids...> [value]"));
return;
}
switch (subcmd) {
case "done":
case "complete": {
const n = bulkComplete(todos, ids);
console.log(green(`✓ Completed ${n} item(s)`));
break;
}
case "delete":
case "rm": {
const n = bulkDelete(todos, ids);
console.log(green(`✓ Deleted ${n} item(s)`));
break;
}
case "priority":
case "p": {
const p = flags.priority || ids.shift();
const n = bulkPriority(todos, ids, p);
console.log(green(`✓ Updated priority on ${n} item(s)`));
break;
}
case "category":
case "c": {
const c = flags.category;
const n = bulkCategory(todos, ids, c);
console.log(green(`✓ Updated category on ${n} item(s)`));
break;
}
case "assign": {
const a = flags.assignee;
const n = bulkAssign(todos, ids, a);
console.log(green(`✓ Updated assignee on ${n} item(s)`));
break;
}
default:
console.log(red(`✗ Unknown bulk command: ${subcmd}`));
}
}
function cmdWeb(todos, { positional }) {
const port = Number(positional[0]) || 3456;
import("./server.js").then(({ startServer }) => startServer(port));
}
function cmdUpdate(_todos, { flags }) {
const tmpDir = path.join(os.tmpdir(), "todo-cli-update");
const run = (cmd) => execSync(cmd, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
console.log(bold("todo update") + dim(" — checking for updates...\n"));
let latestVersion;
try {
if (fs.existsSync(path.join(tmpDir, ".git"))) {
console.log(dim(" pulling latest..."));
run(`git -C ${tmpDir} pull origin main`);
} else {
console.log(dim(" cloning repository..."));
fs.rmSync(tmpDir, { recursive: true, force: true });
run(`git clone --depth 1 ${REPO_URL} ${tmpDir}`);
}
latestVersion = JSON.parse(
fs.readFileSync(path.join(tmpDir, "package.json"), "utf8")
).version;
} catch (err) {
console.error(red(`✗ Failed to fetch latest version: ${err.message}`));
process.exitCode = 1;
return;
}
console.log(` ${dim("current:")} v${VERSION}`);
console.log(` ${dim("latest:")} v${latestVersion}`);
if (latestVersion === VERSION) {
console.log(green("\n✓ Already up to date."));
return;
}
if (flags.check) {
console.log(yellow(`\n⬆ Update available: v${VERSION} → v${latestVersion}`));
console.log(dim(" Run `todo update` to install."));
return;
}
try {
console.log(dim("\n installing..."));
execSync(`npm install -g ${tmpDir}`, { stdio: "inherit" });
console.log(green(`\n✓ Updated: v${VERSION} → v${latestVersion}`));
} catch (err) {
console.error(red(`\n✗ Install failed: ${err.message}`));
process.exitCode = 1;
}
}
function cmdHelp() {
console.log(
bold("todo") +
dim(" — a CLI todo list manager\n") +
`
${bold("USAGE")}
todo <command> [args] [flags]
${bold("COMMANDS")}
add <title> Add a new todo
list List todos (default command)
show <id> Show detailed info for a todo
done <id> Mark a todo as done (done --all = complete every pending)
toggle <id> Toggle done / pending
edit <id> Edit a todo (use --title, -p, -t, -c, -d, --note)
delete <id> Delete a todo (alias: rm, remove)
duplicate <id> Clone a todo (alias: dup)
pin <id> Toggle pin status
search <keyword> Search title / tags / category / note / assignee
stats Show summary statistics
export [json|csv|md] Export all todos to stdout
archive Remove all completed todos
clear Remove ALL todos
web [port] Start the web UI server (default port: 3456)
update Update todo-cli to the latest version (--check = check only)
help Show this help
${bold("SUBTASKS")}
subtask add <id> <title> Add a subtask
subtask done <id> <sid> Toggle subtask done
subtask edit <id> <sid> <title> Edit subtask title
subtask delete <id> <sid> Delete a subtask
${bold("RECURRING")}
recur <id> <daily|weekly|monthly|yearly> [interval] Set recurrence
recur <id> off Remove recurrence
${bold("DEPENDENCIES")}
dep add <id> <depId> Add dependency (blocks completion)
dep remove <id> <depId> Remove dependency
${bold("TIME TRACKING")}
timer start <id> Start timer (stops any other running timer)
timer stop <id> Stop timer and log elapsed time
timer log <id> <minutes> Manually log time
timer Show active timer
${bold("BULK OPERATIONS")}
bulk done <ids...> Complete multiple todos
bulk delete <ids...> Delete multiple todos
bulk priority <ids...> -p <val> Set priority on multiple todos
bulk category <ids...> -c <val> Set category on multiple todos
bulk assign <ids...> --assignee <val> Set assignee on multiple todos
${bold("GLOBAL FLAGS")}
-h, --help Show help (works with any command)
-v, --version Show version
${bold("FLAGS")}
-p, --priority <v> Priority: high | medium | low (default: medium)
-t, --tags <v> Comma-separated tags
-c, --category <v> Category name
-d, --due <v> Due date YYYY-MM-DD
--note <v> Longer note / description
--title <v> New title (for edit)
--status <v> Filter: all | done | pending (list only)
--tag <v> Filter by tag (list only)
--sort <v> Sort: priority | created | due | id | title | updated | order
--assignee <v> Filter/set assignee
--estimate <v> Estimated minutes to complete
--color <v> Custom color label
--all Complete all pending (done only)
--pinned Pin the new todo (add only)
--overdue Show only overdue items (list only)
--due-today Show only items due today (list only)
--check Check for update without installing (update only)
--format <v> Export format json|csv|md (export only)
${bold("EXAMPLES")}
todo add "Buy groceries" -p high -t shopping,weekend -c personal -d 2025-01-15
todo add "Write report" --note "Focus on Q4 metrics" -p high -c work --estimate 120
todo add "Team meeting" --assignee alice -p medium
todo list --status pending --sort due
todo list --overdue
todo show 3
todo search report
todo done 3
todo done --all
todo edit 2 --title "Updated" -p low -d 2025-02-01
todo pin 1
todo duplicate 3
todo subtask add 1 "Research options"
todo subtask done 1 1
todo recur 1 weekly 2
todo dep add 3 1
todo timer start 2
todo timer stop 2
todo timer log 2 30
todo bulk done 1 2 3
todo bulk priority 1 2 3 -p high
todo stats
todo export csv
todo archive
todo web 8080
todo update --check
`
);
}
const COMMANDS = {
add: cmdAdd,
list: cmdList,
ls: cmdList,
show: cmdShow,
done: cmdDone,
toggle: cmdToggle,
edit: cmdEdit,
delete: cmdDelete,
rm: cmdDelete,
remove: cmdDelete,
duplicate: cmdDuplicate,
dup: cmdDuplicate,
pin: cmdPin,
search: cmdSearch,
stats: cmdStats,
export: cmdExport,
archive: cmdArchive,
update: cmdUpdate,
clear: cmdClear,
subtask: cmdSubtask,
recur: cmdRecur,
dep: cmdDep,
timer: cmdTimer,
bulk: cmdBulk,
web: cmdWeb,
help: cmdHelp,
};
export function run(argv) {
if (argv.some((a) => a === "-h" || a === "--help")) {
cmdHelp();
return;
}
if (argv.some((a) => a === "-v" || a === "--version")) {
console.log(`todo v${VERSION}`);
return;
}
const store = createStore();
const todos = store.load();
migrateAll(todos);
const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
const rest = command === "list" && argv[0] && !argv[0].startsWith("-")
? argv.slice(1)
: (command === "list" ? argv : argv.slice(1));
const handler = COMMANDS[command] ?? cmdHelp;
const parsed = parseArgs(rest);
if (command === "help") {
cmdHelp();
return;
}
try {
handler(todos, parsed);
if (command !== "web" && command !== "update") {
store.save(todos);
}
} catch (err) {
console.error(red(`✗ ${err.message}`));
process.exitCode = 1;
}
}