taskninja 1.1.2 → 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,7 +346,7 @@ tn so
345
346
 
346
347
  ---
347
348
 
348
- ## Screenshots / Demo
349
+ ## Demo
349
350
 
350
351
  **All Features:**
351
352
 
@@ -387,4 +388,20 @@ tn so
387
388
  ![todos.json](images/todo.json.png "shape of tasks")
388
389
 
389
390
 
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
+
407
+
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.2
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, 15, 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',
@@ -199,13 +165,13 @@ program
199
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',
@@ -249,9 +217,9 @@ program
249
217
  // normailze input `case-insensitive + separators`
250
218
  criteria = criteria.toLowerCase().replace(/[-_]/g, '');
251
219
 
252
- if (!['dueDate', 'priority', 'status'].includes(criteria)) {
220
+ if (!['duedate', 'priority', 'status'].includes(criteria)) {
253
221
  console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
254
- return;
222
+ cleanupAndExit(0);
255
223
  }
256
224
  // sorting logic
257
225
  const sortedTasks = [...tasks];
@@ -270,10 +238,11 @@ program
270
238
  break;
271
239
  default:
272
240
  console.log(chalk.red('Invalid sort criteria. Use dueDate, priority, or status.'));
273
- return;
241
+ cleanupAndExit(0);
274
242
  }
275
243
 
276
244
  displayTasks(sortedTasks);
245
+ cleanupAndExit(0);
277
246
  });
278
247
 
279
248
 
@@ -284,11 +253,11 @@ program
284
253
  .description('Update a task by ==> ID <==')
285
254
  .action(async () =>{
286
255
  const tasks = await loadTasks();
287
- if (tasks.length === 0) {
256
+ if (!tasks.length) {
288
257
  console.log(chalk.red('No tasks found to update.'));
289
- return;
258
+ cleanupAndExit(0);
290
259
  }
291
-
260
+ enableEscExit();
292
261
  const { id } = await inquirer.prompt([
293
262
  {
294
263
  type: 'rawlist',
@@ -302,9 +271,10 @@ program
302
271
  const task = tasks.find(t => t.id === Number(id));
303
272
  if (!task) {
304
273
  console.log(chalk.red('Task not found!'));
305
- return;
274
+ cleanupAndExit(0);
306
275
  }
307
276
 
277
+ enableEscExit();
308
278
  const answers = await inquirer.prompt([
309
279
  // for title
310
280
  {
@@ -415,6 +385,7 @@ program
415
385
  }
416
386
 
417
387
  displayTasks(tasks);
388
+ cleanupAndExit(0);
418
389
  });
419
390
 
420
391
 
@@ -424,17 +395,17 @@ program
424
395
  .description('Mark a task as done by ==> ID <==')
425
396
  .action(async () => {
426
397
  const tasks = await loadTasks();
427
- if (tasks.length === 0) {
398
+ if (!tasks.length) {
428
399
  console.log(chalk.magenta('Congratulations! All tasks are already done.'));
429
- return;
400
+ cleanupAndExit(0);
430
401
  }
431
402
 
432
403
  const activeTasks = tasks.filter(t => t.status !== 'done');
433
- if (activeTasks.length === 0) {
404
+ if (!activeTasks.length) {
434
405
  console.log(chalk.magenta('Congratulations! All tasks are already done.'));
435
- return;
406
+ cleanupAndExit(0);
436
407
  }
437
-
408
+ enableEscExit();
438
409
  const { id } = await inquirer.prompt([
439
410
  {
440
411
  type: 'rawlist',
@@ -455,6 +426,7 @@ program
455
426
  console.log(chalk.green('Task marked as done successfully!'));
456
427
 
457
428
  displayTasks(tasks);
429
+ cleanupAndExit(0);
458
430
  });
459
431
 
460
432
 
@@ -466,11 +438,11 @@ program
466
438
  .description('delete a task by ==> ID <==')
467
439
  .action(async () => {
468
440
  const tasks = await loadTasks();
469
- if (tasks.length === 0) {
441
+ if (!tasks.length) {
470
442
  console.log(chalk.yellow('No tasks found to delete.'));
471
- return;
443
+ cleanupAndExit(0);
472
444
  }
473
-
445
+ enableEscExit();
474
446
  const { id } = await inquirer.prompt([
475
447
  {
476
448
  type: 'rawlist',
@@ -479,7 +451,7 @@ program
479
451
  choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
480
452
  }
481
453
  ]);
482
-
454
+ enableEscExit();
483
455
  const { confirm } = await inquirer.prompt([
484
456
  {
485
457
  type: 'confirm',
@@ -490,7 +462,7 @@ program
490
462
  ]);
491
463
  if (!confirm) {
492
464
  console.log(chalk.yellow('Task deletion cancelled.'));
493
- return;
465
+ cleanupAndExit(0);
494
466
  }
495
467
  const taskToDelete = tasks.find(t => t.id === Number(id));
496
468
  // save deleted task for undo functionality
@@ -503,6 +475,7 @@ program
503
475
  console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
504
476
 
505
477
  displayTasks(newTasks);
478
+ cleanupAndExit(0);
506
479
  });
507
480
 
508
481
  // use command 'undo' to restore last deleted task
@@ -511,17 +484,21 @@ program
511
484
  .alias('un')
512
485
  .description('Undo the last deleted task')
513
486
  .action( async () => {
514
- const lastDeletedTask = await loadDeletedTask();
515
- if (!lastDeletedTask) {
487
+ await cleanupExpiredDeletedTasks();
488
+
489
+ const deletedTasks = await loadDeletedTask();
490
+ if (!deletedTasks || !deletedTasks.length) {
516
491
  console.log(chalk.yellow('No deleted task to restore.'));
517
- return;
492
+ cleanupAndExit(0);
518
493
  }
519
494
 
495
+ const lastDeletedTask = deletedTasks.pop();
496
+
520
497
  const tasks = await loadTasks();
521
498
  tasks.push(lastDeletedTask);
522
499
  tasks.sort((a, b) => a.id - b.id); // keep tasks sorted by ID
523
500
  await saveTasks(tasks);
524
- await clearDeletedTask();
501
+ // await clearDeletedTask();
525
502
 
526
503
  console.log(chalk.green(`Last deleted task restored successfully!, (Task name: ${lastDeletedTask.title})`));
527
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskninja",
3
- "version": "1.1.2",
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 () => {