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 +19 -2
- package/app.js +48 -71
- package/helpers/helpers.js +70 -0
- package/package.json +1 -1
- package/utils/taskService.js +24 -4
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
|
-
##
|
|
349
|
+
## Demo
|
|
349
350
|
|
|
350
351
|
**All Features:**
|
|
351
352
|
|
|
@@ -387,4 +388,20 @@ tn so
|
|
|
387
388
|

|
|
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
|
|
13
|
-
* Version: 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
|
|
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.
|
|
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
|
-
|
|
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
|
|
168
|
+
if (!founded.length) {
|
|
203
169
|
console.log(chalk.red('No task matched your search!'));
|
|
204
|
-
|
|
170
|
+
cleanupAndExit(0);
|
|
205
171
|
}
|
|
206
172
|
|
|
207
173
|
displayTasks(founded);
|
|
208
|
-
|
|
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
|
|
182
|
+
if (!founded.length) {
|
|
217
183
|
console.log(chalk.red('No task matched your search!'));
|
|
218
|
-
|
|
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 (!['
|
|
220
|
+
if (!['duedate', 'priority', 'status'].includes(criteria)) {
|
|
253
221
|
console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|
256
|
+
if (!tasks.length) {
|
|
288
257
|
console.log(chalk.red('No tasks found to update.'));
|
|
289
|
-
|
|
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
|
-
|
|
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
|
|
398
|
+
if (!tasks.length) {
|
|
428
399
|
console.log(chalk.magenta('Congratulations! All tasks are already done.'));
|
|
429
|
-
|
|
400
|
+
cleanupAndExit(0);
|
|
430
401
|
}
|
|
431
402
|
|
|
432
403
|
const activeTasks = tasks.filter(t => t.status !== 'done');
|
|
433
|
-
if (activeTasks.length
|
|
404
|
+
if (!activeTasks.length) {
|
|
434
405
|
console.log(chalk.magenta('Congratulations! All tasks are already done.'));
|
|
435
|
-
|
|
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
|
|
441
|
+
if (!tasks.length) {
|
|
470
442
|
console.log(chalk.yellow('No tasks found to delete.'));
|
|
471
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
package/utils/taskService.js
CHANGED
|
@@ -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 () => {
|