taskninja 1.1.1 → 1.1.3

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 CHANGED
@@ -20,6 +20,7 @@
20
20
  * [Sort Tasks](#sort-tasks)
21
21
  * [Task Fields & Allowed Values](#task-fields--allowed-values)
22
22
  * [Examples](#examples)
23
+ * [Demo](#demo)
23
24
 
24
25
  ---
25
26
 
@@ -345,38 +346,62 @@ tn so
345
346
 
346
347
  ---
347
348
 
348
- ## Screenshots / Demo
349
+ ## Demo
350
+
351
+ **All Features:**
352
+
353
+ ![All Features](images/allFeatures.png "all commands")
349
354
 
350
355
  **Adding a Task:**
351
356
 
352
- ![Add Task](images/add.png "Adding a Task in TaskNinja CLI")
357
+ ![Add Task](images/addNew.png "Adding a Task in TaskNinja CLI")
353
358
 
354
359
  **Listing Tasks:**
355
360
 
356
- ![List Tasks](images/list.png "Listing Tasks in TaskNinja CLI")
361
+ ![List Tasks](images/listAll.png "Listing Tasks in TaskNinja CLI")
357
362
 
358
363
  **Updating a Task:**
359
364
 
360
- ![Update Task](images/update.png "Updating a Task")
365
+ ![Update Task](images/updateWith.png "Updating a Task")
361
366
 
362
367
  **Deleting a Task:**
363
368
 
364
- ![Delete Task](images/delete.png "Deleting a Task")
365
- ![Delete Task](images/delete2.png "Deleting a Task")
369
+ ![Delete Task](images/deleteMethod.png "Deleting a Task")
366
370
 
367
371
  **Restore Last-Deleted Task:**
368
372
 
369
- ![Undo](images/undo.png "Restore Last-deleted task")
373
+ ![Undo](images/undoMethod.png "Restore Last-deleted task")
370
374
 
371
375
  **Search / Sort Tasks:**
372
376
 
373
- ![Search Tasks](images/search.png "Searching Tasks")
374
- ![Sort Tasks](images/sort.png "Sorting Tasks")
375
- ![Sort Tasks-2](images/sort2.png "Sorting Tasks")
377
+ ![Search Tasks](images/searchWith.png "Searching Tasks")
378
+ ![Sort Tasks](images/sort_.png "Sorting Tasks")
379
+ ![Sort Tasks-2](images/sortBy.png "Sorting Tasks")
376
380
 
377
381
  **Mark as Done:**
378
382
 
379
- ![Mark Task As Done](images/done.png "Mark as Done In one step")
380
- ![Mark Task As Done](images/done2.png "Mark as Done In one step")
383
+ ![Mark Task As Done](images/doneTask.png "Mark as Done In one step")
384
+ ![Mark Task As Done](images/doneTask2.png "Mark as Done In one step")
385
+
386
+ **Tasks in `todos.json`:**
387
+
388
+ ![todos.json](images/todo.json.png "shape of tasks")
389
+
390
+
391
+ ---
392
+
393
+ ## New Additions for Version `1.1.3`
394
+
395
+ **ESC Button End Operation:**
396
+
397
+ * If the user presses the `Esc` button, this will end the ongoing process
398
+ * This may benefit the user by terminating the process without having to complete the process until the end
399
+
400
+ ### SOON
401
+ **Auto Deleted Task:**
402
+
403
+ * When you delete the task, it will remain saved in `deleted_todos.json` for one minute, after which it will be deleted automatically
404
+ * This somewhat conserves the user's storage space while calculating that the user may undo deleting a task
405
+
406
+
381
407
 
382
- ---
package/app.js CHANGED
@@ -9,71 +9,34 @@
9
9
  * - inquirer: For interactive prompts
10
10
  * - fs: For file system operations
11
11
  * Author: Mohamed Bakr
12
- * Date: January 2024
13
- * Version: 1.1.1
12
+ * Date: January 2026
13
+ * Version: 1.1.3
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
- // supporting colors in table forms
21
- import Table from 'cli-table3';
22
20
  // for colored text
23
21
  import chalk from 'chalk';
24
22
 
23
+ // helpers
24
+ import {displayTasks, enableEscExit, cleanupAndExit} from './helpers/helpers.js';
25
+
25
26
  // assigning Commander to a variable
26
27
  const program = new Command();
27
28
 
28
29
  // importing validators and allowed values
29
30
  import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "./utils/validators.js";
30
31
  // 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;
39
- }
32
+ import { loadTasks, saveTasks, getNextId, saveDeletedTask, loadDeletedTask, clearDeletedTask, cleanupExpiredDeletedTasks } from "./utils/taskService.js";
40
33
 
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
34
 
70
- console.log(table.toString());
71
- };
72
35
  // setting up
73
36
  program
74
37
  .name("taskninja")
75
38
  .description("A simple CLI application to manage your tasks")
76
- .version("1.1.0");
39
+ .version("1.1.3");
77
40
 
78
41
  // use command 'add' with title + status + priority + dueDate + description and action
79
42
  program
@@ -81,6 +44,7 @@ program
81
44
  .alias('a')
82
45
  .description('Add a new task')
83
46
  .action(async () => {
47
+ enableEscExit();
84
48
  const answers = await inquirer.prompt([
85
49
  {
86
50
  type: 'input',
@@ -153,12 +117,13 @@ program
153
117
  if (options.status) {
154
118
  if (!ALLOWED_STATUSES.includes(options.status)){
155
119
  console.log(chalk.red(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`));
156
- return;
120
+ cleanupAndExit(0);
157
121
  }
158
122
  tasks = tasks.filter(task => task.status === options.status);
159
123
  }
160
124
  // display all tasks in table format
161
125
  displayTasks(tasks);
126
+ cleanupAndExit(0);
162
127
  });
163
128
 
164
129
 
@@ -174,6 +139,7 @@ program
174
139
  let searchTerm = keyword || options.find;
175
140
 
176
141
  if (!searchTerm) {
142
+ enableEscExit();
177
143
  const answers = await inquirer.prompt([
178
144
  {
179
145
  type: 'input',
@@ -196,16 +162,16 @@ program
196
162
  if (field === 'title') return task.title.toLowerCase().includes(searchTerm.toLowerCase());
197
163
  if (field === 'description') return task.description.toLowerCase().includes(searchTerm.toLowerCase());
198
164
  return task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
199
- task.description.toLowerCase().includes(searchTerm.toLowerCase());
165
+ task.description.toLowerCase().includes(searchTerm.toLowerCase());
200
166
  });
201
167
 
202
- if (founded.length === 0) {
168
+ if (!founded.length) {
203
169
  console.log(chalk.red('No task matched your search!'));
204
- return;
170
+ cleanupAndExit(0);
205
171
  }
206
172
 
207
173
  displayTasks(founded);
208
- return;
174
+ cleanupAndExit(0);
209
175
  }
210
176
 
211
177
  const founded = tasks.filter(task =>
@@ -213,12 +179,13 @@ program
213
179
  task.description.toLowerCase().includes(searchTerm.toLowerCase())
214
180
  );
215
181
 
216
- if (founded.length === 0) {
182
+ if (!founded.length) {
217
183
  console.log(chalk.red('No task matched your search!'));
218
- return;
184
+ cleanupAndExit(0);
219
185
  }
220
186
 
221
187
  displayTasks(founded);
188
+ cleanupAndExit(0);
222
189
  });
223
190
 
224
191
 
@@ -236,6 +203,7 @@ program
236
203
  let criteria = options.by;
237
204
 
238
205
  if (!criteria) {
206
+ enableEscExit();
239
207
  const answer = await inquirer.prompt([
240
208
  {
241
209
  type: 'rawlist',
@@ -246,33 +214,35 @@ program
246
214
  ]);
247
215
  criteria = answer.criteria;
248
216
  }
249
-
217
+ // normailze input `case-insensitive + separators`
218
+ criteria = criteria.toLowerCase().replace(/[-_]/g, '');
250
219
 
251
- if (!['dueDate', 'priority', 'status'].includes(criteria)) {
220
+ if (!['duedate', 'priority', 'status'].includes(criteria)) {
252
221
  console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
253
- return;
222
+ cleanupAndExit(0);
254
223
  }
255
224
  // sorting logic
256
225
  const sortedTasks = [...tasks];
257
226
 
258
227
  switch (criteria) {
259
- case 'dueDate' || 'duedate' || 'DueDate' || 'Duedate' || 'due date' || 'Due date' || 'due-date':
228
+ case 'duedate':
260
229
  sortedTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
261
230
  break;
262
- case 'priority' || 'Priority':
231
+ case 'priority':
263
232
  const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
264
233
  sortedTasks.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
265
234
  break;
266
- case 'status' || 'Status':
235
+ case 'status':
267
236
  const statusOrder = { 'todo': 1, 'in-progress': 2, 'done': 3 };
268
- sortedTasks.sort((a, b) => statusOrder[a.priority] - statusOrder[b.priority]);
237
+ sortedTasks.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
269
238
  break;
270
239
  default:
271
240
  console.log(chalk.red('Invalid sort criteria. Use dueDate, priority, or status.'));
272
- return;
241
+ cleanupAndExit(0);
273
242
  }
274
243
 
275
244
  displayTasks(sortedTasks);
245
+ cleanupAndExit(0);
276
246
  });
277
247
 
278
248
 
@@ -283,11 +253,11 @@ program
283
253
  .description('Update a task by ==> ID <==')
284
254
  .action(async () =>{
285
255
  const tasks = await loadTasks();
286
- if (tasks.length === 0) {
256
+ if (!tasks.length) {
287
257
  console.log(chalk.red('No tasks found to update.'));
288
- return;
258
+ cleanupAndExit(0);
289
259
  }
290
-
260
+ enableEscExit();
291
261
  const { id } = await inquirer.prompt([
292
262
  {
293
263
  type: 'rawlist',
@@ -301,9 +271,10 @@ program
301
271
  const task = tasks.find(t => t.id === Number(id));
302
272
  if (!task) {
303
273
  console.log(chalk.red('Task not found!'));
304
- return;
274
+ cleanupAndExit(0);
305
275
  }
306
276
 
277
+ enableEscExit();
307
278
  const answers = await inquirer.prompt([
308
279
  // for title
309
280
  {
@@ -380,10 +351,11 @@ program
380
351
  default: false
381
352
  },
382
353
  {
383
- type: "rawlist",
354
+ type: "input",
384
355
  name: 'description',
385
356
  message: 'Enter the new description:',
386
- when: answers => answers.changeDescription
357
+ when: answers => answers.changeDescription,
358
+ validate: input => input.trim() ? true : 'Description cannot be empty!'
387
359
  }
388
360
  ]);
389
361
 
@@ -413,6 +385,7 @@ program
413
385
  }
414
386
 
415
387
  displayTasks(tasks);
388
+ cleanupAndExit(0);
416
389
  });
417
390
 
418
391
 
@@ -422,17 +395,17 @@ program
422
395
  .description('Mark a task as done by ==> ID <==')
423
396
  .action(async () => {
424
397
  const tasks = await loadTasks();
425
- if (tasks.length === 0) {
398
+ if (!tasks.length) {
426
399
  console.log(chalk.magenta('Congratulations! All tasks are already done.'));
427
- return;
400
+ cleanupAndExit(0);
428
401
  }
429
402
 
430
403
  const activeTasks = tasks.filter(t => t.status !== 'done');
431
- if (activeTasks.length === 0) {
404
+ if (!activeTasks.length) {
432
405
  console.log(chalk.magenta('Congratulations! All tasks are already done.'));
433
- return;
406
+ cleanupAndExit(0);
434
407
  }
435
-
408
+ enableEscExit();
436
409
  const { id } = await inquirer.prompt([
437
410
  {
438
411
  type: 'rawlist',
@@ -453,6 +426,7 @@ program
453
426
  console.log(chalk.green('Task marked as done successfully!'));
454
427
 
455
428
  displayTasks(tasks);
429
+ cleanupAndExit(0);
456
430
  });
457
431
 
458
432
 
@@ -464,11 +438,11 @@ program
464
438
  .description('delete a task by ==> ID <==')
465
439
  .action(async () => {
466
440
  const tasks = await loadTasks();
467
- if (tasks.length === 0) {
441
+ if (!tasks.length) {
468
442
  console.log(chalk.yellow('No tasks found to delete.'));
469
- return;
443
+ cleanupAndExit(0);
470
444
  }
471
-
445
+ enableEscExit();
472
446
  const { id } = await inquirer.prompt([
473
447
  {
474
448
  type: 'rawlist',
@@ -477,7 +451,7 @@ program
477
451
  choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
478
452
  }
479
453
  ]);
480
-
454
+ enableEscExit();
481
455
  const { confirm } = await inquirer.prompt([
482
456
  {
483
457
  type: 'confirm',
@@ -488,7 +462,7 @@ program
488
462
  ]);
489
463
  if (!confirm) {
490
464
  console.log(chalk.yellow('Task deletion cancelled.'));
491
- return;
465
+ cleanupAndExit(0);
492
466
  }
493
467
  const taskToDelete = tasks.find(t => t.id === Number(id));
494
468
  // save deleted task for undo functionality
@@ -501,6 +475,7 @@ program
501
475
  console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
502
476
 
503
477
  displayTasks(newTasks);
478
+ cleanupAndExit(0);
504
479
  });
505
480
 
506
481
  // use command 'undo' to restore last deleted task
@@ -509,17 +484,21 @@ program
509
484
  .alias('un')
510
485
  .description('Undo the last deleted task')
511
486
  .action( async () => {
512
- const lastDeletedTask = await loadDeletedTask();
513
- if (!lastDeletedTask) {
487
+ await cleanupExpiredDeletedTasks();
488
+
489
+ const deletedTasks = await loadDeletedTask();
490
+ if (!deletedTasks || !deletedTasks.length) {
514
491
  console.log(chalk.yellow('No deleted task to restore.'));
515
- return;
492
+ cleanupAndExit(0);
516
493
  }
517
494
 
495
+ const lastDeletedTask = deletedTasks.pop();
496
+
518
497
  const tasks = await loadTasks();
519
498
  tasks.push(lastDeletedTask);
520
499
  tasks.sort((a, b) => a.id - b.id); // keep tasks sorted by ID
521
500
  await saveTasks(tasks);
522
- await clearDeletedTask();
501
+ // await clearDeletedTask();
523
502
 
524
503
  console.log(chalk.green(`Last deleted task restored successfully!, (Task name: ${lastDeletedTask.title})`));
525
504
  });
@@ -0,0 +1,70 @@
1
+
2
+ // supporting colors in table forms
3
+ import Table from 'cli-table3';
4
+ // for colored text
5
+ import chalk from 'chalk';
6
+ // for reading keyboard buttons
7
+ import readline from 'readline';
8
+
9
+ // helper function to display tasks in colored table
10
+ const displayTasks = (tasks) => {
11
+ if (!tasks.length) {
12
+ console.log(chalk.yellow("No tasks to display."));
13
+ cleanupAndExit(0);
14
+ }
15
+
16
+ const table = new Table({
17
+ head: [
18
+ chalk.cyanBright('#'),
19
+ chalk.cyanBright('ID'),
20
+ chalk.cyanBright('Title'),
21
+ chalk.cyanBright('Status'),
22
+ chalk.cyanBright('Priority'),
23
+ chalk.cyanBright('DueDate'),
24
+ chalk.cyanBright('Description')
25
+ ],
26
+ colWidths: [4, 4, 20, 15, 10, 12, 45]
27
+ });
28
+
29
+ tasks.forEach((task, index) => {
30
+ table.push([
31
+ index + 1,
32
+ task.id,
33
+ task.title,
34
+ task.status === "done" ? chalk.green(task.status)
35
+ : task.status === "in-progress" ? chalk.yellow(task.status)
36
+ : chalk.blue(task.status),
37
+ task.priority === "high" ? chalk.red(task.priority)
38
+ : task.priority === "medium" ? chalk.yellow(task.priority)
39
+ : chalk.green(task.priority),
40
+ task.dueDate,
41
+ task.description || ''
42
+ ]);
43
+ });
44
+
45
+ console.log(table.toString());
46
+ };
47
+
48
+ const cleanupAndExit = (code = 0) => {
49
+ if (process.stdin.isTTY) {
50
+ process.stdin.setRawMode(false);
51
+ }
52
+ process.exit(code);
53
+ };
54
+
55
+ const enableEscExit = () => {
56
+ readline.emitKeypressEvents(process.stdin);
57
+
58
+ if (process.stdin.isTTY) {
59
+ process.stdin.setRawMode(true);
60
+ }
61
+
62
+ process.stdin.on('keypress', (_, key) => {
63
+ if (key?.name === 'escape'){
64
+ console.log(chalk.yellow('\nOperation Cancelled!'));
65
+ cleanupAndExit(0);
66
+ }
67
+ });
68
+ };
69
+
70
+ export {displayTasks, enableEscExit, cleanupAndExit};
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskninja",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "a simple CLI application with JS as a (To-Do Application)",
5
5
  "main": "app.js",
6
6
  "type": "module",
@@ -35,10 +35,6 @@ export const getNextId = (tasks) => {
35
35
  * @description this section responsible to store deleted tasks in deleted_todos.json
36
36
  * for undo functionality
37
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
38
 
43
39
  // to load deleted last-deleted task
44
40
  export const loadDeletedTask = async () => {
@@ -50,6 +46,30 @@ export const loadDeletedTask = async () => {
50
46
  throw error;
51
47
  }
52
48
  };
49
+ // to save deleted last-deleted task
50
+ export const saveDeletedTask = async (task, expiredAfter = 60000) => {
51
+ const deletedTask = await loadDeletedTask(task) || [];
52
+ const now = Date.now();
53
+
54
+ deletedTask.push({
55
+ ...task,
56
+ deletedAt: now,
57
+ expiredAfter
58
+ });
59
+
60
+ await fs.writeFile(DELETED_TASKS_FILE, JSON.stringify(deletedTask, null, 2));
61
+ }
62
+
63
+ // function to delete all deleted-tasks after a while
64
+ export const cleanupExpiredDeletedTasks = async () => {
65
+ const deletedTasks = await loadDeletedTask() || [];
66
+ const now = Date.now();
67
+
68
+ const remaining = deletedTasks.filter(task => now - task.deletedAt < task.expiredAfter);
69
+ if (remaining.length !== deletedTasks.length){
70
+ await fs.writeFile(DELETED_TASKS_FILE, JSON.stringify(remaining, null, 2));
71
+ }
72
+ }
53
73
 
54
74
  // to clear deleted tasks file after undo
55
75
  export const clearDeletedTask = async () => {
package/images/add.png DELETED
Binary file
package/images/delete.png DELETED
Binary file
Binary file
package/images/done.png DELETED
Binary file
package/images/done2.png DELETED
Binary file
package/images/list.png DELETED
Binary file
package/images/search.png DELETED
Binary file
package/images/sort.png DELETED
Binary file
package/images/sort2.png DELETED
Binary file
package/images/undo.png DELETED
Binary file
package/images/update.png DELETED
Binary file