@walknpm/todo-cli 1.1.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 ADDED
@@ -0,0 +1,203 @@
1
+ # todo-cli
2
+
3
+ > **English** | [简体中文](./README.zh-CN.md)
4
+
5
+ A zero-dependency Node.js CLI todo list manager. Full CRUD, completion toggle,
6
+ categories/tags, priority sorting, due dates with overdue highlighting, search,
7
+ statistics, and export (JSON / CSV / Markdown). Data persists to
8
+ `~/.todo-cli/todos.json`.
9
+
10
+ ## Requirements
11
+
12
+ - Node.js >= 18
13
+
14
+ ## Quick install
15
+
16
+ ```bash
17
+ git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
18
+ cd todo_cli
19
+ npm install -g .
20
+ todo --version # → todo v1.0.0
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ | Command | Description |
26
+ | --------------------- | -------------------------------------------------- |
27
+ | `add <title>` | Add a new todo |
28
+ | `list` | List todos (default command) |
29
+ | `done <id>` | Mark a todo as done (`done --all` = complete all) |
30
+ | `toggle <id>` | Toggle done / pending |
31
+ | `edit <id>` | Edit a todo |
32
+ | `delete <id>` | Delete a todo |
33
+ | `search <keyword>` | Search title / tags / category / note |
34
+ | `stats` | Show summary statistics |
35
+ | `export [json\|csv\|md]` | Export all todos to stdout |
36
+ | `archive` | Remove all completed todos |
37
+ | `update` | Update todo-cli to latest (`--check` = check only) |
38
+ | `clear` | Remove ALL todos |
39
+ | `help` | Show help |
40
+
41
+ `-h` / `--help` can be appended to **any** command (or used alone) to show help.
42
+ `-v` / `--version` prints the version.
43
+
44
+ ## Flags
45
+
46
+ | Flag | Applies to | Description |
47
+ | ----------------------- | ---------------- | ------------------------------------------ |
48
+ | `-p, --priority <v>` | add, edit | `high` \| `medium` \| `low` (default: medium) |
49
+ | `-t, --tags <v>` | add, edit | Comma-separated tags |
50
+ | `-c, --category <v>` | add, edit | Category name |
51
+ | `-d, --due <v>` | add, edit | Due date `YYYY-MM-DD` |
52
+ | `--note <v>` | add, edit | Longer note / description |
53
+ | `--title <v>` | edit | New title |
54
+ | `--status <v>` | list | `all` \| `done` \| `pending` |
55
+ | `--tag <v>` | list | Filter by tag |
56
+ | `--sort <v>` | list | `priority` \| `created` \| `due` \| `id` |
57
+ | `--all` | done | Complete all pending |
58
+ | `--check` | update | Check for update without installing |
59
+ | `--format <v>` | export | `json` \| `csv` \| `md` (default: json) |
60
+
61
+ ## Examples
62
+
63
+ ```bash
64
+ # Add with due date, priority, tags, category, and note
65
+ todo add "Submit tax return" -p high -d 2025-01-31 -c finance -t urgent,monthly --note "Include receipts"
66
+
67
+ # List & filter
68
+ todo list
69
+ todo list --status pending --sort due
70
+ todo list --category work --priority high
71
+
72
+ # Search
73
+ todo search tax
74
+
75
+ # Bulk complete & archive
76
+ todo done --all
77
+ todo archive
78
+
79
+ # Statistics
80
+ todo stats
81
+
82
+ # Auto-update
83
+ todo update --check # check for updates
84
+ todo update # install latest version
85
+
86
+ # Export
87
+ todo export csv > todos.csv
88
+ todo export md
89
+ ```
90
+
91
+ ## Project structure
92
+
93
+ ```
94
+ todo_cli/
95
+ ├── bin/
96
+ │ └── todo.js # executable entry point (shebang)
97
+ ├── src/
98
+ │ ├── store.js # JSON file persistence layer
99
+ │ ├── todo.js # domain logic (CRUD, search, stats, export, ...)
100
+ │ └── cli.js # command parsing, ANSI colours, table output
101
+ ├── package.json
102
+ ├── README.md # English documentation
103
+ └── README.zh-CN.md # Chinese documentation
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 🤖 AI 自动部署提示词
109
+
110
+ > 以下提示词可直接粘贴给 AI 助手(如 Claude、ChatGPT、DeepSeek 等),AI 会自动完成部署。
111
+ > 复制对应场景的代码块即可。
112
+
113
+ ### 场景一:从零安装并验证
114
+
115
+ ```
116
+ 请帮我安装部署 todo-cli 命令行待办工具,步骤如下:
117
+
118
+ 1. 克隆仓库:git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
119
+ 2. 进入目录:cd todo_cli
120
+ 3. 全局安装:npm install -g .
121
+ 4. 验证安装:运行 todo --version,确认输出 "todo v1.0.0"
122
+ 5. 功能验证:依次执行以下命令并确认每步输出正常:
123
+ - todo add "部署测试任务" -p high -t test -c verify
124
+ - todo list
125
+ - todo done 1
126
+ - todo stats
127
+ - todo clear
128
+ 6. 如果任何步骤报错,请展示完整错误信息并尝试修复。
129
+
130
+ 完成后告诉我安装路径和 todo --version 的输出。
131
+ ```
132
+
133
+ ### 场景二:安装 + 导入已有数据
134
+
135
+ ```
136
+ 请帮我部署 todo-cli 并导入已有待办数据:
137
+
138
+ 1. 克隆并安装:
139
+ git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
140
+ cd todo_cli && npm install -g .
141
+
142
+ 2. 确认 todo --version 正常输出。
143
+
144
+ 3. 以下是我的待办事项,请逐条用 todo add 命令创建(根据内容自动判断优先级、分类和标签):
145
+ - [在此粘贴你的待办列表,每行一条]
146
+
147
+ 4. 创建完成后运行 todo list 和 todo stats,把结果展示给我确认。
148
+ ```
149
+
150
+ ### 场景三:卸载
151
+
152
+ ```
153
+ 请帮我卸载 todo-cli 工具:
154
+
155
+ 1. 运行 npm uninstall -g todo-cli
156
+ 2. 删除数据文件:rm -rf ~/.todo-cli
157
+ 3. 运行 todo --version 确认已无法找到该命令
158
+ 4. 告诉我卸载是否成功。
159
+ ```
160
+
161
+ ### 场景四:更新到最新版本
162
+
163
+ ```
164
+ 请帮我更新 todo-cli 到最新版本:
165
+
166
+ 1. 运行 todo update --check 检查是否有新版本
167
+ 2. 如果有更新,运行 todo update 自动安装最新版
168
+ 3. 运行 todo --version 确认版本号已更新
169
+ 4. 运行 todo list 确认已有数据未丢失
170
+ 5. 告诉我更新结果。
171
+ ```
172
+
173
+ ### 场景五:完整部署(含数据备份与恢复)
174
+
175
+ ```
176
+ 请帮我完成 todo-cli 的完整部署流程:
177
+
178
+ 【安装】
179
+ 1. git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
180
+ 2. cd todo_cli && npm install -g .
181
+ 3. 确认 todo --version 输出版本号
182
+
183
+ 【备份】(如果 ~/.todo-cli/todos.json 已存在)
184
+ 4. 如果存在 ~/.todo-cli/todos.json,先备份到 ~/todo-backup-$(date +%Y%m%d).json
185
+ 5. 确认备份文件内容有效
186
+
187
+ 【验证】
188
+ 6. 执行 todo add "验证任务" -p medium 并确认输出
189
+ 7. 执行 todo list 确认表格正常显示
190
+ 8. 执行 todo delete 1 清理验证数据
191
+
192
+ 【恢复】(如果有备份)
193
+ 9. 如果之前做了备份,将备份文件复制回 ~/.todo-cli/todos.json
194
+ 10. 执行 todo list 确认数据已恢复
195
+
196
+ 最后汇总报告:安装状态、版本号、备份/恢复状态。
197
+ ```
198
+
199
+ ---
200
+
201
+ ## License
202
+
203
+ MIT
@@ -0,0 +1,202 @@
1
+ # todo-cli
2
+
3
+ > **简体中文** | [English](./README.md)
4
+
5
+ 一个零依赖的 Node.js 命令行待办事项管理器。支持完整的增删改查、完成状态切换、
6
+ 分类/标签、优先级排序、截止日期(含逾期高亮)、搜索、统计概览,以及导出
7
+ (JSON / CSV / Markdown)。数据持久化到 `~/.todo-cli/todos.json`。
8
+
9
+ ## 环境要求
10
+
11
+ - Node.js >= 18
12
+
13
+ ## 快速安装
14
+
15
+ ```bash
16
+ git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
17
+ cd todo_cli
18
+ npm install -g .
19
+ todo --version # → todo v1.0.0
20
+ ```
21
+
22
+ ## 命令一览
23
+
24
+ | 命令 | 说明 |
25
+ | --------------------- | -------------------------------------------------- |
26
+ | `add <标题>` | 添加新待办 |
27
+ | `list` | 列出待办(默认命令) |
28
+ | `done <id>` | 标记完成(`done --all` = 全部完成) |
29
+ | `toggle <id>` | 切换完成 / 待办状态 |
30
+ | `edit <id>` | 编辑待办 |
31
+ | `delete <id>` | 删除待办 |
32
+ | `search <关键词>` | 搜索标题 / 标签 / 分类 / 备注 |
33
+ | `stats` | 显示统计概览 |
34
+ | `export [json\|csv\|md]` | 导出全部待办到标准输出 |
35
+ | `archive` | 归档(删除所有已完成项) |
36
+ | `update` | 自动更新到最新版(`--check` = 仅检查不安装) |
37
+ | `clear` | 清空所有待办 |
38
+ | `help` | 显示帮助 |
39
+
40
+ `-h` / `--help` 可附加在**任意命令**后(或单独使用)显示帮助。
41
+ `-v` / `--version` 显示版本号。
42
+
43
+ ## 参数标志
44
+
45
+ | 标志 | 适用命令 | 说明 |
46
+ | ---------------------- | ---------------- | ------------------------------------------ |
47
+ | `-p, --priority <v>` | add, edit | `high` \| `medium` \| `low`(默认: medium) |
48
+ | `-t, --tags <v>` | add, edit | 逗号分隔的标签 |
49
+ | `-c, --category <v>` | add, edit | 分类名称 |
50
+ | `-d, --due <v>` | add, edit | 截止日期 `YYYY-MM-DD` |
51
+ | `--note <v>` | add, edit | 较长的备注 / 描述 |
52
+ | `--title <v>` | edit | 新标题 |
53
+ | `--status <v>` | list | `all` \| `done` \| `pending` |
54
+ | `--tag <v>` | list | 按标签过滤 |
55
+ | `--sort <v>` | list | `priority` \| `created` \| `due` \| `id` |
56
+ | `--all` | done | 完成所有待办项 |
57
+ | `--check` | update | 仅检查是否有更新,不安装 |
58
+ | `--format <v>` | export | `json` \| `csv` \| `md`(默认: json) |
59
+
60
+ ## 使用示例
61
+
62
+ ```bash
63
+ # 添加待办(含截止日期、优先级、标签、分类、备注)
64
+ todo add "提交报税单" -p high -d 2025-01-31 -c finance -t urgent,monthly --note "附上收据"
65
+
66
+ # 列出与过滤
67
+ todo list
68
+ todo list --status pending --sort due
69
+ todo list --category work --priority high
70
+
71
+ # 搜索
72
+ todo search 报税
73
+
74
+ # 批量完成与归档
75
+ todo done --all
76
+ todo archive
77
+
78
+ # 统计概览
79
+ todo stats
80
+
81
+ # 自动更新
82
+ todo update --check # 检查是否有新版本
83
+ todo update # 安装最新版本
84
+
85
+ # 导出
86
+ todo export csv > todos.csv
87
+ todo export md
88
+ ```
89
+
90
+ ## 项目结构
91
+
92
+ ```
93
+ todo_cli/
94
+ ├── bin/
95
+ │ └── todo.js # 可执行入口(shebang)
96
+ ├── src/
97
+ │ ├── store.js # JSON 文件持久化层
98
+ │ ├── todo.js # 领域逻辑(增删改查、搜索、统计、导出等)
99
+ │ └── cli.js # 命令解析、ANSI 彩色、表格输出
100
+ ├── package.json
101
+ ├── README.md # 英文文档
102
+ └── README.zh-CN.md # 中文文档
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 🤖 AI 自动部署提示词
108
+
109
+ > 以下提示词可直接粘贴给 AI 助手(如 Claude、ChatGPT、DeepSeek 等),AI 会自动完成部署。
110
+ > 复制对应场景的代码块即可。
111
+
112
+ ### 场景一:从零安装并验证
113
+
114
+ ```
115
+ 请帮我安装部署 todo-cli 命令行待办工具,步骤如下:
116
+
117
+ 1. 克隆仓库:git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
118
+ 2. 进入目录:cd todo_cli
119
+ 3. 全局安装:npm install -g .
120
+ 4. 验证安装:运行 todo --version,确认输出 "todo v1.0.0"
121
+ 5. 功能验证:依次执行以下命令并确认每步输出正常:
122
+ - todo add "部署测试任务" -p high -t test -c verify
123
+ - todo list
124
+ - todo done 1
125
+ - todo stats
126
+ - todo clear
127
+ 6. 如果任何步骤报错,请展示完整错误信息并尝试修复。
128
+
129
+ 完成后告诉我安装路径和 todo --version 的输出。
130
+ ```
131
+
132
+ ### 场景二:安装 + 导入已有数据
133
+
134
+ ```
135
+ 请帮我部署 todo-cli 并导入已有待办数据:
136
+
137
+ 1. 克隆并安装:
138
+ git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
139
+ cd todo_cli && npm install -g .
140
+
141
+ 2. 确认 todo --version 正常输出。
142
+
143
+ 3. 以下是我的待办事项,请逐条用 todo add 命令创建(根据内容自动判断优先级、分类和标签):
144
+ - [在此粘贴你的待办列表,每行一条]
145
+
146
+ 4. 创建完成后运行 todo list 和 todo stats,把结果展示给我确认。
147
+ ```
148
+
149
+ ### 场景三:卸载
150
+
151
+ ```
152
+ 请帮我卸载 todo-cli 工具:
153
+
154
+ 1. 运行 npm uninstall -g todo-cli
155
+ 2. 删除数据文件:rm -rf ~/.todo-cli
156
+ 3. 运行 todo --version 确认已无法找到该命令
157
+ 4. 告诉我卸载是否成功。
158
+ ```
159
+
160
+ ### 场景四:更新到最新版本
161
+
162
+ ```
163
+ 请帮我更新 todo-cli 到最新版本:
164
+
165
+ 1. 运行 todo update --check 检查是否有新版本
166
+ 2. 如果有更新,运行 todo update 自动安装最新版
167
+ 3. 运行 todo --version 确认版本号已更新
168
+ 4. 运行 todo list 确认已有数据未丢失
169
+ 5. 告诉我更新结果。
170
+ ```
171
+
172
+ ### 场景五:完整部署(含数据备份与恢复)
173
+
174
+ ```
175
+ 请帮我完成 todo-cli 的完整部署流程:
176
+
177
+ 【安装】
178
+ 1. git clone https://gitcode.com/hw_aishell_projects/todo_cli.git
179
+ 2. cd todo_cli && npm install -g .
180
+ 3. 确认 todo --version 输出版本号
181
+
182
+ 【备份】(如果 ~/.todo-cli/todos.json 已存在)
183
+ 4. 如果存在 ~/.todo-cli/todos.json,先备份到 ~/todo-backup-$(date +%Y%m%d).json
184
+ 5. 确认备份文件内容有效
185
+
186
+ 【验证】
187
+ 6. 执行 todo add "验证任务" -p medium 并确认输出
188
+ 7. 执行 todo list 确认表格正常显示
189
+ 8. 执行 todo delete 1 清理验证数据
190
+
191
+ 【恢复】(如果有备份)
192
+ 9. 如果之前做了备份,将备份文件复制回 ~/.todo-cli/todos.json
193
+ 10. 执行 todo list 确认数据已恢复
194
+
195
+ 最后汇总报告:安装状态、版本号、备份/恢复状态。
196
+ ```
197
+
198
+ ---
199
+
200
+ ## 开源协议
201
+
202
+ MIT
package/bin/todo.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // bin/todo.js — executable entry point for the todo CLI.
3
+ import { run } from "../src/cli.js";
4
+
5
+ run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@walknpm/todo-cli",
3
+ "version": "1.1.0",
4
+ "description": "A zero-dependency Node.js CLI todo list manager with CRUD, due dates, search, stats, export, and auto-update.",
5
+ "type": "module",
6
+ "main": "src/cli.js",
7
+ "bin": {
8
+ "todo": "bin/todo.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "start": "node bin/todo.js"
15
+ },
16
+ "keywords": [
17
+ "todo",
18
+ "cli",
19
+ "task",
20
+ "productivity",
21
+ "todo-list",
22
+ "command-line"
23
+ ],
24
+ "author": "walknpm",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://gitcode.com/hw_aishell_projects/todo_cli.git"
29
+ },
30
+ "homepage": "https://gitcode.com/hw_aishell_projects/todo_cli",
31
+ "files": [
32
+ "bin/",
33
+ "src/",
34
+ "README.md",
35
+ "README.zh-CN.md"
36
+ ]
37
+ }
package/src/cli.js ADDED
@@ -0,0 +1,419 @@
1
+ // cli.js — command-line interface for the todo list.
2
+ // Parses argv into commands, executes them against the domain + store,
3
+ // and prints results with ANSI colours and a table layout.
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ import { execSync } from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createStore } from "./store.js";
11
+ import {
12
+ addTodo,
13
+ editTodo,
14
+ deleteTodo,
15
+ toggleTodo,
16
+ completeTodo,
17
+ completeAll,
18
+ listTodos,
19
+ clearTodos,
20
+ searchTodos,
21
+ getStats,
22
+ archiveCompleted,
23
+ exportTodos,
24
+ dueStatus,
25
+ PRIORITIES,
26
+ } from "./todo.js";
27
+
28
+ // --- version & repo (read from package.json so they stay in sync) -----------
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ const __pkg = JSON.parse(
31
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
32
+ );
33
+ const VERSION = __pkg.version;
34
+ const REPO_URL = "https://gitcode.com/hw_aishell_projects/todo_cli.git";
35
+
36
+ // --- ANSI colours (disabled when not a TTY) -------------------------------
37
+ const useColor = process.stdout.isTTY;
38
+ function c(code, str) {
39
+ return useColor ? `\x1b[${code}m${str}\x1b[0m` : str;
40
+ }
41
+ const green = (s) => c("32", s);
42
+ const red = (s) => c("31", s);
43
+ const yellow = (s) => c("33", s);
44
+ const cyan = (s) => c("36", s);
45
+ const bold = (s) => c("1", s);
46
+ const dim = (s) => c("2", s);
47
+
48
+ const PRIORITY_COLOR = { high: red, medium: yellow, low: green };
49
+
50
+ // --- 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
+ function parseArgs(argv) {
57
+ const positional = [];
58
+ const flags = {};
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const a = argv[i];
61
+ if (a === "-p" || a === "--priority") flags.priority = argv[++i];
62
+ else if (a === "-t" || a === "--tags") flags.tags = argv[++i];
63
+ else if (a === "-c" || a === "--category") flags.category = argv[++i];
64
+ else if (a === "-d" || a === "--due") flags.due = argv[++i];
65
+ else if (a === "--note") flags.note = argv[++i];
66
+ else if (a === "--title") flags.title = argv[++i];
67
+ else if (a === "--status") flags.status = argv[++i];
68
+ else if (a === "--tag") flags.tag = argv[++i];
69
+ else if (a === "--sort") flags.sort = argv[++i];
70
+ else if (a === "--format") flags.format = argv[++i];
71
+ else if (a === "--all") flags.all = true;
72
+ else if (a === "--check") flags.check = true;
73
+ else if (a === "-h" || a === "--help") flags.help = true;
74
+ else if (a.startsWith("-")) {
75
+ // unknown flag — ignore but don't swallow a value
76
+ } else {
77
+ positional.push(a);
78
+ }
79
+ }
80
+ return { positional, flags };
81
+ }
82
+
83
+ // --- table rendering -------------------------------------------------------
84
+ function pad(str, width) {
85
+ str = String(str);
86
+ return str.length >= width ? str : str + " ".repeat(width - str.length);
87
+ }
88
+
89
+ function renderTable(todos) {
90
+ if (todos.length === 0) {
91
+ console.log(dim(" (no todos)"));
92
+ return;
93
+ }
94
+ 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"];
98
+
99
+ function dueCell(t) {
100
+ if (!t.due) return "-";
101
+ const ds = dueStatus(t.due);
102
+ if (t.done) return dim(t.due);
103
+ if (ds === "overdue") return red(t.due);
104
+ if (ds === "today") return yellow(t.due);
105
+ if (ds === "soon") return yellow(t.due);
106
+ return t.due;
107
+ }
108
+
109
+ const rows = todos.map((t) => {
110
+ const base = [
111
+ String(t.id),
112
+ t.done ? green("✓") : dim("○"),
113
+ PRIORITY_COLOR[t.priority](t.priority),
114
+ ];
115
+ if (hasDue) base.push(dueCell(t));
116
+ base.push(
117
+ t.category || "-",
118
+ t.tags.length ? cyan(t.tags.join(", ")) : "-",
119
+ t.done ? dim(t.title) : t.title
120
+ );
121
+ return base;
122
+ });
123
+
124
+ const widths = cols.map((_, ci) =>
125
+ Math.max(cols[ci].length, ...rows.map((r) => r[ci].length))
126
+ );
127
+
128
+ const header = " " + cols.map((h, i) => bold(pad(h, widths[i]))).join(" ");
129
+ console.log(header);
130
+ console.log(" " + dim("-".repeat(widths.reduce((a, w) => a + w + 2, -2))));
131
+ for (const row of rows) {
132
+ console.log(" " + row.map((cell, i) => pad(cell, widths[i])).join(" "));
133
+ }
134
+ console.log(dim(`\n ${todos.length} item(s)`));
135
+ }
136
+
137
+ // --- commands --------------------------------------------------------------
138
+ function cmdAdd(todos, { positional, flags }) {
139
+ const title = positional.join(" ");
140
+ const todo = addTodo(todos, {
141
+ title,
142
+ priority: flags.priority,
143
+ tags: flags.tags,
144
+ category: flags.category,
145
+ due: flags.due,
146
+ note: flags.note,
147
+ });
148
+ let msg = green("✓ Added:") + ` #${todo.id} ${todo.title}`;
149
+ if (todo.due) msg += dim(` (due ${todo.due})`);
150
+ console.log(msg);
151
+ }
152
+
153
+ function cmdList(todos, { flags }) {
154
+ const result = listTodos(todos, {
155
+ status: flags.status,
156
+ category: flags.category,
157
+ tag: flags.tag,
158
+ priority: flags.priority,
159
+ sort: flags.sort,
160
+ });
161
+ renderTable(result);
162
+ }
163
+
164
+ function cmdDone(todos, { positional, flags }) {
165
+ if (flags.all) {
166
+ const n = completeAll(todos);
167
+ console.log(green(`✓ Marked ${n} pending item(s) as done`));
168
+ return;
169
+ }
170
+ const id = positional[0];
171
+ if (!id) {
172
+ console.log(red("✗ Usage: todo done <id> (or todo done --all)"));
173
+ return;
174
+ }
175
+ const todo = completeTodo(todos, id);
176
+ if (!todo) console.log(red(`✗ No todo with id ${id}`));
177
+ else console.log(green("✓ Done:") + ` #${todo.id} ${todo.title}`);
178
+ }
179
+
180
+ function cmdToggle(todos, { positional }) {
181
+ const id = positional[0];
182
+ const todo = toggleTodo(todos, id);
183
+ if (!todo) console.log(red(`✗ No todo with id ${id}`));
184
+ else
185
+ console.log(
186
+ (todo.done ? green("✓ Done:") : yellow("◌ Reopened:")) +
187
+ ` #${todo.id} ${todo.title}`
188
+ );
189
+ }
190
+
191
+ function cmdEdit(todos, { positional, flags }) {
192
+ const id = positional[0];
193
+ const todo = editTodo(todos, id, {
194
+ title: flags.title,
195
+ priority: flags.priority,
196
+ tags: flags.tags,
197
+ category: flags.category,
198
+ due: flags.due,
199
+ note: flags.note,
200
+ });
201
+ if (!todo) console.log(red(`✗ No todo with id ${id}`));
202
+ else console.log(green("✓ Edited:") + ` #${todo.id} ${todo.title}`);
203
+ }
204
+
205
+ function cmdDelete(todos, { positional }) {
206
+ const id = positional[0];
207
+ const ok = deleteTodo(todos, id);
208
+ if (!ok) console.log(red(`✗ No todo with id ${id}`));
209
+ else console.log(green("✓ Deleted:") + ` #${id}`);
210
+ }
211
+
212
+ function cmdClear(todos) {
213
+ const n = clearTodos(todos);
214
+ console.log(green(`✓ Cleared ${n} item(s)`));
215
+ }
216
+
217
+ function cmdSearch(todos, { positional }) {
218
+ const keyword = positional.join(" ");
219
+ const results = searchTodos(todos, keyword);
220
+ console.log(bold(`Search:`) + ` "${keyword}" — ${results.length} match(es)`);
221
+ renderTable(results);
222
+ }
223
+
224
+ function cmdStats(todos) {
225
+ const s = getStats(todos);
226
+ console.log(bold("Todo Statistics"));
227
+ 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}`);
232
+ console.log(dim("\n By priority:"));
233
+ console.log(` ${red("high")}: ${s.byPriority.high} ${yellow("medium")}: ${s.byPriority.medium} ${green("low")}: ${s.byPriority.low}`);
234
+ const cats = Object.keys(s.byCategory);
235
+ if (cats.length) {
236
+ console.log(dim(" By category:"));
237
+ for (const c of cats.sort()) console.log(` ${c}: ${s.byCategory[c]}`);
238
+ }
239
+ }
240
+
241
+ function cmdExport(todos, { positional, flags }) {
242
+ const format = (positional[0] || flags.format || "json").toLowerCase();
243
+ const out = exportTodos(todos, format);
244
+ console.log(out);
245
+ }
246
+
247
+ function cmdArchive(todos) {
248
+ const n = archiveCompleted(todos);
249
+ console.log(green(`✓ Archived (removed) ${n} completed item(s)`));
250
+ }
251
+
252
+ function cmdUpdate(_todos, { flags }) {
253
+ const tmpDir = path.join(os.tmpdir(), "todo-cli-update");
254
+ const run = (cmd) => execSync(cmd, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
255
+
256
+ console.log(bold("todo update") + dim(" — checking for updates...\n"));
257
+
258
+ let latestVersion;
259
+ try {
260
+ // Clone or pull the repo into a temp dir.
261
+ if (fs.existsSync(path.join(tmpDir, ".git"))) {
262
+ console.log(dim(" pulling latest..."));
263
+ run(`git -C ${tmpDir} pull origin main`);
264
+ } else {
265
+ console.log(dim(" cloning repository..."));
266
+ fs.rmSync(tmpDir, { recursive: true, force: true });
267
+ run(`git clone --depth 1 ${REPO_URL} ${tmpDir}`);
268
+ }
269
+
270
+ latestVersion = JSON.parse(
271
+ fs.readFileSync(path.join(tmpDir, "package.json"), "utf8")
272
+ ).version;
273
+ } catch (err) {
274
+ console.error(red(`✗ Failed to fetch latest version: ${err.message}`));
275
+ process.exitCode = 1;
276
+ return;
277
+ }
278
+
279
+ console.log(` ${dim("current:")} v${VERSION}`);
280
+ console.log(` ${dim("latest:")} v${latestVersion}`);
281
+
282
+ if (latestVersion === VERSION) {
283
+ console.log(green("\n✓ Already up to date."));
284
+ return;
285
+ }
286
+
287
+ if (flags.check) {
288
+ console.log(yellow(`\n⬆ Update available: v${VERSION} → v${latestVersion}`));
289
+ console.log(dim(" Run `todo update` to install."));
290
+ return;
291
+ }
292
+
293
+ try {
294
+ console.log(dim("\n installing..."));
295
+ execSync(`npm install -g ${tmpDir}`, { stdio: "inherit" });
296
+ console.log(green(`\n✓ Updated: v${VERSION} → v${latestVersion}`));
297
+ } catch (err) {
298
+ console.error(red(`\n✗ Install failed: ${err.message}`));
299
+ process.exitCode = 1;
300
+ }
301
+ }
302
+
303
+ function cmdHelp() {
304
+ console.log(
305
+ bold("todo") +
306
+ dim(" — a CLI todo list manager\n") +
307
+ `
308
+ ${bold("USAGE")}
309
+ todo <command> [args] [flags]
310
+
311
+ ${bold("COMMANDS")}
312
+ add <title> Add a new todo
313
+ list List todos (default command)
314
+ done <id> Mark a todo as done (done --all = complete every pending)
315
+ toggle <id> Toggle done / pending
316
+ edit <id> Edit a todo (use --title, -p, -t, -c, -d, --note)
317
+ delete <id> Delete a todo (alias: rm, remove)
318
+ search <keyword> Search title / tags / category / note
319
+ stats Show summary statistics
320
+ export [json|csv|md] Export all todos to stdout
321
+ archive Remove all completed todos
322
+ update Update todo-cli to the latest version (--check = check only)
323
+ clear Remove ALL todos
324
+ help Show this help
325
+
326
+ ${bold("GLOBAL FLAGS")}
327
+ -h, --help Show help (works with any command)
328
+ -v, --version Show version
329
+
330
+ ${bold("FLAGS")}
331
+ -p, --priority <v> Priority: high | medium | low (default: medium)
332
+ -t, --tags <v> Comma-separated tags
333
+ -c, --category <v> Category name
334
+ -d, --due <v> Due date YYYY-MM-DD
335
+ --note <v> Longer note / description
336
+ --title <v> New title (for edit)
337
+ --status <v> Filter: all | done | pending (list only)
338
+ --tag <v> Filter by tag (list only)
339
+ --sort <v> Sort: priority | created | due | id (default: priority)
340
+ --all Complete all pending (done only)
341
+ --check Check for update without installing (update only)
342
+ --format <v> Export format json|csv|md (export only)
343
+
344
+ ${bold("EXAMPLES")}
345
+ 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
347
+ todo list --status pending --sort due
348
+ todo search report
349
+ todo done 3
350
+ todo done --all
351
+ todo edit 2 --title "Updated" -p low -d 2025-02-01
352
+ todo stats
353
+ todo export csv
354
+ todo archive
355
+ todo update --check
356
+ todo update
357
+ todo delete 5
358
+ `
359
+ );
360
+ }
361
+
362
+ // --- dispatch --------------------------------------------------------------
363
+ const COMMANDS = {
364
+ add: cmdAdd,
365
+ list: cmdList,
366
+ ls: cmdList,
367
+ done: cmdDone,
368
+ toggle: cmdToggle,
369
+ edit: cmdEdit,
370
+ delete: cmdDelete,
371
+ rm: cmdDelete,
372
+ remove: cmdDelete,
373
+ search: cmdSearch,
374
+ stats: cmdStats,
375
+ export: cmdExport,
376
+ archive: cmdArchive,
377
+ update: cmdUpdate,
378
+ clear: cmdClear,
379
+ help: cmdHelp,
380
+ };
381
+
382
+ /** Main entry point. argv should be process.argv.slice(2). */
383
+ export function run(argv) {
384
+ // -h / --help anywhere (including bare `todo --help`) shows help and exits.
385
+ if (argv.some((a) => a === "-h" || a === "--help")) {
386
+ cmdHelp();
387
+ return;
388
+ }
389
+
390
+ // -v / --version prints the version (read from package.json) and exits.
391
+ if (argv.some((a) => a === "-v" || a === "--version")) {
392
+ console.log(`todo v${VERSION}`);
393
+ return;
394
+ }
395
+
396
+ const store = createStore();
397
+ const todos = store.load();
398
+
399
+ const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
400
+ const rest = command === "list" && argv[0] && !argv[0].startsWith("-")
401
+ ? argv.slice(1)
402
+ : (command === "list" ? argv : argv.slice(1));
403
+
404
+ const handler = COMMANDS[command] ?? cmdHelp;
405
+ const parsed = parseArgs(rest);
406
+
407
+ if (command === "help") {
408
+ cmdHelp();
409
+ return;
410
+ }
411
+
412
+ try {
413
+ handler(todos, parsed);
414
+ store.save(todos);
415
+ } catch (err) {
416
+ console.error(red(`✗ ${err.message}`));
417
+ process.exitCode = 1;
418
+ }
419
+ }
package/src/store.js ADDED
@@ -0,0 +1,48 @@
1
+ // store.js — JSON file persistence layer for todos.
2
+ // Keeps all data in a single JSON file under the user's home directory.
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+
8
+ const DEFAULT_DATA_DIR = path.join(os.homedir(), ".todo-cli");
9
+ const DEFAULT_DATA_FILE = "todos.json";
10
+
11
+ /**
12
+ * Create a store backed by a JSON file.
13
+ * @param {object} [opts]
14
+ * @param {string} [opts.dataDir] Directory for the data file.
15
+ * @param {string} [opts.dataFile] Filename inside the data dir.
16
+ */
17
+ export function createStore(opts = {}) {
18
+ const dataDir = opts.dataDir ?? DEFAULT_DATA_DIR;
19
+ const dataFile = opts.dataFile ?? DEFAULT_DATA_FILE;
20
+ const filePath = path.join(dataDir, dataFile);
21
+
22
+ function ensureDir() {
23
+ if (!fs.existsSync(dataDir)) {
24
+ fs.mkdirSync(dataDir, { recursive: true });
25
+ }
26
+ }
27
+
28
+ /** Load all todos from disk. Returns [] when the file does not exist. */
29
+ function load() {
30
+ if (!fs.existsSync(filePath)) return [];
31
+ try {
32
+ const raw = fs.readFileSync(filePath, "utf8");
33
+ const parsed = JSON.parse(raw);
34
+ return Array.isArray(parsed) ? parsed : [];
35
+ } catch {
36
+ // Corrupt file — start fresh rather than crashing.
37
+ return [];
38
+ }
39
+ }
40
+
41
+ /** Persist the todos array to disk. */
42
+ function save(todos) {
43
+ ensureDir();
44
+ fs.writeFileSync(filePath, JSON.stringify(todos, null, 2), "utf8");
45
+ }
46
+
47
+ return { load, save, filePath };
48
+ }
package/src/todo.js ADDED
@@ -0,0 +1,291 @@
1
+ // todo.js — domain logic for the todo list.
2
+ // Pure functions over a todos array; persistence is delegated to a store.
3
+
4
+ export const PRIORITIES = ["high", "medium", "low"];
5
+ const PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
6
+
7
+ /** Normalise and validate a priority string. Defaults to "medium". */
8
+ export function normalizePriority(p) {
9
+ const v = String(p ?? "").toLowerCase().trim();
10
+ return PRIORITIES.includes(v) ? v : "medium";
11
+ }
12
+
13
+ /** Parse a comma-separated tag string into a deduplicated, lowercased array. */
14
+ export function parseTags(tags) {
15
+ if (!tags) return [];
16
+ const list = Array.isArray(tags)
17
+ ? tags
18
+ : String(tags).split(",").map((t) => t.trim().toLowerCase());
19
+ return [...new Set(list.filter(Boolean))];
20
+ }
21
+
22
+ /**
23
+ * Parse & validate a due date (YYYY-MM-DD). Returns "" for empty input,
24
+ * or throws on an unparseable value.
25
+ */
26
+ export function parseDueDate(due) {
27
+ if (!due) return "";
28
+ const s = String(due).trim();
29
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
30
+ if (!m) throw new Error(`Invalid due date "${s}". Use YYYY-MM-DD.`);
31
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
32
+ if (
33
+ d.getFullYear() !== Number(m[1]) ||
34
+ d.getMonth() !== Number(m[2]) - 1 ||
35
+ d.getDate() !== Number(m[3])
36
+ ) {
37
+ throw new Error(`Invalid due date "${s}".`);
38
+ }
39
+ return s;
40
+ }
41
+
42
+ /** Compare a due date (YYYY-MM-DD) to today. Returns null | "overdue" | "today" | "soon". */
43
+ export function dueStatus(due) {
44
+ if (!due) return null;
45
+ const today = new Date();
46
+ const t = new Date(today.getFullYear(), today.getMonth(), today.getDate());
47
+ const d = new Date(due + "T00:00:00");
48
+ const diff = Math.round((d - t) / 86400000);
49
+ if (diff < 0) return "overdue";
50
+ if (diff === 0) return "today";
51
+ if (diff <= 3) return "soon";
52
+ return null;
53
+ }
54
+
55
+ function nextId(todos) {
56
+ return todos.reduce((max, t) => Math.max(max, t.id), 0) + 1;
57
+ }
58
+
59
+ /** Add a new todo. Returns the created todo object. */
60
+ export function addTodo(todos, { title, priority, tags, category, due, note } = {}) {
61
+ if (!title || !title.trim()) {
62
+ throw new Error("Title is required.");
63
+ }
64
+ const todo = {
65
+ id: nextId(todos),
66
+ title: title.trim(),
67
+ done: false,
68
+ priority: normalizePriority(priority),
69
+ tags: parseTags(tags),
70
+ category: category ? String(category).trim() : "",
71
+ due: parseDueDate(due),
72
+ note: note ? String(note).trim() : "",
73
+ createdAt: new Date().toISOString(),
74
+ };
75
+ todos.push(todo);
76
+ return todo;
77
+ }
78
+
79
+ /** Find the index of a todo by id. Returns -1 if not found. */
80
+ function indexOf(todos, id) {
81
+ return todos.findIndex((t) => t.id === Number(id));
82
+ }
83
+
84
+ /** Delete a todo by id. Returns true if something was removed. */
85
+ export function deleteTodo(todos, id) {
86
+ const i = indexOf(todos, id);
87
+ if (i === -1) return false;
88
+ todos.splice(i, 1);
89
+ return true;
90
+ }
91
+
92
+ /** Edit an existing todo by id with partial fields. Returns the updated todo or null. */
93
+ export function editTodo(todos, id, { title, priority, tags, category, due, note } = {}) {
94
+ const i = indexOf(todos, id);
95
+ if (i === -1) return null;
96
+ const todo = todos[i];
97
+ if (title != null) todo.title = title.trim();
98
+ if (priority != null) todo.priority = normalizePriority(priority);
99
+ if (tags != null) todo.tags = parseTags(tags);
100
+ if (category != null) todo.category = String(category).trim();
101
+ if (due !== undefined) todo.due = parseDueDate(due);
102
+ if (note !== undefined) todo.note = String(note).trim();
103
+ todo.updatedAt = new Date().toISOString();
104
+ return todo;
105
+ }
106
+
107
+ /** Toggle the done flag of a todo by id. Returns the updated todo or null. */
108
+ export function toggleTodo(todos, id) {
109
+ const i = indexOf(todos, id);
110
+ if (i === -1) return null;
111
+ todos[i].done = !todos[i].done;
112
+ todos[i].updatedAt = new Date().toISOString();
113
+ return todos[i];
114
+ }
115
+
116
+ /** Mark a todo done by id. Returns the updated todo or null. */
117
+ export function completeTodo(todos, id) {
118
+ const i = indexOf(todos, id);
119
+ if (i === -1) return null;
120
+ todos[i].done = true;
121
+ todos[i].updatedAt = new Date().toISOString();
122
+ return todos[i];
123
+ }
124
+
125
+ /**
126
+ * Filter and sort the todos.
127
+ * @param {Array} todos
128
+ * @param {object} [filter]
129
+ * @param {string} [filter.status] "all" | "done" | "pending"
130
+ * @param {string} [filter.category] Exact category match.
131
+ * @param {string} [filter.tag] Exact tag match.
132
+ * @param {string} [filter.priority] "high" | "medium" | "low"
133
+ * @param {string} [filter.sort] "priority" | "created" | "id"
134
+ */
135
+ export function listTodos(todos, filter = {}) {
136
+ const status = filter.status ?? "all";
137
+ let result = todos.slice();
138
+
139
+ if (status === "done") result = result.filter((t) => t.done);
140
+ else if (status === "pending") result = result.filter((t) => !t.done);
141
+
142
+ if (filter.category) {
143
+ const c = String(filter.category).toLowerCase().trim();
144
+ result = result.filter((t) => t.category.toLowerCase() === c);
145
+ }
146
+ if (filter.tag) {
147
+ const tg = String(filter.tag).toLowerCase().trim();
148
+ result = result.filter((t) => t.tags.includes(tg));
149
+ }
150
+ if (filter.priority) {
151
+ const p = normalizePriority(filter.priority);
152
+ result = result.filter((t) => t.priority === p);
153
+ }
154
+
155
+ const sort = filter.sort ?? "priority";
156
+ switch (sort) {
157
+ case "created":
158
+ result.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
159
+ break;
160
+ case "id":
161
+ result.sort((a, b) => a.id - b.id);
162
+ break;
163
+ case "due":
164
+ result.sort((a, b) => {
165
+ if (!a.due && !b.due) return a.id - b.id;
166
+ if (!a.due) return 1;
167
+ if (!b.due) return -1;
168
+ return a.due.localeCompare(b.due);
169
+ });
170
+ break;
171
+ case "priority":
172
+ default:
173
+ result.sort((a, b) => {
174
+ const d = PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
175
+ return d !== 0 ? d : a.id - b.id;
176
+ });
177
+ break;
178
+ }
179
+ return result;
180
+ }
181
+
182
+ /** Remove all todos. Returns the count removed. */
183
+ export function clearTodos(todos) {
184
+ const n = todos.length;
185
+ todos.length = 0;
186
+ return n;
187
+ }
188
+
189
+ /** Search todos by keyword across title, tags, category, and note. */
190
+ export function searchTodos(todos, keyword) {
191
+ if (!keyword) return [];
192
+ const kw = String(keyword).toLowerCase().trim();
193
+ return todos.filter((t) => {
194
+ return (
195
+ t.title.toLowerCase().includes(kw) ||
196
+ t.category.toLowerCase().includes(kw) ||
197
+ t.note.toLowerCase().includes(kw) ||
198
+ t.tags.some((tag) => tag.includes(kw))
199
+ );
200
+ });
201
+ }
202
+
203
+ /** Compute summary statistics over all todos. */
204
+ export function getStats(todos) {
205
+ const stats = {
206
+ total: todos.length,
207
+ done: 0,
208
+ pending: 0,
209
+ overdue: 0,
210
+ byPriority: { high: 0, medium: 0, low: 0 },
211
+ byCategory: {},
212
+ };
213
+ for (const t of todos) {
214
+ if (t.done) stats.done++;
215
+ else stats.pending++;
216
+ stats.byPriority[t.priority] = (stats.byPriority[t.priority] ?? 0) + 1;
217
+ if (t.category) {
218
+ stats.byCategory[t.category] = (stats.byCategory[t.category] ?? 0) + 1;
219
+ }
220
+ if (!t.done && dueStatus(t.due) === "overdue") stats.overdue++;
221
+ }
222
+ return stats;
223
+ }
224
+
225
+ /** Mark ALL pending todos as done. Returns the count completed. */
226
+ export function completeAll(todos) {
227
+ let n = 0;
228
+ const now = new Date().toISOString();
229
+ for (const t of todos) {
230
+ if (!t.done) {
231
+ t.done = true;
232
+ t.updatedAt = now;
233
+ n++;
234
+ }
235
+ }
236
+ return n;
237
+ }
238
+
239
+ /** Remove all completed todos. Returns the count archived (removed). */
240
+ export function archiveCompleted(todos) {
241
+ let n = 0;
242
+ for (let i = todos.length - 1; i >= 0; i--) {
243
+ if (todos[i].done) {
244
+ todos.splice(i, 1);
245
+ n++;
246
+ }
247
+ }
248
+ return n;
249
+ }
250
+
251
+ /** Export todos to a string in the given format: "json" | "csv" | "md". */
252
+ export function exportTodos(todos, format = "json") {
253
+ switch (format) {
254
+ case "csv":
255
+ return exportCSV(todos);
256
+ case "md":
257
+ return exportMarkdown(todos);
258
+ case "json":
259
+ default:
260
+ return JSON.stringify(todos, null, 2);
261
+ }
262
+ }
263
+
264
+ function exportCSV(todos) {
265
+ const cols = ["id", "title", "done", "priority", "category", "tags", "due", "note"];
266
+ const esc = (v) => {
267
+ const s = String(v ?? "");
268
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
269
+ };
270
+ const lines = [cols.join(",")];
271
+ for (const t of todos) {
272
+ lines.push(
273
+ [t.id, t.title, t.done, t.priority, t.category, t.tags.join(";"), t.due, t.note]
274
+ .map(esc)
275
+ .join(",")
276
+ );
277
+ }
278
+ return lines.join("\n");
279
+ }
280
+
281
+ function exportMarkdown(todos) {
282
+ if (todos.length === 0) return "_(no todos)_";
283
+ const lines = ["| ID | Status | Priority | Category | Tags | Due | Title |",
284
+ "|----|--------|----------|----------|------|-----|-------|"];
285
+ for (const t of todos) {
286
+ lines.push(
287
+ `| ${t.id} | ${t.done ? "✓" : "○"} | ${t.priority} | ${t.category || "-"} | ${t.tags.join(", ") || "-"} | ${t.due || "-"} | ${t.title} |`
288
+ );
289
+ }
290
+ return lines.join("\n");
291
+ }