taskninja 1.0.3 → 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 +316 -167
- package/app.js +362 -114
- package/images/add.png +0 -0
- package/images/delete.png +0 -0
- package/images/delete2.png +0 -0
- package/images/done.png +0 -0
- package/images/done2.png +0 -0
- package/images/list.png +0 -0
- package/images/search.png +0 -0
- package/images/sort.png +0 -0
- package/images/sort2.png +0 -0
- package/images/undo.png +0 -0
- package/images/update.png +0 -0
- package/package.json +5 -3
- package/utils/taskService.js +63 -0
- package/utils/validators.js +26 -0
package/README.md
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
# TaskNinja
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Version:** 1.1.0
|
|
4
|
+
**Description:** A simple CLI To-Do Application to manage your tasks directly from the terminal. Includes colored tables, search, soft delete, undo, and interactive sort.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
---
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Table of Contents
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
* [Installation](#installation)
|
|
11
|
+
* [Running the CLI](#running-the-cli)
|
|
12
|
+
* [Commands](#commands)
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
* [Add Task](#add-task)
|
|
15
|
+
* [List Tasks](#list-tasks)
|
|
16
|
+
* [Update Task](#update-task)
|
|
17
|
+
* [Delete Task](#delete-task)
|
|
18
|
+
* [Undo Delete Task](#undo-delete-task)
|
|
19
|
+
* [Search Tasks](#search-tasks)
|
|
20
|
+
* [Sort Tasks](#sort-tasks)
|
|
21
|
+
* [Task Fields & Allowed Values](#task-fields--allowed-values)
|
|
22
|
+
* [Examples](#examples)
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **Node.js**: Runtime environment.
|
|
22
|
-
- **Commander**: For parsing command-line arguments and defining commands.
|
|
23
|
-
- **Inquirer**: For interactive CLI prompts.
|
|
24
|
-
- **fs/promises**: For asynchronous file system operations to handle task storage.
|
|
25
|
-
- **ECMAScript Modules (ESM)**: Modern JavaScript module system.
|
|
26
|
-
|
|
27
|
-
The application is written in pure JavaScript (ES6+), with no additional frameworks or databases required.
|
|
24
|
+
---
|
|
28
25
|
|
|
29
26
|
## Installation
|
|
30
27
|
|
|
@@ -36,198 +33,350 @@ TaskNinja is published on npm as `taskninja`. Install it globally to use the CLI
|
|
|
36
33
|
npm install -g taskninja
|
|
37
34
|
```
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
or use it instantly with:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx taskninja
|
|
40
|
+
npx taskninja <command>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Clone the repository and install dependencies:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone <your-repo-url>
|
|
47
|
+
cd taskninja
|
|
48
|
+
npm install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Running the CLI
|
|
54
|
+
|
|
55
|
+
You can run the CLI using either `node` or the bin aliases:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Using node
|
|
59
|
+
node app.js <command>
|
|
60
|
+
|
|
61
|
+
# Using bin aliases
|
|
62
|
+
dotask <command>
|
|
63
|
+
taskninja <command>
|
|
64
|
+
tn <command>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Replace `<command>` with any of the available commands: `add`, `list`, `update`, `delete`, `undo`, `search`, `sort`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Commands
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
npm start
|
|
62
|
-
```
|
|
63
|
-
Or directly:
|
|
64
|
-
```bash
|
|
65
|
-
node app.js
|
|
66
|
-
```
|
|
73
|
+
### 1. Add Task
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
**Alias:** `a`
|
|
76
|
+
**Description:** Add a new task interactively.
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
```bash
|
|
79
|
+
tn a
|
|
80
|
+
```
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
**Prompts:**
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
dotask add
|
|
77
|
-
```
|
|
78
|
-
- This will prompt you interactively for title, status, priority, due date, and description.
|
|
79
|
-
- Example output: "Task added successfully!"
|
|
84
|
+
1. **Task Title:** Enter any text (cannot be empty).
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
dotask list
|
|
84
|
-
```
|
|
85
|
-
- Lists all tasks in a table format.
|
|
86
|
-
- Optional filter by status: `dotask list --status todo` (or `-s todo`).
|
|
87
|
-
- Aliases: `dotask ls`.
|
|
86
|
+
2. **Task Status:** Select from allowed values using arrows:
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
- Prompts to select a task by ID and update its status.
|
|
94
|
-
- Displays the updated task in a table.
|
|
95
|
-
- Aliases: `dotask up`.
|
|
88
|
+
* todo
|
|
89
|
+
* in-progress
|
|
90
|
+
* done
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
dotask delete
|
|
100
|
-
```
|
|
101
|
-
- Prompts to select a task by ID and confirm deletion.
|
|
102
|
-
- Aliases: `dotask del`.
|
|
92
|
+
3. **Task Priority:** Select from allowed values:
|
|
103
93
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
- Shows all available commands and options.
|
|
94
|
+
* low
|
|
95
|
+
* medium
|
|
96
|
+
* high
|
|
109
97
|
|
|
110
|
-
-
|
|
111
|
-
```bash
|
|
112
|
-
dotask --version
|
|
113
|
-
```
|
|
114
|
-
- Displays the current version (1.0.0).
|
|
98
|
+
4. **Due Date:** Enter in `YYYY-MM-DD` format. Invalid dates will show a validation error.
|
|
115
99
|
|
|
116
|
-
|
|
100
|
+
5. **Description:** Optional text.
|
|
117
101
|
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
dotask add
|
|
121
|
-
```
|
|
122
|
-
- Follow prompts: Enter "Buy groceries" as title, select "todo" status, "medium" priority, "2024-12-31" due date, and "Milk, eggs, bread" as description.
|
|
102
|
+
**Behavior:**
|
|
123
103
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
dotask list -s in-progress
|
|
127
|
-
```
|
|
104
|
+
* Task is saved in `todos.json`.
|
|
105
|
+
* Confirmation message: `Task added successfully!`
|
|
128
106
|
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
dotask update
|
|
132
|
-
```
|
|
133
|
-
- Select task ID from the list and choose new status (e.g., "done").
|
|
107
|
+
---
|
|
134
108
|
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
dotask delete
|
|
138
|
-
```
|
|
139
|
-
- Select task ID and confirm.
|
|
109
|
+
### 2. List Tasks
|
|
140
110
|
|
|
141
|
-
|
|
111
|
+
**Alias:** `ls`
|
|
112
|
+
**Description:** List all tasks, optionally filtered by status.
|
|
142
113
|
|
|
143
|
-
|
|
114
|
+
```bash
|
|
115
|
+
tn ls
|
|
116
|
+
tn ls --status todo
|
|
117
|
+
```
|
|
144
118
|
|
|
145
|
-
|
|
146
|
-
- **Statuses**: Limited to "todo", "in-progress", "done".
|
|
147
|
-
- **Priorities**: Limited to "low", "medium", "high".
|
|
148
|
-
- **Due Date Format**: YYYY-MM-DD (validated during input).
|
|
119
|
+
**Behavior:**
|
|
149
120
|
|
|
150
|
-
|
|
121
|
+
* Displays tasks in a **colored table** using chalk:
|
|
151
122
|
|
|
152
|
-
|
|
123
|
+
| # | ID | Title | Status | Priority | DueDate | Description |
|
|
124
|
+
| - | -- | ----- | ------ | -------- | ------- | ----------- |
|
|
125
|
+
|
|
126
|
+
* Status colors: `todo` → blue, `in-progress` → yellow, `done` → green
|
|
127
|
+
* Priority colors: `low` → green, `medium` → yellow, `high` → red
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 3. Update Task
|
|
132
|
+
|
|
133
|
+
**Alias:** `up`
|
|
134
|
+
**Description:** Update an existing task by selecting its ID.
|
|
153
135
|
|
|
154
|
-
To run in development mode:
|
|
155
136
|
```bash
|
|
156
|
-
|
|
137
|
+
tn up
|
|
157
138
|
```
|
|
158
139
|
|
|
159
|
-
|
|
140
|
+
**Steps:**
|
|
141
|
+
|
|
142
|
+
1. Select task by ID from a list.
|
|
143
|
+
2. Confirm which fields to change (title, status, priority, due date, description).
|
|
144
|
+
3. Enter new values where applicable.
|
|
145
|
+
|
|
146
|
+
**Behavior:**
|
|
147
|
+
|
|
148
|
+
* Only fields selected for change are updated.
|
|
149
|
+
* Confirmation message: `Task updated successfully!`
|
|
150
|
+
* Shows updated **colored task table**.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### 4. Delete Task
|
|
155
|
+
|
|
156
|
+
**Alias:** `del`
|
|
157
|
+
**Description:** Soft delete a task by selecting its ID.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
tn del
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Steps:**
|
|
164
|
+
|
|
165
|
+
1. Select task by ID from a list.
|
|
166
|
+
2. Confirm deletion.
|
|
167
|
+
|
|
168
|
+
**Behavior:**
|
|
169
|
+
|
|
170
|
+
* Task is removed from `todos.json` **but saved in `deleted-todos.json`**.
|
|
171
|
+
* Confirmation: `Task deleted successfully!`
|
|
172
|
+
* You can restore it using the `undo` command.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### 5. Undo Delete Task
|
|
177
|
+
|
|
178
|
+
**Alias:** `un`
|
|
179
|
+
**Description:** Restore the last deleted task.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
tn un
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Behavior:**
|
|
186
|
+
|
|
187
|
+
* Restores the last deleted task from `deleted-todos.json`.
|
|
188
|
+
* Confirmation: `Last deleted task restored successfully!, (Task name: taskName)`
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
Here’s the **Search Tasks** section in the README updated in English after your CLI changes:
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### 6. Search Tasks
|
|
197
|
+
|
|
198
|
+
**Alias:** `sr`
|
|
199
|
+
**Description:** Search tasks by keyword in title or description.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Search interactively
|
|
203
|
+
tn search
|
|
204
|
+
|
|
205
|
+
# Or search directly using --find
|
|
206
|
+
tn search --find "meeting"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Behavior:**
|
|
210
|
+
|
|
211
|
+
* If you run `tn search` without a keyword, the CLI will ask you:
|
|
160
212
|
|
|
161
|
-
|
|
213
|
+
1. Enter the keyword you want to search for.
|
|
214
|
+
2. Choose where to search: in the title, description, or both.
|
|
162
215
|
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
* If you use `--find <keyword>` directly, the CLI will return all tasks containing that keyword in the title or description.
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
tn search
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
Enter the keyword you want to search for: meeting
|
|
226
|
+
Where do you want to search? (Use arrow keys)
|
|
227
|
+
1. title
|
|
228
|
+
2. description
|
|
229
|
+
3. both
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
tn search --find "meeting"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
┌─┬────┬────────────┬─────────────┬─────────┬────────────┬───────────────────┐
|
|
237
|
+
│#│ ID │ Title │ Status │ Priority│ DueDate │ Description │
|
|
238
|
+
├─┼────┼────────────┼─────────────┼─────────┼────────────┼───────────────────┤
|
|
239
|
+
│1│ 3 │ Team meeting│ todo │ medium │ 2026-02-01 │ Discuss project │
|
|
240
|
+
└─┴────┴────────────┴─────────────┴─────────┴────────────┴───────────────────┘
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 7. Sort Tasks
|
|
245
|
+
|
|
246
|
+
**Alias:** `so`
|
|
247
|
+
**Description:** Sort tasks interactively by due date, priority, or status.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
tn so
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Behavior:**
|
|
254
|
+
|
|
255
|
+
* Prompts: `Sort tasks by:` → select from `dueDate`, `priority`, `status`.
|
|
256
|
+
* Displays **sorted colored table**.
|
|
257
|
+
* Can also pass `--by` option: `tn so --by priority`
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Task Fields & Allowed Values
|
|
262
|
+
|
|
263
|
+
| Field | Allowed Values / Description |
|
|
264
|
+
| ----------- | ---------------------------- |
|
|
265
|
+
| title | Any non-empty string |
|
|
266
|
+
| status | todo, in-progress, done |
|
|
267
|
+
| priority | low, medium, high |
|
|
268
|
+
| dueDate | YYYY-MM-DD |
|
|
269
|
+
| description | Optional text |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Examples
|
|
274
|
+
|
|
275
|
+
**Adding a Task:**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
tn a
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
Task Title: Buy groceries
|
|
283
|
+
Task Status: → todo
|
|
284
|
+
Task Priority: → medium
|
|
285
|
+
Due Date (YYYY-MM-DD): 2026-02-01
|
|
286
|
+
Task Description (optional): Buy fruits and vegetables
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Listing Tasks:**
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
tn ls
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
┌─┬────┬────────────────┬─────────────┬─────────┬────────────┬─────────────────────────────┐
|
|
297
|
+
│#│ ID │ Title │ Status │ Priority│ DueDate │ Description │
|
|
298
|
+
├─┼────┼────────────────┼─────────────┼─────────┼────────────┼─────────────────────────────┤
|
|
299
|
+
│1│ 1 │ Buy groceries │ todo │ medium │ 2026-02-01 │ Buy fruits and vegetables │
|
|
300
|
+
└─┴────┴────────────────┴─────────────┴─────────┴────────────┴─────────────────────────────┘
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Updating a Task:**
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
tn up
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Deleting a Task:**
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
tn del
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Undo Last Deleted Task:**
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
tn un
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Search Tasks:**
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
tn sr groceries
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Sort Tasks:**
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
tn so
|
|
331
|
+
```
|
|
165
332
|
|
|
166
|
-
|
|
333
|
+
* Select `dueDate`, `priority`, or `status` interactively
|
|
334
|
+
* Or use `--by` option: `tn so --by priority`
|
|
167
335
|
|
|
168
|
-
|
|
336
|
+
---
|
|
169
337
|
|
|
170
|
-
|
|
338
|
+
## Notes
|
|
171
339
|
|
|
172
|
-
|
|
173
|
-
|
|
340
|
+
* All selection prompts use **arrow keys** in terminal.
|
|
341
|
+
* Task table always shows **numbered index (`#`) starting from 1**.
|
|
342
|
+
* Status and priority are displayed in **colors** for better readability.
|
|
174
343
|
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
git clone https://github.com/yourusername/taskninja.git
|
|
178
|
-
cd taskninja
|
|
179
|
-
```
|
|
344
|
+
---
|
|
180
345
|
|
|
181
|
-
|
|
182
|
-
- Use a descriptive name: `git checkout -b feature/new-feature` or `git checkout -b fix/bug-fix`.
|
|
346
|
+
---
|
|
183
347
|
|
|
184
|
-
|
|
185
|
-
- Follow the existing code style (ESLint can be added later for consistency).
|
|
186
|
-
- Add or update tests if applicable.
|
|
187
|
-
- Ensure your code handles errors gracefully and maintains validation logic.
|
|
348
|
+
## Screenshots / Demo
|
|
188
349
|
|
|
189
|
-
|
|
190
|
-
- Use clear commit messages: e.g., "feat: add search functionality" or "fix: resolve date validation issue".
|
|
191
|
-
- Reference issues if relevant: e.g., "Closes #123".
|
|
350
|
+
**Adding a Task:**
|
|
192
351
|
|
|
193
|
-
|
|
194
|
-
```bash
|
|
195
|
-
git push origin feature/new-feature
|
|
196
|
-
```
|
|
352
|
+

|
|
197
353
|
|
|
198
|
-
|
|
199
|
-
- Go to the original repo and click "New Pull Request".
|
|
200
|
-
- Provide a detailed description of your changes, including why they're needed and any relevant screenshots.
|
|
201
|
-
- Link to any related issues.
|
|
354
|
+
**Listing Tasks:**
|
|
202
355
|
|
|
203
|
-
|
|
356
|
+

|
|
204
357
|
|
|
205
|
-
|
|
206
|
-
- **Testing**: Add unit tests using a framework like Jest if expanding features. Currently, no tests are implemented—contributions here are encouraged!
|
|
207
|
-
- **Dependencies**: Keep them minimal. Justify any new additions.
|
|
208
|
-
- **Features**: New commands or options should align with the simple CLI philosophy. Discuss major changes in an issue first.
|
|
209
|
-
- **Documentation**: Update README.md with any new features or changes.
|
|
210
|
-
- **Issues**: Check for existing issues before creating a new one. Use labels like "bug", "enhancement", or "question".
|
|
358
|
+
**Updating a Task:**
|
|
211
359
|
|
|
212
|
-
|
|
360
|
+

|
|
213
361
|
|
|
214
|
-
|
|
215
|
-
- No harassment or discrimination.
|
|
216
|
-
- Report issues to the maintainer (Mohamed Bakr).
|
|
362
|
+
**Deleting a Task:**
|
|
217
363
|
|
|
218
|
-
|
|
364
|
+

|
|
365
|
+

|
|
219
366
|
|
|
220
|
-
|
|
367
|
+
**Restore Last-Deleted Task:**
|
|
221
368
|
|
|
222
|
-
|
|
369
|
+

|
|
223
370
|
|
|
224
|
-
|
|
371
|
+
**Search / Sort Tasks:**
|
|
225
372
|
|
|
226
|
-
|
|
373
|
+

|
|
374
|
+

|
|
375
|
+

|
|
227
376
|
|
|
228
|
-
|
|
377
|
+
**Mark as Done:**
|
|
229
378
|
|
|
230
|
-
|
|
231
|
-
|
|
379
|
+

|
|
380
|
+

|
|
232
381
|
|
|
233
|
-
|
|
382
|
+
---
|
package/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
|
|
3
2
|
/**
|
|
3
|
+
* TASKNINJA -
|
|
4
4
|
* Task Manager CLI Application
|
|
5
5
|
* This application allows users to manage their tasks via command line interface.
|
|
6
6
|
* It supports adding, listing, and removing tasks.
|
|
@@ -9,78 +9,78 @@
|
|
|
9
9
|
* - inquirer: For interactive prompts
|
|
10
10
|
* - fs: For file system operations
|
|
11
11
|
* Author: Mohamed Bakr
|
|
12
|
-
* Date:
|
|
13
|
-
* Version: 1.
|
|
12
|
+
* Date: January 2024
|
|
13
|
+
* Version: 1.1.0
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
// for using commands in terminal
|
|
17
17
|
import { Command } from "commander";
|
|
18
18
|
// for interactive command line prompts
|
|
19
19
|
import inquirer from "inquirer";
|
|
20
|
-
//
|
|
21
|
-
import
|
|
20
|
+
// supporting colors in table forms
|
|
21
|
+
import Table from 'cli-table3';
|
|
22
|
+
// for colored text
|
|
23
|
+
import chalk from 'chalk';
|
|
22
24
|
|
|
23
25
|
// assigning Commander to a variable
|
|
24
26
|
const program = new Command();
|
|
25
27
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const data = await fs.readFile(TASKS_FILE, 'utf8');
|
|
38
|
-
return JSON.parse(data);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
if (error.code === 'ENOENT') return [];
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
// function to save tasks to file
|
|
45
|
-
const saveTasks = async (tasks) => {
|
|
46
|
-
await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
|
|
47
|
-
};
|
|
48
|
-
// get next task ID
|
|
49
|
-
const getNextId = (tasks) => {
|
|
50
|
-
return tasks.length > 0 ? Math.max(...tasks.map(task => task.id)) + 1 : 1;
|
|
51
|
-
};
|
|
52
|
-
// validate task status
|
|
53
|
-
const validateStatus = (status) => {
|
|
54
|
-
if (!ALLOWED_STATUSES.includes(status)) {
|
|
55
|
-
throw new Error(`Invalid status. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
// validate task priority
|
|
59
|
-
const validatePriority = (priority) => {
|
|
60
|
-
if (!ALLOWED_PRIORITIES.includes(priority)) {
|
|
61
|
-
throw new Error(`Invalid priority. Allowed priorities are: ${ALLOWED_PRIORITIES.join(", ")}`);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
// verify due date format
|
|
65
|
-
const validateDueDate = (dueDate) => {
|
|
66
|
-
if (isNaN(Date.parse(dueDate))) {
|
|
67
|
-
throw new Error("Invalid due date. Please use a valid date format (YYYY-MM-DD).");
|
|
28
|
+
// importing validators and allowed values
|
|
29
|
+
import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "./utils/validators.js";
|
|
30
|
+
// importing task service functions
|
|
31
|
+
import { loadTasks, saveTasks, getNextId, saveDeletedTask, loadDeletedTask, clearDeletedTask } from "./utils/taskService.js";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// helper function to display tasks in colored table
|
|
35
|
+
const displayTasks = (tasks) => {
|
|
36
|
+
if (!tasks.length) {
|
|
37
|
+
console.log(chalk.yellow("No tasks to display."));
|
|
38
|
+
return;
|
|
68
39
|
}
|
|
69
|
-
};
|
|
70
40
|
|
|
41
|
+
const table = new Table({
|
|
42
|
+
head: [
|
|
43
|
+
chalk.cyanBright('#'),
|
|
44
|
+
chalk.cyanBright('ID'),
|
|
45
|
+
chalk.cyanBright('Title'),
|
|
46
|
+
chalk.cyanBright('Status'),
|
|
47
|
+
chalk.cyanBright('Priority'),
|
|
48
|
+
chalk.cyanBright('DueDate'),
|
|
49
|
+
chalk.cyanBright('Description')
|
|
50
|
+
],
|
|
51
|
+
colWidths: [4, 4, 30, 12, 10, 12, 60]
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
tasks.forEach((task, index) => {
|
|
55
|
+
table.push([
|
|
56
|
+
index + 1,
|
|
57
|
+
task.id,
|
|
58
|
+
task.title,
|
|
59
|
+
task.status === "done" ? chalk.green(task.status)
|
|
60
|
+
: task.status === "in-progress" ? chalk.yellow(task.status)
|
|
61
|
+
: chalk.blue(task.status),
|
|
62
|
+
task.priority === "high" ? chalk.red(task.priority)
|
|
63
|
+
: task.priority === "medium" ? chalk.yellow(task.priority)
|
|
64
|
+
: chalk.green(task.priority),
|
|
65
|
+
task.dueDate,
|
|
66
|
+
task.description || ''
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
71
69
|
|
|
70
|
+
console.log(table.toString());
|
|
71
|
+
};
|
|
72
72
|
// setting up
|
|
73
73
|
program
|
|
74
|
-
.name("
|
|
74
|
+
.name("taskninja")
|
|
75
75
|
.description("A simple CLI application to manage your tasks")
|
|
76
|
-
.version("1.
|
|
76
|
+
.version("1.1.0");
|
|
77
77
|
|
|
78
78
|
// use command 'add' with title + status + priority + dueDate + description and action
|
|
79
79
|
program
|
|
80
80
|
.command('add')
|
|
81
81
|
.alias('a')
|
|
82
82
|
.description('Add a new task')
|
|
83
|
-
.action(async (
|
|
83
|
+
.action(async () => {
|
|
84
84
|
const answers = await inquirer.prompt([
|
|
85
85
|
{
|
|
86
86
|
type: 'input',
|
|
@@ -89,14 +89,14 @@ program
|
|
|
89
89
|
validate : input => input ? true : 'Title cannot be empty!'
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
|
-
type: '
|
|
92
|
+
type: 'rawlist',
|
|
93
93
|
name: 'status',
|
|
94
94
|
message : 'Task Status:',
|
|
95
95
|
choices : ALLOWED_STATUSES,
|
|
96
96
|
default : 'todo'
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
|
-
type: '
|
|
99
|
+
type: 'rawlist',
|
|
100
100
|
name: 'priority',
|
|
101
101
|
message : 'Task Priority:',
|
|
102
102
|
choices : ALLOWED_PRIORITIES,
|
|
@@ -119,7 +119,7 @@ program
|
|
|
119
119
|
type: 'input',
|
|
120
120
|
name: 'description',
|
|
121
121
|
message : 'Task Description (optional):'
|
|
122
|
-
}
|
|
122
|
+
},
|
|
123
123
|
]);
|
|
124
124
|
// load existing tasks
|
|
125
125
|
const tasks = await loadTasks();
|
|
@@ -137,7 +137,7 @@ program
|
|
|
137
137
|
tasks.push(newTask);
|
|
138
138
|
// save updated tasks to file
|
|
139
139
|
await saveTasks(tasks);
|
|
140
|
-
console.log(
|
|
140
|
+
console.log(chalk.green('Task added successfully!'));
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
|
|
@@ -146,102 +146,332 @@ program
|
|
|
146
146
|
.command('list')
|
|
147
147
|
.alias('ls')
|
|
148
148
|
.description('List all tasks')
|
|
149
|
-
.option('-s, --status <status>', 'Filter tasks by status')
|
|
149
|
+
.option('-s, --status <status>', 'Filter tasks by status (todo, in-progress, done)')
|
|
150
150
|
.action(async (options) => {
|
|
151
151
|
let tasks = await loadTasks();
|
|
152
152
|
|
|
153
153
|
if (options.status) {
|
|
154
154
|
if (!ALLOWED_STATUSES.includes(options.status)){
|
|
155
|
-
console.
|
|
156
|
-
|
|
155
|
+
console.log(chalk.red(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`));
|
|
156
|
+
return;
|
|
157
157
|
}
|
|
158
158
|
tasks = tasks.filter(task => task.status === options.status);
|
|
159
159
|
}
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// display all tasks in table format
|
|
161
|
+
displayTasks(tasks);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// use command 'search' to find tasks by keyword in title or description
|
|
167
|
+
program
|
|
168
|
+
.command('search [keyword]')
|
|
169
|
+
.alias('sr')
|
|
170
|
+
.option('-f, --find <keyword>', 'Keyword to search in title or description')
|
|
171
|
+
.description('Search tasks by keyword in title or description')
|
|
172
|
+
.action(async (keyword, options) => {
|
|
173
|
+
const tasks = await loadTasks();
|
|
174
|
+
let searchTerm = keyword || options.find;
|
|
175
|
+
|
|
176
|
+
if (!searchTerm) {
|
|
177
|
+
const answers = await inquirer.prompt([
|
|
178
|
+
{
|
|
179
|
+
type: 'input',
|
|
180
|
+
name: 'term',
|
|
181
|
+
message: 'Enter the keyword you want to search for:',
|
|
182
|
+
validate: input => input ? true : 'Keyword cannot be empty!'
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'rawlist',
|
|
186
|
+
name: 'field',
|
|
187
|
+
message: 'Where do you want to search?',
|
|
188
|
+
choices: ['title', 'description', 'both'],
|
|
189
|
+
default: 'both'
|
|
190
|
+
}
|
|
191
|
+
]);
|
|
192
|
+
searchTerm = answers.term;
|
|
193
|
+
const field = answers.field;
|
|
194
|
+
|
|
195
|
+
const founded = tasks.filter(task => {
|
|
196
|
+
if (field === 'title') return task.title.toLowerCase().includes(searchTerm.toLowerCase());
|
|
197
|
+
if (field === 'description') return task.description.toLowerCase().includes(searchTerm.toLowerCase());
|
|
198
|
+
return task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
199
|
+
task.description.toLowerCase().includes(searchTerm.toLowerCase());
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (founded.length === 0) {
|
|
203
|
+
console.log(chalk.red('No task matched your search!'));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
displayTasks(founded);
|
|
162
208
|
return;
|
|
163
209
|
}
|
|
164
|
-
// map all tasks to displayable objects
|
|
165
|
-
const tableData = tasks.map(task => ({
|
|
166
|
-
ID: task.id,
|
|
167
|
-
Title: task.title,
|
|
168
|
-
Status: task.status,
|
|
169
|
-
Priority: task.priority,
|
|
170
|
-
DueDate: task.dueDate,
|
|
171
|
-
Description: task.description || ''
|
|
172
|
-
}));
|
|
173
210
|
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
const founded = tasks.filter(task =>
|
|
212
|
+
task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
213
|
+
task.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (founded.length === 0) {
|
|
217
|
+
console.log(chalk.red('No task matched your search!'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
displayTasks(founded);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
// use command 'sort' to sort tasks by due date, priority, or status
|
|
228
|
+
program
|
|
229
|
+
.command('sort')
|
|
230
|
+
.alias('so')
|
|
231
|
+
.option('--by <criteria>', 'Sort tasks by criteria (dueDate, priority, status)')
|
|
232
|
+
.description('Sort tasks by due date, priority, or status')
|
|
233
|
+
.action( async (options) => {
|
|
234
|
+
|
|
235
|
+
const tasks = await loadTasks();
|
|
236
|
+
let criteria = options.by;
|
|
237
|
+
|
|
238
|
+
if (!criteria) {
|
|
239
|
+
const answer = await inquirer.prompt([
|
|
240
|
+
{
|
|
241
|
+
type: 'rawlist',
|
|
242
|
+
name: 'criteria',
|
|
243
|
+
message: 'Sort tasks by:',
|
|
244
|
+
choices: ['dueDate', 'priority', 'status']
|
|
245
|
+
}
|
|
246
|
+
]);
|
|
247
|
+
criteria = answer.criteria;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if (!['dueDate', 'priority', 'status'].includes(criteria)) {
|
|
252
|
+
console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// sorting logic
|
|
256
|
+
const sortedTasks = [...tasks];
|
|
257
|
+
|
|
258
|
+
switch (criteria) {
|
|
259
|
+
case 'dueDate' || 'duedate' || 'DueDate' || 'Duedate' || 'due date' || 'Due date' || 'due-date':
|
|
260
|
+
sortedTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
|
|
261
|
+
break;
|
|
262
|
+
case 'priority' || 'Priority':
|
|
263
|
+
const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
|
|
264
|
+
sortedTasks.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
265
|
+
break;
|
|
266
|
+
case 'status' || 'Status':
|
|
267
|
+
const statusOrder = { 'todo': 1, 'in-progress': 2, 'done': 3 };
|
|
268
|
+
sortedTasks.sort((a, b) => statusOrder[a.priority] - statusOrder[b.priority]);
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
console.log(chalk.red('Invalid sort criteria. Use dueDate, priority, or status.'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
displayTasks(sortedTasks);
|
|
176
276
|
});
|
|
177
277
|
|
|
278
|
+
|
|
178
279
|
// use command 'update' with task ID and action
|
|
179
280
|
program
|
|
180
281
|
.command('update')
|
|
181
282
|
.alias('up')
|
|
182
|
-
.description('Update a task by ID')
|
|
283
|
+
.description('Update a task by ==> ID <==')
|
|
183
284
|
.action(async () =>{
|
|
184
285
|
const tasks = await loadTasks();
|
|
185
286
|
if (tasks.length === 0) {
|
|
186
|
-
console.log('No tasks found to update.');
|
|
287
|
+
console.log(chalk.red('No tasks found to update.'));
|
|
187
288
|
return;
|
|
188
289
|
}
|
|
189
290
|
|
|
190
291
|
const { id } = await inquirer.prompt([
|
|
191
292
|
{
|
|
192
|
-
type: '
|
|
293
|
+
type: 'rawlist',
|
|
193
294
|
name: 'id',
|
|
194
|
-
message: 'Select the task to update:',
|
|
295
|
+
message: 'Select the task to update (By ID):',
|
|
195
296
|
choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
|
|
196
297
|
}
|
|
197
298
|
]);
|
|
198
|
-
const { status } = await inquirer.prompt([
|
|
199
|
-
{
|
|
200
|
-
type: 'list',
|
|
201
|
-
name: 'status',
|
|
202
|
-
message: 'Select the new status:',
|
|
203
|
-
choices: ALLOWED_STATUSES
|
|
204
|
-
}
|
|
205
|
-
]);
|
|
206
299
|
|
|
300
|
+
// find the task to update
|
|
207
301
|
const task = tasks.find(t => t.id === Number(id));
|
|
208
302
|
if (!task) {
|
|
209
|
-
console.
|
|
303
|
+
console.log(chalk.red('Task not found!'));
|
|
210
304
|
return;
|
|
211
305
|
}
|
|
212
|
-
task.status = status;
|
|
213
306
|
|
|
214
|
-
await
|
|
215
|
-
|
|
307
|
+
const answers = await inquirer.prompt([
|
|
308
|
+
// for title
|
|
309
|
+
{
|
|
310
|
+
type: 'confirm',
|
|
311
|
+
name: 'changeTitle',
|
|
312
|
+
message: 'Do you want to change the title?',
|
|
313
|
+
default: false
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
type: 'input',
|
|
317
|
+
name: 'title',
|
|
318
|
+
message: 'Enter the new title:',
|
|
319
|
+
when: answers => answers.changeTitle,
|
|
320
|
+
validate: input => input ? true : 'Title cannot be empty!'
|
|
321
|
+
},
|
|
216
322
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
323
|
+
// for status
|
|
324
|
+
{
|
|
325
|
+
type: 'confirm',
|
|
326
|
+
name: 'changeStatus',
|
|
327
|
+
message: 'Do you want to change the status?',
|
|
328
|
+
default: false
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: 'rawlist',
|
|
332
|
+
name: 'status',
|
|
333
|
+
message: 'Select the new status:',
|
|
334
|
+
choices: ALLOWED_STATUSES,
|
|
335
|
+
when: answers => answers.changeStatus
|
|
336
|
+
},
|
|
225
337
|
|
|
226
|
-
|
|
227
|
-
|
|
338
|
+
// for priority
|
|
339
|
+
{
|
|
340
|
+
type: 'confirm',
|
|
341
|
+
name: 'changePriority',
|
|
342
|
+
message: 'Do you want to change the priority?',
|
|
343
|
+
default: false
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
type: 'rawlist',
|
|
347
|
+
name: 'priority',
|
|
348
|
+
message: 'Select the new priority:',
|
|
349
|
+
choices: ALLOWED_PRIORITIES,
|
|
350
|
+
when: answers => answers.changePriority
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// for due date
|
|
354
|
+
{
|
|
355
|
+
type: 'confirm',
|
|
356
|
+
name: 'changeDueDate',
|
|
357
|
+
message: 'Do you want to change the due date?',
|
|
358
|
+
default: false
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
type: 'input',
|
|
362
|
+
name: 'dueDate',
|
|
363
|
+
message: 'Enter the new due date (YYYY-MM-DD):',
|
|
364
|
+
when: answers => answers.changeDueDate,
|
|
365
|
+
validate: input => {
|
|
366
|
+
try {
|
|
367
|
+
validateDueDate(input);
|
|
368
|
+
return true;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return error.message;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// for description
|
|
376
|
+
{
|
|
377
|
+
type: 'confirm',
|
|
378
|
+
name: 'changeDescription',
|
|
379
|
+
message: 'Do you want to change the description?',
|
|
380
|
+
default: false
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
type: "rawlist",
|
|
384
|
+
name: 'description',
|
|
385
|
+
message: 'Enter the new description:',
|
|
386
|
+
when: answers => answers.changeDescription
|
|
387
|
+
}
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
// apply updates only if user chose to change them
|
|
391
|
+
if (answers.changeTitle) task.title = answers.title;
|
|
392
|
+
if (answers.changeStatus) task.status = answers.status;
|
|
393
|
+
if (answers.changePriority) task.priority = answers.priority;
|
|
394
|
+
if (answers.changeDueDate) task.dueDate = answers.dueDate;
|
|
395
|
+
if (answers.changeDescription)
|
|
396
|
+
task.description = answers.description || '';
|
|
397
|
+
|
|
398
|
+
// is there any change?
|
|
399
|
+
const hasChanges = [
|
|
400
|
+
answers.changeTitle,
|
|
401
|
+
answers.changeStatus,
|
|
402
|
+
answers.changePriority,
|
|
403
|
+
answers.changeDueDate,
|
|
404
|
+
answers.changeDescription
|
|
405
|
+
].some(Boolean);
|
|
406
|
+
|
|
407
|
+
if (!hasChanges) {
|
|
408
|
+
console.log(chalk.yellow('No changes were made.'));
|
|
409
|
+
}else {
|
|
410
|
+
// save updated tasks to file
|
|
411
|
+
await saveTasks(tasks);
|
|
412
|
+
console.log(chalk.green('Task updated successfully!'));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
displayTasks(tasks);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
// use command 'done' with task ID instead of 'update' + confirm to mark task as done
|
|
420
|
+
program
|
|
421
|
+
.command('done')
|
|
422
|
+
.description('Mark a task as done by ==> ID <==')
|
|
423
|
+
.action(async () => {
|
|
424
|
+
const tasks = await loadTasks();
|
|
425
|
+
if (tasks.length === 0) {
|
|
426
|
+
console.log(chalk.magenta('Congratulations! All tasks are already done.'));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const activeTasks = tasks.filter(t => t.status !== 'done');
|
|
431
|
+
if (activeTasks.length === 0) {
|
|
432
|
+
console.log(chalk.magenta('Congratulations! All tasks are already done.'));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const { id } = await inquirer.prompt([
|
|
437
|
+
{
|
|
438
|
+
type: 'rawlist',
|
|
439
|
+
name: 'id',
|
|
440
|
+
message: 'Select the task to mark as done:',
|
|
441
|
+
choices: activeTasks.map(t => (
|
|
442
|
+
{
|
|
443
|
+
name: `${t.id}: ${t.title}[Current Status: ${t.status}]`,
|
|
444
|
+
value: t.id
|
|
445
|
+
}
|
|
446
|
+
))
|
|
447
|
+
}
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
const task = tasks.find(t => t.id === Number(id));
|
|
451
|
+
task.status = 'done';
|
|
452
|
+
await saveTasks(tasks);
|
|
453
|
+
console.log(chalk.green('Task marked as done successfully!'));
|
|
454
|
+
|
|
455
|
+
displayTasks(tasks);
|
|
228
456
|
});
|
|
229
457
|
|
|
458
|
+
|
|
459
|
+
|
|
230
460
|
// use command 'delete' with task ID and action
|
|
231
461
|
program
|
|
232
462
|
.command('delete')
|
|
233
463
|
.alias('del')
|
|
234
|
-
.description('delete a task by ID')
|
|
464
|
+
.description('delete a task by ==> ID <==')
|
|
235
465
|
.action(async () => {
|
|
236
466
|
const tasks = await loadTasks();
|
|
237
467
|
if (tasks.length === 0) {
|
|
238
|
-
console.log('No tasks found to delete.');
|
|
468
|
+
console.log(chalk.yellow('No tasks found to delete.'));
|
|
239
469
|
return;
|
|
240
470
|
}
|
|
241
471
|
|
|
242
472
|
const { id } = await inquirer.prompt([
|
|
243
473
|
{
|
|
244
|
-
type: '
|
|
474
|
+
type: 'rawlist',
|
|
245
475
|
name: 'id',
|
|
246
476
|
message: 'Select the task to delete:',
|
|
247
477
|
choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
|
|
@@ -257,26 +487,44 @@ program
|
|
|
257
487
|
}
|
|
258
488
|
]);
|
|
259
489
|
if (!confirm) {
|
|
260
|
-
console.log('Task deletion cancelled.');
|
|
490
|
+
console.log(chalk.yellow('Task deletion cancelled.'));
|
|
261
491
|
return;
|
|
262
492
|
}
|
|
493
|
+
const taskToDelete = tasks.find(t => t.id === Number(id));
|
|
494
|
+
// save deleted task for undo functionality
|
|
495
|
+
await saveDeletedTask(taskToDelete);
|
|
496
|
+
// filter out the deleted task
|
|
497
|
+
const newTasks = tasks.filter(t => t.id !== Number(id));
|
|
263
498
|
|
|
264
|
-
const newTasks = tasks.filter(t => t.id !== id);
|
|
265
499
|
await saveTasks(newTasks);
|
|
266
|
-
console.log('Task deleted successfully!');
|
|
267
|
-
|
|
268
|
-
const tableData = tasks.map(task => ({
|
|
269
|
-
ID: task.id,
|
|
270
|
-
Title: task.title,
|
|
271
|
-
Status: task.status,
|
|
272
|
-
Priority: task.priority,
|
|
273
|
-
DueDate: task.dueDate,
|
|
274
|
-
Description: task.description || ''
|
|
275
|
-
}));
|
|
500
|
+
console.log(chalk.green('Task deleted successfully!'));
|
|
501
|
+
console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
|
|
276
502
|
|
|
277
|
-
|
|
278
|
-
console.table(tableData);
|
|
503
|
+
displayTasks(newTasks);
|
|
279
504
|
});
|
|
280
505
|
|
|
506
|
+
// use command 'undo' to restore last deleted task
|
|
507
|
+
program
|
|
508
|
+
.command('undo')
|
|
509
|
+
.alias('un')
|
|
510
|
+
.description('Undo the last deleted task')
|
|
511
|
+
.action( async () => {
|
|
512
|
+
const lastDeletedTask = await loadDeletedTask();
|
|
513
|
+
if (!lastDeletedTask) {
|
|
514
|
+
console.log(chalk.yellow('No deleted task to restore.'));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const tasks = await loadTasks();
|
|
519
|
+
tasks.push(lastDeletedTask);
|
|
520
|
+
tasks.sort((a, b) => a.id - b.id); // keep tasks sorted by ID
|
|
521
|
+
await saveTasks(tasks);
|
|
522
|
+
await clearDeletedTask();
|
|
523
|
+
|
|
524
|
+
console.log(chalk.green(`Last deleted task restored successfully!, (Task name: ${lastDeletedTask.title})`));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
|
|
281
529
|
// parse command line arguments
|
|
282
530
|
program.parse(process.argv);
|
package/images/add.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/images/done.png
ADDED
|
Binary file
|
package/images/done2.png
ADDED
|
Binary file
|
package/images/list.png
ADDED
|
Binary file
|
|
Binary file
|
package/images/sort.png
ADDED
|
Binary file
|
package/images/sort2.png
ADDED
|
Binary file
|
package/images/undo.png
ADDED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskninja",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "a simple CLI application with JS as a (To-Do Application)",
|
|
5
5
|
"main": "app.js",
|
|
6
|
-
"type"
|
|
6
|
+
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
9
|
"start": "node app.js",
|
|
10
10
|
"dev": "node app.js"
|
|
11
11
|
},
|
|
12
|
-
"bin"
|
|
12
|
+
"bin": {
|
|
13
13
|
"dotask": "./app.js",
|
|
14
14
|
"taskninja": "./app.js",
|
|
15
15
|
"tn": "./app.js"
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"author": "Moahmed Bakr",
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"chalk": "^5.6.2",
|
|
21
|
+
"cli-table3": "^0.6.5",
|
|
20
22
|
"commander": "^14.0.2",
|
|
21
23
|
"inquirer": "^13.2.1"
|
|
22
24
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// for file system operations
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// file to store tasks
|
|
6
|
+
const TASKS_FILE = "./todos.json";
|
|
7
|
+
|
|
8
|
+
// file to temporarily store deleted tasks for undo functionality
|
|
9
|
+
export const DELETED_TASKS_FILE = "./deleted_todos.json";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @description this section responsible to store tasks in todo.json
|
|
14
|
+
*/
|
|
15
|
+
// function to load tasks from file
|
|
16
|
+
export const loadTasks = async () => {
|
|
17
|
+
try{
|
|
18
|
+
const data = await fs.readFile(TASKS_FILE, 'utf8');
|
|
19
|
+
return JSON.parse(data);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT') return [];
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
// function to save tasks to file
|
|
26
|
+
export const saveTasks = async (tasks) => {
|
|
27
|
+
await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
|
|
28
|
+
};
|
|
29
|
+
// get next task ID
|
|
30
|
+
export const getNextId = (tasks) => {
|
|
31
|
+
return tasks.length > 0 ? Math.max(...tasks.map(task => task.id)) + 1 : 1;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @description this section responsible to store deleted tasks in deleted_todos.json
|
|
36
|
+
* for undo functionality
|
|
37
|
+
*/
|
|
38
|
+
// to save deleted last-deleted task
|
|
39
|
+
export const saveDeletedTask = async (task) => {
|
|
40
|
+
await fs.writeFile(DELETED_TASKS_FILE, JSON.stringify(task, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// to load deleted last-deleted task
|
|
44
|
+
export const loadDeletedTask = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const data = await fs.readFile(DELETED_TASKS_FILE, 'utf8');
|
|
47
|
+
return JSON.parse(data);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// to clear deleted tasks file after undo
|
|
55
|
+
export const clearDeletedTask = async () => {
|
|
56
|
+
try {
|
|
57
|
+
await fs.unlink(DELETED_TASKS_FILE);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code !== 'ENOENT') {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
// allowed task statuses
|
|
3
|
+
const ALLOWED_STATUSES = ["todo", "in-progress", "done"];
|
|
4
|
+
// allowed task priorities
|
|
5
|
+
const ALLOWED_PRIORITIES = ["low", "medium", "high"];
|
|
6
|
+
|
|
7
|
+
// validate task status
|
|
8
|
+
export const validateStatus = (status) => {
|
|
9
|
+
if (!ALLOWED_STATUSES.includes(status)) {
|
|
10
|
+
throw new Error(`Invalid status. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
// validate task priority
|
|
14
|
+
export const validatePriority = (priority) => {
|
|
15
|
+
if (!ALLOWED_PRIORITIES.includes(priority)) {
|
|
16
|
+
throw new Error(`Invalid priority. Allowed priorities are: ${ALLOWED_PRIORITIES.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// verify due date format
|
|
20
|
+
export const validateDueDate = (dueDate) => {
|
|
21
|
+
if (isNaN(Date.parse(dueDate))) {
|
|
22
|
+
throw new Error("Invalid due date. Please use a valid date format (YYYY-MM-DD).");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { ALLOWED_PRIORITIES, ALLOWED_STATUSES };
|