taskninja 1.0.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/app.js +273 -0
- package/package.json +21 -0
package/app.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Task Manager CLI Application
|
|
5
|
+
* This application allows users to manage their tasks via command line interface.
|
|
6
|
+
* It supports adding, listing, and removing tasks.
|
|
7
|
+
* Dependencies:
|
|
8
|
+
* - commander: For command line argument parsing
|
|
9
|
+
* - inquirer: For interactive prompts
|
|
10
|
+
* - fs: For file system operations
|
|
11
|
+
* Author: Mohamed Bakr
|
|
12
|
+
* Date: June 2024
|
|
13
|
+
* Version: 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// for using commands in terminal
|
|
17
|
+
import { Command } from "commander";
|
|
18
|
+
// for interactive command line prompts
|
|
19
|
+
import inquirer from "inquirer";
|
|
20
|
+
// for file system operations
|
|
21
|
+
import fs from "fs/promises";
|
|
22
|
+
|
|
23
|
+
// assigning Commander to a variable
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
// file to store tasks
|
|
27
|
+
const TASKS_FILE = "./todos.json";
|
|
28
|
+
// allowed task statuses
|
|
29
|
+
const ALLOWED_STATUSES = ["todo", "in-progress", "done"];
|
|
30
|
+
// allowed task priorities
|
|
31
|
+
const ALLOWED_PRIORITIES = ["low", "medium", "high"];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// function to load tasks from file
|
|
35
|
+
const loadTasks = async () => {
|
|
36
|
+
try{
|
|
37
|
+
const data = await fs.readFile(TASKS_FILE, 'utf8');
|
|
38
|
+
return JSON.parse(data);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') return [];
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
// function to save tasks to file
|
|
45
|
+
const saveTasks = async (tasks) => {
|
|
46
|
+
await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
|
|
47
|
+
};
|
|
48
|
+
// get next task ID
|
|
49
|
+
const getNextId = (tasks) => {
|
|
50
|
+
return tasks.length > 0 ? Math.max(...tasks.map(task => task.id)) + 1 : 1;
|
|
51
|
+
};
|
|
52
|
+
// validate task status
|
|
53
|
+
const validateStatus = (status) => {
|
|
54
|
+
if (!ALLOWED_STATUSES.includes(status)) {
|
|
55
|
+
throw new Error(`Invalid status. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// validate task priority
|
|
59
|
+
const validatePriority = (priority) => {
|
|
60
|
+
if (!ALLOWED_PRIORITIES.includes(priority)) {
|
|
61
|
+
throw new Error(`Invalid priority. Allowed priorities are: ${ALLOWED_PRIORITIES.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
// verify due date format
|
|
65
|
+
const validateDueDate = (dueDate) => {
|
|
66
|
+
if (isNaN(Date.parse(dueDate))) {
|
|
67
|
+
throw new Error("Invalid due date. Please use a valid date format (YYYY-MM-DD).");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// setting up
|
|
73
|
+
program
|
|
74
|
+
.name("todo-cli")
|
|
75
|
+
.description("A simple CLI application to manage your tasks")
|
|
76
|
+
.version("1.0.0");
|
|
77
|
+
|
|
78
|
+
// use command 'add' with title + status + priority + dueDate + description and action
|
|
79
|
+
program
|
|
80
|
+
.command('add')
|
|
81
|
+
.alias('a')
|
|
82
|
+
.description('Add a new task')
|
|
83
|
+
.action(async (title, status, priority, dueDate, description = "") => {
|
|
84
|
+
const answers = await inquirer.prompt([
|
|
85
|
+
{
|
|
86
|
+
type: 'input',
|
|
87
|
+
name: 'title',
|
|
88
|
+
message : 'Task Title:',
|
|
89
|
+
validate : input => input ? true : 'Title cannot be empty!'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'list',
|
|
93
|
+
name: 'status',
|
|
94
|
+
message : 'Task Status:',
|
|
95
|
+
choices : ALLOWED_STATUSES,
|
|
96
|
+
default : 'todo'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'list',
|
|
100
|
+
name: 'priority',
|
|
101
|
+
message : 'Task Priority:',
|
|
102
|
+
choices : ALLOWED_PRIORITIES,
|
|
103
|
+
default : 'medium'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'dueDate',
|
|
108
|
+
message : 'Due Date (YYYY-MM-DD):',
|
|
109
|
+
validate : input => {
|
|
110
|
+
try {
|
|
111
|
+
validateDueDate(input);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return error.message;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'description',
|
|
121
|
+
message : 'Task Description (optional):'
|
|
122
|
+
}
|
|
123
|
+
]);
|
|
124
|
+
// load existing tasks
|
|
125
|
+
const tasks = await loadTasks();
|
|
126
|
+
|
|
127
|
+
// create new task object
|
|
128
|
+
const newTask = {
|
|
129
|
+
id: getNextId(tasks),
|
|
130
|
+
title: answers.title,
|
|
131
|
+
status: answers.status,
|
|
132
|
+
priority: answers.priority,
|
|
133
|
+
dueDate: answers.dueDate,
|
|
134
|
+
description: answers.description || ''
|
|
135
|
+
};
|
|
136
|
+
// add new task to tasks array
|
|
137
|
+
tasks.push(newTask);
|
|
138
|
+
// save updated tasks to file
|
|
139
|
+
await saveTasks(tasks);
|
|
140
|
+
console.log("Task added successfully!");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
// use command 'list' with optional status filter and action
|
|
145
|
+
program
|
|
146
|
+
.command('list')
|
|
147
|
+
.alias('ls')
|
|
148
|
+
.description('List all tasks')
|
|
149
|
+
.option('-s, --status <status>', 'Filter tasks by status')
|
|
150
|
+
.action(async (options) => {
|
|
151
|
+
let tasks = await loadTasks();
|
|
152
|
+
|
|
153
|
+
if (options.status) {
|
|
154
|
+
if (!ALLOWED_STATUSES.includes(options.status)){
|
|
155
|
+
console.error(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
tasks = tasks.filter(task => task.status === options.status);
|
|
159
|
+
}
|
|
160
|
+
if (tasks.length === 0) {
|
|
161
|
+
console.log('No tasks found.');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// display tasks in a table format
|
|
165
|
+
console.table([{
|
|
166
|
+
ID: task.id,
|
|
167
|
+
Title: task.title,
|
|
168
|
+
Status: task.status,
|
|
169
|
+
Priority: task.priority,
|
|
170
|
+
DueDate: task.dueDate,
|
|
171
|
+
Description: task.description || ''
|
|
172
|
+
}]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// use command 'update' with task ID and action
|
|
176
|
+
program
|
|
177
|
+
.command('update')
|
|
178
|
+
.alias('up')
|
|
179
|
+
.description('Update a task by ID')
|
|
180
|
+
.action(async () =>{
|
|
181
|
+
const tasks = await loadTasks();
|
|
182
|
+
if (tasks.length === 0) {
|
|
183
|
+
console.log('No tasks found to update.');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const { id } = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: 'list',
|
|
190
|
+
name: 'id',
|
|
191
|
+
message: 'Select the task to update:',
|
|
192
|
+
choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
|
|
193
|
+
}
|
|
194
|
+
]);
|
|
195
|
+
const { status } = await inquirer.prompt([
|
|
196
|
+
{
|
|
197
|
+
type: 'list',
|
|
198
|
+
name: 'status',
|
|
199
|
+
message: 'Select the new status:',
|
|
200
|
+
choices: ALLOWED_STATUSES
|
|
201
|
+
}
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const task = tasks.find(t => t.id === Number(id));
|
|
205
|
+
if (!task) {
|
|
206
|
+
console.error('Task not found!');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
task.status = status;
|
|
210
|
+
|
|
211
|
+
await saveTasks(tasks);
|
|
212
|
+
console.log('Task updated successfully!');
|
|
213
|
+
|
|
214
|
+
console.table([{
|
|
215
|
+
ID: task.id,
|
|
216
|
+
Title: task.title,
|
|
217
|
+
Status: task.status,
|
|
218
|
+
Priority: task.priority,
|
|
219
|
+
DueDate: task.dueDate,
|
|
220
|
+
Description: task.description || ''
|
|
221
|
+
}]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// use command 'delete' with task ID and action
|
|
225
|
+
program
|
|
226
|
+
.command('delete')
|
|
227
|
+
.alias('del')
|
|
228
|
+
.description('delete a task by ID')
|
|
229
|
+
.action(async () => {
|
|
230
|
+
const tasks = await loadTasks();
|
|
231
|
+
if (tasks.length === 0) {
|
|
232
|
+
console.log('No tasks found to delete.');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { id } = await inquirer.prompt([
|
|
237
|
+
{
|
|
238
|
+
type: 'list',
|
|
239
|
+
name: 'id',
|
|
240
|
+
message: 'Select the task to delete:',
|
|
241
|
+
choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
|
|
242
|
+
}
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
const { confirm } = await inquirer.prompt([
|
|
246
|
+
{
|
|
247
|
+
type: 'confirm',
|
|
248
|
+
name: 'confirm',
|
|
249
|
+
message: 'Are you sure you want to delete this task?',
|
|
250
|
+
default: false
|
|
251
|
+
}
|
|
252
|
+
]);
|
|
253
|
+
if (!confirm) {
|
|
254
|
+
console.log('Task deletion cancelled.');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const newTasks = tasks.filter(t => t.id !== id);
|
|
259
|
+
await saveTasks(newTasks);
|
|
260
|
+
console.log('Task deleted successfully!');
|
|
261
|
+
|
|
262
|
+
console.table([{
|
|
263
|
+
ID: task.id,
|
|
264
|
+
Title: task.title,
|
|
265
|
+
Status: task.status,
|
|
266
|
+
Priority: task.priority,
|
|
267
|
+
DueDate: task.dueDate,
|
|
268
|
+
Description: task.description || ''
|
|
269
|
+
}]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// parse command line arguments
|
|
273
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taskninja",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "a simple CLI application with JS as a (To-Do Application)",
|
|
5
|
+
"main": "app.js",
|
|
6
|
+
"type" : "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
|
+
"start": "node app.js",
|
|
10
|
+
"dev": "node app.js"
|
|
11
|
+
},
|
|
12
|
+
"bin" : {
|
|
13
|
+
"dotask": "./app.js"
|
|
14
|
+
},
|
|
15
|
+
"author": "Moahmed Bakr",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^14.0.2",
|
|
19
|
+
"inquirer": "^13.2.1"
|
|
20
|
+
}
|
|
21
|
+
}
|