taskninja 1.1.6 → 1.1.8

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.
@@ -0,0 +1,51 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { loadTasks, saveTasks, saveDeletedTask } from '../utils/taskService.js';
4
+ import { displayTasks, cleanupAndExit } from '../helpers/helpers.js';
5
+
6
+ export const deleteAction = async () => {
7
+ try {
8
+ const tasks = await loadTasks();
9
+ if (!tasks.length) {
10
+ console.log(chalk.yellow('No tasks found to delete.'));
11
+ cleanupAndExit(0);
12
+ }
13
+
14
+ const { id } = await inquirer.prompt([
15
+ {
16
+ type: 'rawlist',
17
+ name: 'id',
18
+ message: 'Select the task to delete:',
19
+ choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
20
+ }
21
+ ]);
22
+
23
+ const { confirm } = await inquirer.prompt([
24
+ {
25
+ type: 'confirm',
26
+ name: 'confirm',
27
+ message: 'Are you sure you want to delete this task?',
28
+ default: false
29
+ }
30
+ ]);
31
+
32
+ if (!confirm) {
33
+ console.log(chalk.yellow('Task deletion cancelled.'));
34
+ cleanupAndExit(0);
35
+ }
36
+
37
+ const taskToDelete = tasks.find(t => t.id === Number(id));
38
+ await saveDeletedTask(taskToDelete);
39
+
40
+ const newTasks = tasks.filter(t => t.id !== Number(id));
41
+ await saveTasks(newTasks);
42
+
43
+ console.log(chalk.green('Task deleted successfully!'));
44
+ console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
45
+ displayTasks(newTasks);
46
+ } catch (error) {
47
+ console.log(chalk.yellow('\nOperation Cancelled!'));
48
+ } finally {
49
+ cleanupAndExit(0);
50
+ }
51
+ };
@@ -0,0 +1,36 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { loadTasks, saveTasks } from "../utils/taskService.js";
4
+ import { displayTasks, cleanupAndExit } from "../helpers/helpers.js";
5
+
6
+ export const doneAction = async () => {
7
+ try {
8
+ const tasks = await loadTasks();
9
+ const activeTasks = tasks.filter(t => t.status !== 'done');
10
+
11
+ if (!activeTasks.length) {
12
+ console.log(chalk.magenta('Congratulations! All tasks are already done.'));
13
+ return;
14
+ }
15
+
16
+ const { id } = await inquirer.prompt([
17
+ {
18
+ type: 'rawlist',
19
+ name: 'id',
20
+ message: 'Select the task to mark as done:',
21
+ choices: activeTasks.map(t => ({ name: `${t.id}: ${t.title} [Current Status: ${t.status}]`, value: t.id }))
22
+ }
23
+ ]);
24
+
25
+ const task = tasks.find(t => t.id === Number(id));
26
+ task.status = 'done';
27
+ await saveTasks(tasks);
28
+
29
+ console.log(chalk.green('Task marked as done successfully!'));
30
+ displayTasks(tasks);
31
+ } catch (error) {
32
+ console.log(chalk.yellow('\nOperation Cancelled!'));
33
+ } finally {
34
+ cleanupAndExit(0);
35
+ }
36
+ };
@@ -0,0 +1,19 @@
1
+ import chalk from "chalk";
2
+ import { loadTasks } from "../utils/taskService.js";
3
+ import { displayTasks, cleanupAndExit } from "../helpers/helpers.js";
4
+ import { ALLOWED_STATUSES } from "../utils/validators.js";
5
+
6
+ export const listAction = async (options) => {
7
+ let tasks = await loadTasks();
8
+
9
+ if (options.status) {
10
+ if (!ALLOWED_STATUSES.includes(options.status)) {
11
+ console.log(chalk.red(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`));
12
+ cleanupAndExit(0);
13
+ }
14
+ tasks = tasks.filter(task => task.status === options.status);
15
+ }
16
+
17
+ displayTasks(tasks);
18
+ cleanupAndExit(0);
19
+ };
@@ -0,0 +1,48 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { loadTasks } from "../utils/taskService.js";
4
+ import { displayTasks, cleanupAndExit } from "../helpers/helpers.js";
5
+
6
+ export const searchAction = async (keyword, options) => {
7
+ try {
8
+ const tasks = await loadTasks();
9
+ let searchTerm = keyword || options.find;
10
+
11
+ if (!searchTerm) {
12
+ const answers = await inquirer.prompt([
13
+ { type: 'input', name: 'term', message: 'Enter the keyword you want to search for:', validate: input => input ? true : 'Keyword cannot be empty!' },
14
+ { type: 'rawlist', name: 'field', message: 'Where do you want to search?', choices: ['title', 'description', 'both'], default: 'both' }
15
+ ]);
16
+ searchTerm = answers.term;
17
+ const field = answers.field;
18
+
19
+ const founded = tasks.filter(task => {
20
+ if (field === 'title') return task.title.toLowerCase().includes(searchTerm.toLowerCase());
21
+ if (field === 'description') return task.description.toLowerCase().includes(searchTerm.toLowerCase());
22
+ return task.title.toLowerCase().includes(searchTerm.toLowerCase()) || task.description.toLowerCase().includes(searchTerm.toLowerCase());
23
+ });
24
+
25
+ if (!founded.length) {
26
+ console.log(chalk.red('No task matched your search!'));
27
+ return;
28
+ }
29
+ displayTasks(founded);
30
+ return;
31
+ }
32
+
33
+ const founded = tasks.filter(task =>
34
+ task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
35
+ task.description.toLowerCase().includes(searchTerm.toLowerCase())
36
+ );
37
+
38
+ if (!founded.length) {
39
+ console.log(chalk.red('No task matched your search!'));
40
+ return;
41
+ }
42
+ displayTasks(founded);
43
+ } catch (error) {
44
+ console.log(chalk.yellow('\nOperation Cancelled!'));
45
+ } finally {
46
+ cleanupAndExit(0);
47
+ }
48
+ };
@@ -0,0 +1,47 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { loadTasks } from "../utils/taskService.js";
4
+ import { displayTasks, cleanupAndExit } from "../helpers/helpers.js";
5
+
6
+ export const sortAction = async (options) => {
7
+ try {
8
+ const tasks = await loadTasks();
9
+ let criteria = options.by;
10
+
11
+ if (!criteria) {
12
+ const answer = await inquirer.prompt([
13
+ { type: 'rawlist', name: 'criteria', message: 'Sort tasks by:', choices: ['dueDate', 'priority', 'status'] }
14
+ ]);
15
+ criteria = answer.criteria;
16
+ }
17
+
18
+ criteria = criteria.toLowerCase().replace(/[-_]/g, '');
19
+
20
+ if (!['duedate', 'priority', 'status'].includes(criteria)) {
21
+ console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
22
+ return;
23
+ }
24
+
25
+ const sortedTasks = [...tasks];
26
+
27
+ switch (criteria) {
28
+ case 'duedate':
29
+ sortedTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
30
+ break;
31
+ case 'priority':
32
+ const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
33
+ sortedTasks.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
34
+ break;
35
+ case 'status':
36
+ const statusOrder = { 'todo': 1, 'in-progress': 2, 'done': 3 };
37
+ sortedTasks.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
38
+ break;
39
+ }
40
+
41
+ displayTasks(sortedTasks);
42
+ } catch (error) {
43
+ console.log(chalk.yellow('\nOperation Cancelled!'));
44
+ } finally {
45
+ cleanupAndExit(0);
46
+ }
47
+ };
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk';
2
+ import { loadTasks, saveTasks, loadDeletedTasks, saveDeletedTasksFile, cleanupExpiredDeletedTasks } from '../utils/taskService.js';
3
+ import { cleanupAndExit } from '../helpers/helpers.js';
4
+
5
+ export const undoAction = async () => {
6
+ try {
7
+ await cleanupExpiredDeletedTasks();
8
+
9
+ const deletedTasks = await loadDeletedTasks();
10
+ if (!deletedTasks || deletedTasks.length === 0) {
11
+ console.log(chalk.yellow('No deleted task to restore.'));
12
+ return;
13
+ }
14
+
15
+ const lastDeletedTask = deletedTasks.pop();
16
+
17
+ const { deletedAt, expiredAfter, ...cleanTask } = lastDeletedTask;
18
+
19
+ const tasks = await loadTasks();
20
+ tasks.push(cleanTask);
21
+ tasks.sort((a, b) => a.id - b.id);
22
+ await saveTasks(tasks);
23
+
24
+ await saveDeletedTasksFile(deletedTasks);
25
+
26
+ console.log(chalk.green(`Last deleted task restored successfully! (Task name: ${cleanTask.title})`));
27
+ } catch (error) {
28
+ console.log(chalk.red('An error occurred during undo operation.'));
29
+ } finally {
30
+ cleanupAndExit(0);
31
+ }
32
+ };
@@ -0,0 +1,62 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { loadTasks, saveTasks } from "../utils/taskService.js";
4
+ import { displayTasks, cleanupAndExit } from "../helpers/helpers.js";
5
+ import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "../utils/validators.js";
6
+
7
+ export const updateAction = async () => {
8
+ try {
9
+ const tasks = await loadTasks();
10
+ if (!tasks.length) {
11
+ console.log(chalk.red('No tasks found to update.'));
12
+ cleanupAndExit(0);
13
+ }
14
+
15
+ const { id } = await inquirer.prompt([
16
+ {
17
+ type: 'rawlist',
18
+ name: 'id',
19
+ message: 'Select the task to update (By ID):',
20
+ choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
21
+ }
22
+ ]);
23
+
24
+ const task = tasks.find(t => t.id === Number(id));
25
+
26
+ const answers = await inquirer.prompt([
27
+ { type: 'confirm', name: 'changeTitle', message: 'Do you want to change the title?', default: false },
28
+ { type: 'input', name: 'title', message: 'Enter the new title:', when: a => a.changeTitle, validate: input => input ? true : 'Title cannot be empty!' },
29
+ { type: 'confirm', name: 'changeStatus', message: 'Do you want to change the status?', default: false },
30
+ { type: 'rawlist', name: 'status', message: 'Select the new status:', choices: ALLOWED_STATUSES, when: a => a.changeStatus },
31
+ { type: 'confirm', name: 'changePriority', message: 'Do you want to change the priority?', default: false },
32
+ { type: 'rawlist', name: 'priority', message: 'Select the new priority:', choices: ALLOWED_PRIORITIES, when: a => a.changePriority },
33
+ { type: 'confirm', name: 'changeDueDate', message: 'Do you want to change the due date?', default: false },
34
+ {
35
+ type: 'input', name: 'dueDate', message: 'Enter the new due date (YYYY-MM-DD):', when: a => a.changeDueDate,
36
+ validate: input => { try { validateDueDate(input); return true; } catch (error) { return error.message; } }
37
+ },
38
+ { type: 'confirm', name: 'changeDescription', message: 'Do you want to change the description?', default: false },
39
+ { type: 'input', name: 'description', message: 'Enter the new description:', when: a => a.changeDescription }
40
+ ]);
41
+
42
+ if (answers.changeTitle) task.title = answers.title;
43
+ if (answers.changeStatus) task.status = answers.status;
44
+ if (answers.changePriority) task.priority = answers.priority;
45
+ if (answers.changeDueDate) task.dueDate = answers.dueDate;
46
+ if (answers.changeDescription) task.description = answers.description || '';
47
+
48
+ const hasChanges = [answers.changeTitle, answers.changeStatus, answers.changePriority, answers.changeDueDate, answers.changeDescription].some(Boolean);
49
+
50
+ if (!hasChanges) {
51
+ console.log(chalk.yellow('No changes were made.'));
52
+ } else {
53
+ await saveTasks(tasks);
54
+ console.log(chalk.green('Task updated successfully!'));
55
+ }
56
+ displayTasks(tasks);
57
+ } catch (error) {
58
+ console.log(chalk.yellow('\nOperation Cancelled!'));
59
+ } finally {
60
+ cleanupAndExit(0);
61
+ }
62
+ };
@@ -3,8 +3,6 @@
3
3
  import Table from 'cli-table3';
4
4
  // for colored text
5
5
  import chalk from 'chalk';
6
- // for reading keyboard buttons
7
- import readline from 'readline';
8
6
 
9
7
  // helper function to display tasks in colored table
10
8
  const displayTasks = (tasks) => {
@@ -52,19 +50,4 @@ const cleanupAndExit = (code = 0) => {
52
50
  process.exit(code);
53
51
  };
54
52
 
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};
53
+ export {displayTasks, cleanupAndExit};
@@ -24,7 +24,17 @@ export const loadTasks = async () => {
24
24
  };
25
25
  // function to save tasks to file
26
26
  export const saveTasks = async (tasks) => {
27
- await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
27
+
28
+ if (tasks.length === 0) {
29
+ try {
30
+ await fs.unlink(TASKS_FILE);
31
+ } catch (error) {
32
+ if (error.code !== 'ENOENT') throw error;
33
+ }
34
+ }
35
+ else {
36
+ await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
37
+ }
28
38
  };
29
39
  // get next task ID
30
40
  export const getNextId = (tasks) => {
@@ -37,18 +47,31 @@ export const getNextId = (tasks) => {
37
47
  */
38
48
 
39
49
  // to load deleted last-deleted task
40
- export const loadDeletedTask = async () => {
50
+ export const loadDeletedTasks = async () => {
41
51
  try {
42
52
  const data = await fs.readFile(DELETED_TASKS_FILE, 'utf8');
43
53
  return JSON.parse(data);
44
54
  } catch (error) {
45
- if (error.code === 'ENOENT') return null;
55
+ if (error.code === 'ENOENT') return [];
46
56
  throw error;
47
57
  }
48
58
  };
49
59
  // to save deleted last-deleted task
60
+
61
+ export const saveDeletedTasksFile = async(deletedTasks) => {
62
+ if (deletedTasks.length === 0) {
63
+ try {
64
+ await fs.unlink(DELETED_TASKS_FILE);
65
+ } catch (error) {
66
+ if (error.code !== 'ENOENT') throw error;
67
+ }
68
+ }
69
+ else {
70
+ await fs.writeFile(DELETED_TASKS_FILE, JSON.stringify(deletedTasks, null, 2));
71
+ }
72
+ };
50
73
  export const saveDeletedTask = async (task, expiredAfter = 60000) => {
51
- const deletedTask = await loadDeletedTask(task) || [];
74
+ const deletedTask = await loadDeletedTasks();
52
75
  const now = Date.now();
53
76
 
54
77
  deletedTask.push({
@@ -57,17 +80,19 @@ export const saveDeletedTask = async (task, expiredAfter = 60000) => {
57
80
  expiredAfter
58
81
  });
59
82
 
60
- await fs.writeFile(DELETED_TASKS_FILE, JSON.stringify(deletedTask, null, 2));
83
+ await saveDeletedTasksFile(deletedTask);
61
84
  }
62
85
 
63
86
  // function to delete all deleted-tasks after a while
64
87
  export const cleanupExpiredDeletedTasks = async () => {
65
- const deletedTasks = await loadDeletedTask() || [];
88
+ const deletedTasks = await loadDeletedTasks();
89
+ if (deletedTasks.length === 0) return;
90
+
66
91
  const now = Date.now();
67
92
 
68
93
  const remaining = deletedTasks.filter(task => now - task.deletedAt < task.expiredAfter);
69
94
  if (remaining.length !== deletedTasks.length){
70
- await fs.writeFile(DELETED_TASKS_FILE, JSON.stringify(remaining, null, 2));
95
+ await saveDeletedTasksFile(remaining);
71
96
  }
72
97
  }
73
98