taskninja 1.0.4 → 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 +185 -52
- package/app.js +245 -55
- 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/taskService.js +0 -25
- package/todos.json +0 -1
- /package/{validators.js → utils/validators.js} +0 -0
package/README.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
|
|
2
1
|
# TaskNinja
|
|
3
2
|
|
|
4
|
-
**Version:** 1.0
|
|
5
|
-
**Description:** A simple CLI To-Do Application to manage your tasks directly from the terminal.
|
|
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.
|
|
6
5
|
|
|
7
6
|
---
|
|
8
7
|
|
|
9
8
|
## Table of Contents
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
|
|
10
|
+
* [Installation](#installation)
|
|
11
|
+
* [Running the CLI](#running-the-cli)
|
|
12
|
+
* [Commands](#commands)
|
|
13
|
+
|
|
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)
|
|
19
23
|
|
|
20
24
|
---
|
|
21
25
|
|
|
@@ -30,24 +34,19 @@ npm install -g taskninja
|
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
or use it instantly with:
|
|
33
|
-
```bash
|
|
34
|
-
npx taskninja
|
|
35
|
-
```
|
|
36
37
|
|
|
37
|
-
and then use it:
|
|
38
38
|
```bash
|
|
39
|
+
npx taskninja
|
|
39
40
|
npx taskninja <command>
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
After installation, you can run the CLI using the `dotask` or `taskninja` or `tn` command (as defined in `package.json`).
|
|
43
|
-
|
|
44
43
|
Clone the repository and install dependencies:
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
46
|
git clone <your-repo-url>
|
|
48
47
|
cd taskninja
|
|
49
48
|
npm install
|
|
50
|
-
|
|
49
|
+
```
|
|
51
50
|
|
|
52
51
|
---
|
|
53
52
|
|
|
@@ -65,7 +64,7 @@ taskninja <command>
|
|
|
65
64
|
tn <command>
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
Replace `<command>` with any of the available commands: `add`, `list`, `update`, `delete`.
|
|
67
|
+
Replace `<command>` with any of the available commands: `add`, `list`, `update`, `delete`, `undo`, `search`, `sort`.
|
|
69
68
|
|
|
70
69
|
---
|
|
71
70
|
|
|
@@ -77,28 +76,30 @@ Replace `<command>` with any of the available commands: `add`, `list`, `update`,
|
|
|
77
76
|
**Description:** Add a new task interactively.
|
|
78
77
|
|
|
79
78
|
```bash
|
|
80
|
-
node app.js add
|
|
81
|
-
# or
|
|
82
79
|
tn a
|
|
83
80
|
```
|
|
84
81
|
|
|
85
82
|
**Prompts:**
|
|
86
83
|
|
|
87
84
|
1. **Task Title:** Enter any text (cannot be empty).
|
|
85
|
+
|
|
88
86
|
2. **Task Status:** Select from allowed values using arrows:
|
|
89
87
|
|
|
90
88
|
* todo
|
|
91
89
|
* in-progress
|
|
92
90
|
* done
|
|
93
|
-
|
|
91
|
+
|
|
92
|
+
3. **Task Priority:** Select from allowed values:
|
|
94
93
|
|
|
95
94
|
* low
|
|
96
95
|
* medium
|
|
97
96
|
* high
|
|
97
|
+
|
|
98
98
|
4. **Due Date:** Enter in `YYYY-MM-DD` format. Invalid dates will show a validation error.
|
|
99
|
+
|
|
99
100
|
5. **Description:** Optional text.
|
|
100
101
|
|
|
101
|
-
**
|
|
102
|
+
**Behavior:**
|
|
102
103
|
|
|
103
104
|
* Task is saved in `todos.json`.
|
|
104
105
|
* Confirmation message: `Task added successfully!`
|
|
@@ -111,25 +112,19 @@ tn a
|
|
|
111
112
|
**Description:** List all tasks, optionally filtered by status.
|
|
112
113
|
|
|
113
114
|
```bash
|
|
114
|
-
node app.js list
|
|
115
|
-
# or
|
|
116
115
|
tn ls
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
**Optional Status Filter:**
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
node app.js list --status todo
|
|
116
|
+
tn ls --status todo
|
|
123
117
|
```
|
|
124
118
|
|
|
125
119
|
**Behavior:**
|
|
126
120
|
|
|
127
|
-
* Displays tasks in a table
|
|
121
|
+
* Displays tasks in a **colored table** using chalk:
|
|
128
122
|
|
|
129
123
|
| # | ID | Title | Status | Priority | DueDate | Description |
|
|
130
124
|
| - | -- | ----- | ------ | -------- | ------- | ----------- |
|
|
131
125
|
|
|
132
|
-
*
|
|
126
|
+
* Status colors: `todo` → blue, `in-progress` → yellow, `done` → green
|
|
127
|
+
* Priority colors: `low` → green, `medium` → yellow, `high` → red
|
|
133
128
|
|
|
134
129
|
---
|
|
135
130
|
|
|
@@ -139,46 +134,127 @@ node app.js list --status todo
|
|
|
139
134
|
**Description:** Update an existing task by selecting its ID.
|
|
140
135
|
|
|
141
136
|
```bash
|
|
142
|
-
node app.js update
|
|
143
|
-
# or
|
|
144
137
|
tn up
|
|
145
138
|
```
|
|
146
139
|
|
|
147
140
|
**Steps:**
|
|
148
141
|
|
|
149
|
-
1. Select task by ID from a list
|
|
142
|
+
1. Select task by ID from a list.
|
|
150
143
|
2. Confirm which fields to change (title, status, priority, due date, description).
|
|
151
|
-
3. Enter new values where applicable
|
|
144
|
+
3. Enter new values where applicable.
|
|
152
145
|
|
|
153
146
|
**Behavior:**
|
|
154
147
|
|
|
155
148
|
* Only fields selected for change are updated.
|
|
156
149
|
* Confirmation message: `Task updated successfully!`
|
|
157
|
-
* Shows updated task table
|
|
150
|
+
* Shows updated **colored task table**.
|
|
158
151
|
|
|
159
152
|
---
|
|
160
153
|
|
|
161
154
|
### 4. Delete Task
|
|
162
155
|
|
|
163
156
|
**Alias:** `del`
|
|
164
|
-
**Description:**
|
|
157
|
+
**Description:** Soft delete a task by selecting its ID.
|
|
165
158
|
|
|
166
159
|
```bash
|
|
167
|
-
node app.js delete
|
|
168
|
-
# or
|
|
169
160
|
tn del
|
|
170
161
|
```
|
|
171
162
|
|
|
172
163
|
**Steps:**
|
|
173
164
|
|
|
174
|
-
1. Select task by ID from a list
|
|
165
|
+
1. Select task by ID from a list.
|
|
175
166
|
2. Confirm deletion.
|
|
176
167
|
|
|
177
168
|
**Behavior:**
|
|
178
169
|
|
|
179
|
-
* Task is removed from `todos.json
|
|
180
|
-
* Confirmation
|
|
181
|
-
*
|
|
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:
|
|
212
|
+
|
|
213
|
+
1. Enter the keyword you want to search for.
|
|
214
|
+
2. Choose where to search: in the title, description, or both.
|
|
215
|
+
|
|
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`
|
|
182
258
|
|
|
183
259
|
---
|
|
184
260
|
|
|
@@ -230,20 +306,77 @@ tn ls
|
|
|
230
306
|
tn up
|
|
231
307
|
```
|
|
232
308
|
|
|
233
|
-
* Select task ID → confirm fields → update values
|
|
234
|
-
* Shows updated table.
|
|
235
|
-
|
|
236
309
|
**Deleting a Task:**
|
|
237
310
|
|
|
238
311
|
```bash
|
|
239
312
|
tn del
|
|
240
313
|
```
|
|
241
314
|
|
|
242
|
-
|
|
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
|
+
```
|
|
332
|
+
|
|
333
|
+
* Select `dueDate`, `priority`, or `status` interactively
|
|
334
|
+
* Or use `--by` option: `tn so --by priority`
|
|
243
335
|
|
|
244
336
|
---
|
|
245
337
|
|
|
246
338
|
## Notes
|
|
247
339
|
|
|
248
|
-
* All selection prompts
|
|
249
|
-
* Task table always shows numbered index (`#`) starting from 1
|
|
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.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Screenshots / Demo
|
|
349
|
+
|
|
350
|
+
**Adding a Task:**
|
|
351
|
+
|
|
352
|
+

|
|
353
|
+
|
|
354
|
+
**Listing Tasks:**
|
|
355
|
+
|
|
356
|
+

|
|
357
|
+
|
|
358
|
+
**Updating a Task:**
|
|
359
|
+
|
|
360
|
+

|
|
361
|
+
|
|
362
|
+
**Deleting a Task:**
|
|
363
|
+
|
|
364
|
+

|
|
365
|
+

|
|
366
|
+
|
|
367
|
+
**Restore Last-Deleted Task:**
|
|
368
|
+
|
|
369
|
+

|
|
370
|
+
|
|
371
|
+
**Search / Sort Tasks:**
|
|
372
|
+
|
|
373
|
+

|
|
374
|
+

|
|
375
|
+

|
|
376
|
+
|
|
377
|
+
**Mark as Done:**
|
|
378
|
+
|
|
379
|
+

|
|
380
|
+

|
|
381
|
+
|
|
382
|
+
---
|
package/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
+
* TASKNINJA -
|
|
3
4
|
* Task Manager CLI Application
|
|
4
5
|
* This application allows users to manage their tasks via command line interface.
|
|
5
6
|
* It supports adding, listing, and removing tasks.
|
|
@@ -8,29 +9,71 @@
|
|
|
8
9
|
* - inquirer: For interactive prompts
|
|
9
10
|
* - fs: For file system operations
|
|
10
11
|
* Author: Mohamed Bakr
|
|
11
|
-
* Date:
|
|
12
|
-
* Version: 1.
|
|
12
|
+
* Date: January 2024
|
|
13
|
+
* Version: 1.1.0
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
// for using commands in terminal
|
|
16
17
|
import { Command } from "commander";
|
|
17
18
|
// for interactive command line prompts
|
|
18
19
|
import inquirer from "inquirer";
|
|
20
|
+
// supporting colors in table forms
|
|
21
|
+
import Table from 'cli-table3';
|
|
22
|
+
// for colored text
|
|
23
|
+
import chalk from 'chalk';
|
|
19
24
|
|
|
20
25
|
// assigning Commander to a variable
|
|
21
26
|
const program = new Command();
|
|
22
27
|
|
|
23
28
|
// importing validators and allowed values
|
|
24
|
-
import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "./validators.js";
|
|
29
|
+
import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "./utils/validators.js";
|
|
25
30
|
// importing task service functions
|
|
26
|
-
import { loadTasks, saveTasks, getNextId } from "./taskService.js";
|
|
31
|
+
import { loadTasks, saveTasks, getNextId, saveDeletedTask, loadDeletedTask, clearDeletedTask } from "./utils/taskService.js";
|
|
27
32
|
|
|
28
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;
|
|
39
|
+
}
|
|
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
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(table.toString());
|
|
71
|
+
};
|
|
29
72
|
// setting up
|
|
30
73
|
program
|
|
31
|
-
.name("
|
|
74
|
+
.name("taskninja")
|
|
32
75
|
.description("A simple CLI application to manage your tasks")
|
|
33
|
-
.version("1.0
|
|
76
|
+
.version("1.1.0");
|
|
34
77
|
|
|
35
78
|
// use command 'add' with title + status + priority + dueDate + description and action
|
|
36
79
|
program
|
|
@@ -76,7 +119,7 @@ program
|
|
|
76
119
|
type: 'input',
|
|
77
120
|
name: 'description',
|
|
78
121
|
message : 'Task Description (optional):'
|
|
79
|
-
}
|
|
122
|
+
},
|
|
80
123
|
]);
|
|
81
124
|
// load existing tasks
|
|
82
125
|
const tasks = await loadTasks();
|
|
@@ -94,7 +137,7 @@ program
|
|
|
94
137
|
tasks.push(newTask);
|
|
95
138
|
// save updated tasks to file
|
|
96
139
|
await saveTasks(tasks);
|
|
97
|
-
console.log(
|
|
140
|
+
console.log(chalk.green('Task added successfully!'));
|
|
98
141
|
});
|
|
99
142
|
|
|
100
143
|
|
|
@@ -109,30 +152,130 @@ program
|
|
|
109
152
|
|
|
110
153
|
if (options.status) {
|
|
111
154
|
if (!ALLOWED_STATUSES.includes(options.status)){
|
|
112
|
-
console.
|
|
113
|
-
|
|
155
|
+
console.log(chalk.red(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`));
|
|
156
|
+
return;
|
|
114
157
|
}
|
|
115
158
|
tasks = tasks.filter(task => task.status === options.status);
|
|
116
159
|
}
|
|
117
|
-
|
|
118
|
-
|
|
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);
|
|
119
208
|
return;
|
|
120
209
|
}
|
|
121
|
-
// map all tasks to displayable objects
|
|
122
|
-
const tableData = tasks.map((task, index) => ({
|
|
123
|
-
'#': index + 1,
|
|
124
|
-
ID: task.id,
|
|
125
|
-
Title: task.title,
|
|
126
|
-
Status: task.status,
|
|
127
|
-
Priority: task.priority,
|
|
128
|
-
DueDate: task.dueDate,
|
|
129
|
-
Description: task.description || ''
|
|
130
|
-
}));
|
|
131
210
|
|
|
132
|
-
|
|
133
|
-
|
|
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);
|
|
134
222
|
});
|
|
135
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);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
|
|
136
279
|
// use command 'update' with task ID and action
|
|
137
280
|
program
|
|
138
281
|
.command('update')
|
|
@@ -141,7 +284,7 @@ program
|
|
|
141
284
|
.action(async () =>{
|
|
142
285
|
const tasks = await loadTasks();
|
|
143
286
|
if (tasks.length === 0) {
|
|
144
|
-
console.log('No tasks found to update.');
|
|
287
|
+
console.log(chalk.red('No tasks found to update.'));
|
|
145
288
|
return;
|
|
146
289
|
}
|
|
147
290
|
|
|
@@ -157,7 +300,7 @@ program
|
|
|
157
300
|
// find the task to update
|
|
158
301
|
const task = tasks.find(t => t.id === Number(id));
|
|
159
302
|
if (!task) {
|
|
160
|
-
console.
|
|
303
|
+
console.log(chalk.red('Task not found!'));
|
|
161
304
|
return;
|
|
162
305
|
}
|
|
163
306
|
|
|
@@ -262,28 +405,58 @@ program
|
|
|
262
405
|
].some(Boolean);
|
|
263
406
|
|
|
264
407
|
if (!hasChanges) {
|
|
265
|
-
console.log('No changes were made.');
|
|
408
|
+
console.log(chalk.yellow('No changes were made.'));
|
|
266
409
|
}else {
|
|
267
410
|
// save updated tasks to file
|
|
268
411
|
await saveTasks(tasks);
|
|
269
|
-
console.log('Task updated successfully!');
|
|
412
|
+
console.log(chalk.green('Task updated successfully!'));
|
|
270
413
|
}
|
|
271
414
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
ID: task.id,
|
|
275
|
-
Title: task.title,
|
|
276
|
-
Status: task.status,
|
|
277
|
-
Priority: task.priority,
|
|
278
|
-
DueDate: task.dueDate,
|
|
279
|
-
Description: task.description || ''
|
|
280
|
-
}));
|
|
415
|
+
displayTasks(tasks);
|
|
416
|
+
});
|
|
281
417
|
|
|
282
|
-
|
|
283
|
-
|
|
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);
|
|
284
456
|
});
|
|
285
457
|
|
|
286
458
|
|
|
459
|
+
|
|
287
460
|
// use command 'delete' with task ID and action
|
|
288
461
|
program
|
|
289
462
|
.command('delete')
|
|
@@ -292,7 +465,7 @@ program
|
|
|
292
465
|
.action(async () => {
|
|
293
466
|
const tasks = await loadTasks();
|
|
294
467
|
if (tasks.length === 0) {
|
|
295
|
-
console.log('No tasks found to delete.');
|
|
468
|
+
console.log(chalk.yellow('No tasks found to delete.'));
|
|
296
469
|
return;
|
|
297
470
|
}
|
|
298
471
|
|
|
@@ -314,27 +487,44 @@ program
|
|
|
314
487
|
}
|
|
315
488
|
]);
|
|
316
489
|
if (!confirm) {
|
|
317
|
-
console.log('Task deletion cancelled.');
|
|
490
|
+
console.log(chalk.yellow('Task deletion cancelled.'));
|
|
318
491
|
return;
|
|
319
492
|
}
|
|
320
|
-
|
|
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
|
|
321
497
|
const newTasks = tasks.filter(t => t.id !== Number(id));
|
|
498
|
+
|
|
322
499
|
await saveTasks(newTasks);
|
|
323
|
-
console.log('Task deleted successfully!');
|
|
324
|
-
|
|
325
|
-
const tableData = newTasks.map((task, index) => ({
|
|
326
|
-
'#': index + 1,
|
|
327
|
-
ID: task.id,
|
|
328
|
-
Title: task.title,
|
|
329
|
-
Status: task.status,
|
|
330
|
-
Priority: task.priority,
|
|
331
|
-
DueDate: task.dueDate,
|
|
332
|
-
Description: task.description || ''
|
|
333
|
-
}));
|
|
500
|
+
console.log(chalk.green('Task deleted successfully!'));
|
|
501
|
+
console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
|
|
334
502
|
|
|
335
|
-
|
|
336
|
-
console.table(tableData);
|
|
503
|
+
displayTasks(newTasks);
|
|
337
504
|
});
|
|
338
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
|
+
|
|
339
529
|
// parse command line arguments
|
|
340
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
|
+
};
|
package/taskService.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
// function to load tasks from file
|
|
9
|
-
export const loadTasks = async () => {
|
|
10
|
-
try{
|
|
11
|
-
const data = await fs.readFile(TASKS_FILE, 'utf8');
|
|
12
|
-
return JSON.parse(data);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
if (error.code === 'ENOENT') return [];
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
// function to save tasks to file
|
|
19
|
-
export const saveTasks = async (tasks) => {
|
|
20
|
-
await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
|
|
21
|
-
};
|
|
22
|
-
// get next task ID
|
|
23
|
-
export const getNextId = (tasks) => {
|
|
24
|
-
return tasks.length > 0 ? Math.max(...tasks.map(task => task.id)) + 1 : 1;
|
|
25
|
-
};
|
package/todos.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[]
|
|
File without changes
|