command-line-tool-task-manager 1.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/LICENSE +21 -0
- package/README.md +209 -0
- package/package.json +46 -0
- package/src/display.js +91 -0
- package/src/index.js +172 -0
- package/src/storage.js +28 -0
- package/src/tasks.js +99 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ankit Wahane
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# cli-task-manager
|
|
2
|
+
|
|
3
|
+
A simple and powerful command-line task manager. Add, list, complete, edit, search, and track tasks right from your terminal — no account, no cloud, just a local JSON file in your home directory.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/ankitw497/Command-Line-Task-Manager/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/command-line-tool-task-manager)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g command-line-tool-task-manager
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> Requires Node.js >= 18
|
|
18
|
+
|
|
19
|
+
Once installed, the `task` command is available globally.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
task add "Buy groceries"
|
|
27
|
+
task add "Submit report" --priority high
|
|
28
|
+
task list
|
|
29
|
+
task complete 1
|
|
30
|
+
task stats
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
### `task add <title>`
|
|
38
|
+
|
|
39
|
+
Add a new task.
|
|
40
|
+
|
|
41
|
+
| Option | Alias | Description | Default |
|
|
42
|
+
|--------|-------|-------------|---------|
|
|
43
|
+
| `--priority <level>` | `-p` | `high`, `medium`, or `low` | `medium` |
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
task add "Read documentation"
|
|
47
|
+
task add "Fix critical bug" -p high
|
|
48
|
+
task add "Clean up comments" -p low
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### `task list` / `task ls`
|
|
54
|
+
|
|
55
|
+
List all tasks in a formatted table, sorted by status → priority → ID.
|
|
56
|
+
|
|
57
|
+
| Option | Alias | Description |
|
|
58
|
+
|--------|-------|-------------|
|
|
59
|
+
| `--filter <status>` | `-f` | `completed` or `pending` |
|
|
60
|
+
| `--priority <level>` | `-p` | `high`, `medium`, or `low` |
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
task list
|
|
64
|
+
task ls
|
|
65
|
+
task list --filter pending
|
|
66
|
+
task list --filter completed
|
|
67
|
+
task list --priority high
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `task complete <id>` / `task done <id>`
|
|
73
|
+
|
|
74
|
+
Mark a task as completed.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
task complete 3
|
|
78
|
+
task done 3
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### `task edit <id>`
|
|
84
|
+
|
|
85
|
+
Edit a task's title or priority.
|
|
86
|
+
|
|
87
|
+
| Option | Alias | Description |
|
|
88
|
+
|--------|-------|-------------|
|
|
89
|
+
| `--title <title>` | `-t` | New title |
|
|
90
|
+
| `--priority <level>` | `-p` | New priority |
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
task edit 2 --title "Updated title"
|
|
94
|
+
task edit 2 --priority high
|
|
95
|
+
task edit 2 -t "New title" -p low
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### `task delete <id>` / `task rm <id>`
|
|
101
|
+
|
|
102
|
+
Permanently delete a task.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
task delete 4
|
|
106
|
+
task rm 4
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `task search <keyword>`
|
|
112
|
+
|
|
113
|
+
Search tasks by keyword (case-insensitive). Matching text is highlighted.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
task search bug
|
|
117
|
+
task search "meeting notes"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `task stats`
|
|
123
|
+
|
|
124
|
+
Show completion statistics with a visual progress bar.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
task stats
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Task Statistics
|
|
132
|
+
─────────────────────────────────
|
|
133
|
+
Total : 8
|
|
134
|
+
Completed : 5
|
|
135
|
+
Pending : 3
|
|
136
|
+
Progress : [██████████████████░░░░░░░░░░░░] 63%
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `task clear`
|
|
142
|
+
|
|
143
|
+
Remove all completed tasks in one go.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
task clear
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Data Storage
|
|
152
|
+
|
|
153
|
+
Tasks are saved to `~/.task-manager/tasks.json`. This file is created automatically on first use and persists across all directories and sessions.
|
|
154
|
+
|
|
155
|
+
To use a custom location (e.g. for per-project task lists):
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
TASK_MANAGER_DATA_DIR=/path/to/dir task list
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
git clone https://github.com/ankitw497/Command-Line-Task-Manager.git
|
|
167
|
+
cd Command-Line-Task-Manager
|
|
168
|
+
npm install
|
|
169
|
+
|
|
170
|
+
# Run the CLI locally
|
|
171
|
+
node src/index.js add "Test task"
|
|
172
|
+
|
|
173
|
+
# Or link it globally for development
|
|
174
|
+
npm link
|
|
175
|
+
task add "Test task"
|
|
176
|
+
|
|
177
|
+
# Run tests
|
|
178
|
+
npm test
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Project Structure
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
src/
|
|
185
|
+
index.js — CLI entry point (commands + argument parsing)
|
|
186
|
+
tasks.js — Business logic (add, complete, edit, delete, search …)
|
|
187
|
+
storage.js — JSON persistence (read/write ~/.task-manager/tasks.json)
|
|
188
|
+
display.js — Table rendering and output formatting
|
|
189
|
+
tests/
|
|
190
|
+
tasks.test.js — Unit tests for task operations (35 tests)
|
|
191
|
+
storage.test.js — Unit tests for storage layer
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Contributing
|
|
197
|
+
|
|
198
|
+
1. Fork the repository
|
|
199
|
+
2. Create a feature branch (`git checkout -b feat/my-feature`)
|
|
200
|
+
3. Commit your changes
|
|
201
|
+
4. Open a Pull Request
|
|
202
|
+
|
|
203
|
+
Please ensure all tests pass (`npm test`) before submitting.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT © [Ankit Wahane](https://github.com/ankitwahane)
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "command-line-tool-task-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple and powerful command-line task manager — add, list, complete, edit, search and track tasks right from your terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"task": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "node --test tests/tasks.test.js tests/storage.test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"task",
|
|
17
|
+
"manager",
|
|
18
|
+
"todo",
|
|
19
|
+
"productivity",
|
|
20
|
+
"command-line",
|
|
21
|
+
"terminal"
|
|
22
|
+
],
|
|
23
|
+
"author": "Ankit Wahane",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/ankitw497/Command-Line-Task-Manager.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/ankitw497/Command-Line-Task-Manager/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/ankitw497/Command-Line-Task-Manager#readme",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src/",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^5.3.0",
|
|
43
|
+
"cli-table3": "^0.6.3",
|
|
44
|
+
"commander": "^12.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/display.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
|
|
4
|
+
export const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
|
|
5
|
+
|
|
6
|
+
export const PRIORITY_LABEL = {
|
|
7
|
+
high: chalk.red.bold('HIGH'),
|
|
8
|
+
medium: chalk.yellow.bold('MED'),
|
|
9
|
+
low: chalk.green.bold('LOW'),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const PRIORITY_DOT = {
|
|
13
|
+
high: chalk.red('●'),
|
|
14
|
+
medium: chalk.yellow('●'),
|
|
15
|
+
low: chalk.green('●'),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function formatDate(iso) {
|
|
19
|
+
if (!iso) return chalk.gray('—');
|
|
20
|
+
const d = new Date(iso);
|
|
21
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
22
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatTitle(task) {
|
|
26
|
+
return task.completed ? chalk.gray.strikethrough(task.title) : task.title;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function statusIcon(task) {
|
|
30
|
+
return task.completed ? chalk.green('✔') : chalk.gray('○');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function highlightMatch(text, keyword) {
|
|
34
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
35
|
+
return text.replace(new RegExp(escaped, 'gi'), (m) => chalk.cyan.bold(m));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a formatted table for the given tasks.
|
|
40
|
+
* @param {object[]} tasks
|
|
41
|
+
* @param {(task: object) => string} [titleFn] Optional override for title cell rendering
|
|
42
|
+
*/
|
|
43
|
+
export function buildTable(tasks, titleFn = null) {
|
|
44
|
+
const table = new Table({
|
|
45
|
+
head: [
|
|
46
|
+
chalk.cyan.bold('ID'),
|
|
47
|
+
chalk.cyan.bold(''),
|
|
48
|
+
chalk.cyan.bold('Title'),
|
|
49
|
+
chalk.cyan.bold('Priority'),
|
|
50
|
+
chalk.cyan.bold('Created'),
|
|
51
|
+
chalk.cyan.bold('Completed'),
|
|
52
|
+
],
|
|
53
|
+
colWidths: [5, 3, 38, 10, 20, 20],
|
|
54
|
+
style: { head: [], border: ['gray'] },
|
|
55
|
+
wordWrap: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
for (const task of tasks) {
|
|
59
|
+
const title = titleFn ? titleFn(task) : formatTitle(task);
|
|
60
|
+
table.push([
|
|
61
|
+
chalk.dim(String(task.id)),
|
|
62
|
+
statusIcon(task),
|
|
63
|
+
title,
|
|
64
|
+
`${PRIORITY_DOT[task.priority]} ${PRIORITY_LABEL[task.priority]}`,
|
|
65
|
+
chalk.dim(formatDate(task.createdAt)),
|
|
66
|
+
chalk.dim(formatDate(task.completedAt)),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return table;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function printStats(tasks) {
|
|
74
|
+
const total = tasks.length;
|
|
75
|
+
const completed = tasks.filter((t) => t.completed).length;
|
|
76
|
+
const pending = total - completed;
|
|
77
|
+
const pct = total === 0 ? 0 : Math.round((completed / total) * 100);
|
|
78
|
+
|
|
79
|
+
const BAR_WIDTH = 30;
|
|
80
|
+
const filled = Math.round((pct / 100) * BAR_WIDTH);
|
|
81
|
+
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(BAR_WIDTH - filled));
|
|
82
|
+
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(chalk.bold(' Task Statistics'));
|
|
85
|
+
console.log(chalk.gray(' ─────────────────────────────────'));
|
|
86
|
+
console.log(` Total : ${chalk.bold(total)}`);
|
|
87
|
+
console.log(` Completed : ${chalk.green.bold(completed)}`);
|
|
88
|
+
console.log(` Pending : ${chalk.yellow.bold(pending)}`);
|
|
89
|
+
console.log(` Progress : [${bar}] ${chalk.bold(pct + '%')}`);
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
addTask,
|
|
11
|
+
listTasks,
|
|
12
|
+
completeTask,
|
|
13
|
+
deleteTask,
|
|
14
|
+
editTask,
|
|
15
|
+
searchTasks,
|
|
16
|
+
clearCompleted,
|
|
17
|
+
getAllTasks,
|
|
18
|
+
VALID_PRIORITIES,
|
|
19
|
+
} from './tasks.js';
|
|
20
|
+
import { buildTable, printStats, highlightMatch } from './display.js';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
24
|
+
|
|
25
|
+
function handleError(err) {
|
|
26
|
+
console.error(chalk.red('Error: ') + err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.name('task')
|
|
32
|
+
.description('A simple and powerful command-line task manager')
|
|
33
|
+
.version(pkg.version, '-V, --version', 'output the current version');
|
|
34
|
+
|
|
35
|
+
// ── add ──────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('add <title>')
|
|
39
|
+
.description('Add a new task')
|
|
40
|
+
.option('-p, --priority <level>', `Priority: ${VALID_PRIORITIES.join(' | ')}`, 'medium')
|
|
41
|
+
.action((title, opts) => {
|
|
42
|
+
try {
|
|
43
|
+
const task = addTask(title, opts.priority.toLowerCase());
|
|
44
|
+
console.log(chalk.green(`✔ Added task #${task.id}:`) + ' ' + task.title);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
handleError(err);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command('list')
|
|
54
|
+
.alias('ls')
|
|
55
|
+
.description('List all tasks')
|
|
56
|
+
.option('-f, --filter <status>', 'Filter: completed | pending')
|
|
57
|
+
.option('-p, --priority <level>', `Filter by priority: ${VALID_PRIORITIES.join(' | ')}`)
|
|
58
|
+
.action((opts) => {
|
|
59
|
+
try {
|
|
60
|
+
const tasks = listTasks({ filter: opts.filter, priority: opts.priority });
|
|
61
|
+
if (tasks.length === 0) {
|
|
62
|
+
console.log(chalk.yellow('No tasks found.'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(buildTable(tasks).toString());
|
|
66
|
+
console.log(chalk.dim(` ${tasks.length} task(s)`));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
handleError(err);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ── complete ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command('complete <id>')
|
|
76
|
+
.alias('done')
|
|
77
|
+
.description('Mark a task as completed')
|
|
78
|
+
.action((id) => {
|
|
79
|
+
try {
|
|
80
|
+
const task = completeTask(parseInt(id, 10));
|
|
81
|
+
console.log(chalk.green(`✔ Completed task #${task.id}:`) + ' ' + task.title);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
handleError(err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── delete ────────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('delete <id>')
|
|
91
|
+
.alias('rm')
|
|
92
|
+
.description('Delete a task permanently')
|
|
93
|
+
.action((id) => {
|
|
94
|
+
try {
|
|
95
|
+
const task = deleteTask(parseInt(id, 10));
|
|
96
|
+
console.log(chalk.red(`✖ Deleted task #${task.id}:`) + ' ' + task.title);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
handleError(err);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── edit ──────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command('edit <id>')
|
|
106
|
+
.description('Edit a task title or priority')
|
|
107
|
+
.option('-t, --title <title>', 'New title')
|
|
108
|
+
.option('-p, --priority <level>', `New priority: ${VALID_PRIORITIES.join(' | ')}`)
|
|
109
|
+
.action((id, opts) => {
|
|
110
|
+
try {
|
|
111
|
+
const task = editTask(parseInt(id, 10), { title: opts.title, priority: opts.priority });
|
|
112
|
+
console.log(chalk.blue(`✎ Updated task #${task.id}:`) + ' ' + task.title);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
handleError(err);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── search ────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
program
|
|
121
|
+
.command('search <keyword>')
|
|
122
|
+
.description('Search tasks by keyword')
|
|
123
|
+
.action((keyword) => {
|
|
124
|
+
try {
|
|
125
|
+
const tasks = searchTasks(keyword);
|
|
126
|
+
if (tasks.length === 0) {
|
|
127
|
+
console.log(chalk.yellow(`No tasks match "${keyword}".`));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const table = buildTable(tasks, (task) => {
|
|
131
|
+
const highlighted = highlightMatch(task.title, keyword);
|
|
132
|
+
return task.completed ? chalk.gray.strikethrough(highlighted) : highlighted;
|
|
133
|
+
});
|
|
134
|
+
console.log(table.toString());
|
|
135
|
+
console.log(chalk.dim(` ${tasks.length} match(es) for "${chalk.cyan(keyword)}"`));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
handleError(err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── stats ─────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
program
|
|
144
|
+
.command('stats')
|
|
145
|
+
.description('Show task statistics and progress')
|
|
146
|
+
.action(() => {
|
|
147
|
+
try {
|
|
148
|
+
printStats(getAllTasks());
|
|
149
|
+
} catch (err) {
|
|
150
|
+
handleError(err);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── clear ─────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command('clear')
|
|
158
|
+
.description('Remove all completed tasks')
|
|
159
|
+
.action(() => {
|
|
160
|
+
try {
|
|
161
|
+
const count = clearCompleted();
|
|
162
|
+
if (count === 0) {
|
|
163
|
+
console.log(chalk.yellow('No completed tasks to clear.'));
|
|
164
|
+
} else {
|
|
165
|
+
console.log(chalk.green(`✔ Cleared ${count} completed task(s).`));
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
handleError(err);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
program.parse(process.argv);
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
// Read dynamically so tests can override via TASK_MANAGER_DATA_DIR
|
|
6
|
+
function getDataDir() {
|
|
7
|
+
return process.env.TASK_MANAGER_DATA_DIR || join(homedir(), '.task-manager');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getTasksFile() {
|
|
11
|
+
return join(getDataDir(), 'tasks.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loadTasks() {
|
|
15
|
+
const file = getTasksFile();
|
|
16
|
+
if (!existsSync(file)) return [];
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveTasks(tasks) {
|
|
25
|
+
const dir = getDataDir();
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
writeFileSync(getTasksFile(), JSON.stringify(tasks, null, 2), 'utf-8');
|
|
28
|
+
}
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { loadTasks, saveTasks } from './storage.js';
|
|
2
|
+
import { PRIORITY_ORDER } from './display.js';
|
|
3
|
+
|
|
4
|
+
export const VALID_PRIORITIES = ['high', 'medium', 'low'];
|
|
5
|
+
|
|
6
|
+
function nextId(tasks) {
|
|
7
|
+
return tasks.length === 0 ? 1 : Math.max(...tasks.map((t) => t.id)) + 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sortTasks(tasks) {
|
|
11
|
+
return [...tasks].sort((a, b) => {
|
|
12
|
+
if (a.completed !== b.completed) return a.completed ? 1 : -1;
|
|
13
|
+
const pd = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
|
|
14
|
+
if (pd !== 0) return pd;
|
|
15
|
+
return a.id - b.id;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addTask(title, priority = 'medium') {
|
|
20
|
+
if (!VALID_PRIORITIES.includes(priority)) {
|
|
21
|
+
throw new Error(`Invalid priority "${priority}". Use: ${VALID_PRIORITIES.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
const trimmed = title.trim();
|
|
24
|
+
if (!trimmed) throw new Error('Task title cannot be empty.');
|
|
25
|
+
|
|
26
|
+
const tasks = loadTasks();
|
|
27
|
+
const task = {
|
|
28
|
+
id: nextId(tasks),
|
|
29
|
+
title: trimmed,
|
|
30
|
+
priority,
|
|
31
|
+
completed: false,
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
completedAt: null,
|
|
34
|
+
};
|
|
35
|
+
tasks.push(task);
|
|
36
|
+
saveTasks(tasks);
|
|
37
|
+
return task;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listTasks({ filter, priority } = {}) {
|
|
41
|
+
let tasks = loadTasks();
|
|
42
|
+
if (filter === 'completed') tasks = tasks.filter((t) => t.completed);
|
|
43
|
+
else if (filter === 'pending') tasks = tasks.filter((t) => !t.completed);
|
|
44
|
+
if (priority) tasks = tasks.filter((t) => t.priority === priority);
|
|
45
|
+
return sortTasks(tasks);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function completeTask(id) {
|
|
49
|
+
const tasks = loadTasks();
|
|
50
|
+
const task = tasks.find((t) => t.id === id);
|
|
51
|
+
if (!task) throw new Error(`Task #${id} not found.`);
|
|
52
|
+
if (task.completed) throw new Error(`Task #${id} is already completed.`);
|
|
53
|
+
task.completed = true;
|
|
54
|
+
task.completedAt = new Date().toISOString();
|
|
55
|
+
saveTasks(tasks);
|
|
56
|
+
return task;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function deleteTask(id) {
|
|
60
|
+
const tasks = loadTasks();
|
|
61
|
+
const idx = tasks.findIndex((t) => t.id === id);
|
|
62
|
+
if (idx === -1) throw new Error(`Task #${id} not found.`);
|
|
63
|
+
const [task] = tasks.splice(idx, 1);
|
|
64
|
+
saveTasks(tasks);
|
|
65
|
+
return task;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function editTask(id, { title, priority } = {}) {
|
|
69
|
+
if (!title && !priority) {
|
|
70
|
+
throw new Error('Provide at least --title or --priority to edit.');
|
|
71
|
+
}
|
|
72
|
+
if (priority && !VALID_PRIORITIES.includes(priority)) {
|
|
73
|
+
throw new Error(`Invalid priority "${priority}". Use: ${VALID_PRIORITIES.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
const tasks = loadTasks();
|
|
76
|
+
const task = tasks.find((t) => t.id === id);
|
|
77
|
+
if (!task) throw new Error(`Task #${id} not found.`);
|
|
78
|
+
if (title) task.title = title.trim();
|
|
79
|
+
if (priority) task.priority = priority;
|
|
80
|
+
saveTasks(tasks);
|
|
81
|
+
return task;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function searchTasks(keyword) {
|
|
85
|
+
const kw = keyword.toLowerCase();
|
|
86
|
+
return sortTasks(loadTasks().filter((t) => t.title.toLowerCase().includes(kw)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function clearCompleted() {
|
|
90
|
+
const tasks = loadTasks();
|
|
91
|
+
const remaining = tasks.filter((t) => !t.completed);
|
|
92
|
+
const count = tasks.length - remaining.length;
|
|
93
|
+
saveTasks(remaining);
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getAllTasks() {
|
|
98
|
+
return loadTasks();
|
|
99
|
+
}
|