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.
package/src/index.js DELETED
@@ -1,523 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- /**
4
- * TASKNINJA -
5
- * Task Manager CLI Application
6
- * This application allows users to manage their tasks via command line interface.
7
- * It supports adding, listing, and removing tasks.
8
- * Dependencies:
9
- * - commander: For command line argument parsing
10
- * - inquirer: For interactive prompts
11
- * - fs: For file system operations
12
- * Author: Mohamed Bakr
13
- * Date: January 2026
14
- * Version: 1.1.5
15
- * @license MIT
16
- * Copyright (c) 2026 Mohamed Bakr
17
- */
18
-
19
- // for using commands in terminal
20
- import { Command } from "commander";
21
- // for interactive command line prompts
22
- import inquirer from "inquirer";
23
- // for colored text
24
- import chalk from 'chalk';
25
-
26
- // helpers
27
- import {displayTasks, enableEscExit, cleanupAndExit} from './helpers/helpers.js';
28
-
29
- // assigning Commander to a variable
30
- const program = new Command();
31
-
32
- // importing validators and allowed values
33
- import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "./utils/validators.js";
34
- // importing task service functions
35
- import { loadTasks, saveTasks, getNextId, saveDeletedTask, loadDeletedTask, clearDeletedTask, cleanupExpiredDeletedTasks } from "./utils/taskService.js";
36
-
37
-
38
- // setting up
39
- program
40
- .name("taskninja")
41
- .description("A simple CLI application to manage your tasks")
42
- .version("1.1.6");
43
-
44
- // use command 'add' with title + status + priority + dueDate + description and action
45
- program
46
- .command('add')
47
- .alias('a')
48
- .description('Add a new task')
49
- .action(async () => {
50
- enableEscExit();
51
- const answers = await inquirer.prompt([
52
- {
53
- type: 'input',
54
- name: 'title',
55
- message : 'Task Title:',
56
- validate : input => input ? true : 'Title cannot be empty!'
57
- },
58
- {
59
- type: 'rawlist',
60
- name: 'status',
61
- message : 'Task Status:',
62
- choices : ALLOWED_STATUSES,
63
- default : 'todo'
64
- },
65
- {
66
- type: 'rawlist',
67
- name: 'priority',
68
- message : 'Task Priority:',
69
- choices : ALLOWED_PRIORITIES,
70
- default : 'medium'
71
- },
72
- {
73
- type: 'input',
74
- name: 'dueDate',
75
- message : 'Due Date (YYYY-MM-DD):',
76
- default : () => {
77
- const today = new Date();
78
- return today.toISOString().split('T')[0];
79
- },
80
- filter: (input) => {
81
- const trimmed = input.trim();
82
- if (!trimmed) {
83
- return new Date().toISOString().split('T')[0];
84
- }
85
- return trimmed;
86
- },
87
- validate : input => {
88
- try {
89
- validateDueDate(input);
90
- return true;
91
- } catch (error) {
92
- return error.message;
93
- }
94
- }
95
- },
96
- {
97
- type: 'input',
98
- name: 'description',
99
- message : 'Task Description (optional):'
100
- },
101
- ]);
102
- // load existing tasks
103
- const tasks = await loadTasks();
104
-
105
- // create new task object
106
- const newTask = {
107
- id: getNextId(tasks),
108
- title: answers.title,
109
- status: answers.status,
110
- priority: answers.priority,
111
- dueDate: answers.dueDate,
112
- description: answers.description || ''
113
- };
114
- // add new task to tasks array
115
- tasks.push(newTask);
116
- // save updated tasks to file
117
- await saveTasks(tasks);
118
- console.log(chalk.green('Task added successfully!'));
119
- });
120
-
121
-
122
- // use command 'list' with optional status filter and action
123
- program
124
- .command('list')
125
- .alias('ls')
126
- .description('List all tasks')
127
- .option('-s, --status <status>', 'Filter tasks by status (todo, in-progress, done)')
128
- .action(async (options) => {
129
- let tasks = await loadTasks();
130
-
131
- if (options.status) {
132
- if (!ALLOWED_STATUSES.includes(options.status)){
133
- console.log(chalk.red(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`));
134
- cleanupAndExit(0);
135
- }
136
- tasks = tasks.filter(task => task.status === options.status);
137
- }
138
- // display all tasks in table format
139
- displayTasks(tasks);
140
- cleanupAndExit(0);
141
- });
142
-
143
-
144
-
145
- // use command 'search' to find tasks by keyword in title or description
146
- program
147
- .command('search [keyword]')
148
- .alias('sr')
149
- .option('-f, --find <keyword>', 'Keyword to search in title or description')
150
- .description('Search tasks by keyword in title or description')
151
- .action(async (keyword, options) => {
152
- const tasks = await loadTasks();
153
- let searchTerm = keyword || options.find;
154
-
155
- if (!searchTerm) {
156
- enableEscExit();
157
- const answers = await inquirer.prompt([
158
- {
159
- type: 'input',
160
- name: 'term',
161
- message: 'Enter the keyword you want to search for:',
162
- validate: input => input ? true : 'Keyword cannot be empty!'
163
- },
164
- {
165
- type: 'rawlist',
166
- name: 'field',
167
- message: 'Where do you want to search?',
168
- choices: ['title', 'description', 'both'],
169
- default: 'both'
170
- }
171
- ]);
172
- searchTerm = answers.term;
173
- const field = answers.field;
174
-
175
- const founded = tasks.filter(task => {
176
- if (field === 'title') return task.title.toLowerCase().includes(searchTerm.toLowerCase());
177
- if (field === 'description') return task.description.toLowerCase().includes(searchTerm.toLowerCase());
178
- return task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
179
- task.description.toLowerCase().includes(searchTerm.toLowerCase());
180
- });
181
-
182
- if (!founded.length) {
183
- console.log(chalk.red('No task matched your search!'));
184
- cleanupAndExit(0);
185
- }
186
-
187
- displayTasks(founded);
188
- cleanupAndExit(0);
189
- }
190
-
191
- const founded = tasks.filter(task =>
192
- task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
193
- task.description.toLowerCase().includes(searchTerm.toLowerCase())
194
- );
195
-
196
- if (!founded.length) {
197
- console.log(chalk.red('No task matched your search!'));
198
- cleanupAndExit(0);
199
- }
200
-
201
- displayTasks(founded);
202
- cleanupAndExit(0);
203
- });
204
-
205
-
206
-
207
-
208
- // use command 'sort' to sort tasks by due date, priority, or status
209
- program
210
- .command('sort')
211
- .alias('so')
212
- .option('--by <criteria>', 'Sort tasks by criteria (dueDate, priority, status)')
213
- .description('Sort tasks by due date, priority, or status')
214
- .action( async (options) => {
215
-
216
- const tasks = await loadTasks();
217
- let criteria = options.by;
218
-
219
- if (!criteria) {
220
- enableEscExit();
221
- const answer = await inquirer.prompt([
222
- {
223
- type: 'rawlist',
224
- name: 'criteria',
225
- message: 'Sort tasks by:',
226
- choices: ['dueDate', 'priority', 'status']
227
- }
228
- ]);
229
- criteria = answer.criteria;
230
- }
231
- // normailze input `case-insensitive + separators`
232
- criteria = criteria.toLowerCase().replace(/[-_]/g, '');
233
-
234
- if (!['duedate', 'priority', 'status'].includes(criteria)) {
235
- console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
236
- cleanupAndExit(0);
237
- }
238
- // sorting logic
239
- const sortedTasks = [...tasks];
240
-
241
- switch (criteria) {
242
- case 'duedate':
243
- sortedTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
244
- break;
245
- case 'priority':
246
- const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
247
- sortedTasks.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
248
- break;
249
- case 'status':
250
- const statusOrder = { 'todo': 1, 'in-progress': 2, 'done': 3 };
251
- sortedTasks.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
252
- break;
253
- default:
254
- console.log(chalk.red('Invalid sort criteria. Use dueDate, priority, or status.'));
255
- cleanupAndExit(0);
256
- }
257
-
258
- displayTasks(sortedTasks);
259
- cleanupAndExit(0);
260
- });
261
-
262
-
263
- // use command 'update' with task ID and action
264
- program
265
- .command('update')
266
- .alias('up')
267
- .description('Update a task by ==> ID <==')
268
- .action(async () =>{
269
- const tasks = await loadTasks();
270
- if (!tasks.length) {
271
- console.log(chalk.red('No tasks found to update.'));
272
- cleanupAndExit(0);
273
- }
274
- enableEscExit();
275
- const { id } = await inquirer.prompt([
276
- {
277
- type: 'rawlist',
278
- name: 'id',
279
- message: 'Select the task to update (By ID):',
280
- choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
281
- }
282
- ]);
283
-
284
- // find the task to update
285
- const task = tasks.find(t => t.id === Number(id));
286
- if (!task) {
287
- console.log(chalk.red('Task not found!'));
288
- cleanupAndExit(0);
289
- }
290
-
291
- enableEscExit();
292
- const answers = await inquirer.prompt([
293
- // for title
294
- {
295
- type: 'confirm',
296
- name: 'changeTitle',
297
- message: 'Do you want to change the title?',
298
- default: false
299
- },
300
- {
301
- type: 'input',
302
- name: 'title',
303
- message: 'Enter the new title:',
304
- when: answers => answers.changeTitle,
305
- validate: input => input ? true : 'Title cannot be empty!'
306
- },
307
-
308
- // for status
309
- {
310
- type: 'confirm',
311
- name: 'changeStatus',
312
- message: 'Do you want to change the status?',
313
- default: false
314
- },
315
- {
316
- type: 'rawlist',
317
- name: 'status',
318
- message: 'Select the new status:',
319
- choices: ALLOWED_STATUSES,
320
- when: answers => answers.changeStatus
321
- },
322
-
323
- // for priority
324
- {
325
- type: 'confirm',
326
- name: 'changePriority',
327
- message: 'Do you want to change the priority?',
328
- default: false
329
- },
330
- {
331
- type: 'rawlist',
332
- name: 'priority',
333
- message: 'Select the new priority:',
334
- choices: ALLOWED_PRIORITIES,
335
- when: answers => answers.changePriority
336
- },
337
-
338
- // for due date
339
- {
340
- type: 'confirm',
341
- name: 'changeDueDate',
342
- message: 'Do you want to change the due date?',
343
- default: false
344
- },
345
- {
346
- type: 'input',
347
- name: 'dueDate',
348
- message: 'Enter the new due date (YYYY-MM-DD):',
349
- when: answers => answers.changeDueDate,
350
- validate: input => {
351
- try {
352
- validateDueDate(input);
353
- return true;
354
- } catch (error) {
355
- return error.message;
356
- }
357
- }
358
- },
359
-
360
- // for description
361
- {
362
- type: 'confirm',
363
- name: 'changeDescription',
364
- message: 'Do you want to change the description?',
365
- default: false
366
- },
367
- {
368
- type: "input",
369
- name: 'description',
370
- message: 'Enter the new description:',
371
- when: answers => answers.changeDescription,
372
- validate: input => input.trim() ? true : 'Description cannot be empty!'
373
- }
374
- ]);
375
-
376
- // apply updates only if user chose to change them
377
- if (answers.changeTitle) task.title = answers.title;
378
- if (answers.changeStatus) task.status = answers.status;
379
- if (answers.changePriority) task.priority = answers.priority;
380
- if (answers.changeDueDate) task.dueDate = answers.dueDate;
381
- if (answers.changeDescription)
382
- task.description = answers.description || '';
383
-
384
- // is there any change?
385
- const hasChanges = [
386
- answers.changeTitle,
387
- answers.changeStatus,
388
- answers.changePriority,
389
- answers.changeDueDate,
390
- answers.changeDescription
391
- ].some(Boolean);
392
-
393
- if (!hasChanges) {
394
- console.log(chalk.yellow('No changes were made.'));
395
- }else {
396
- // save updated tasks to file
397
- await saveTasks(tasks);
398
- console.log(chalk.green('Task updated successfully!'));
399
- }
400
-
401
- displayTasks(tasks);
402
- cleanupAndExit(0);
403
- });
404
-
405
-
406
- // use command 'done' with task ID instead of 'update' + confirm to mark task as done
407
- program
408
- .command('done')
409
- .description('Mark a task as done by ==> ID <==')
410
- .action(async () => {
411
- const tasks = await loadTasks();
412
- if (!tasks.length) {
413
- console.log(chalk.magenta('Congratulations! All tasks are already done.'));
414
- cleanupAndExit(0);
415
- }
416
-
417
- const activeTasks = tasks.filter(t => t.status !== 'done');
418
- if (!activeTasks.length) {
419
- console.log(chalk.magenta('Congratulations! All tasks are already done.'));
420
- cleanupAndExit(0);
421
- }
422
- enableEscExit();
423
- const { id } = await inquirer.prompt([
424
- {
425
- type: 'rawlist',
426
- name: 'id',
427
- message: 'Select the task to mark as done:',
428
- choices: activeTasks.map(t => (
429
- {
430
- name: `${t.id}: ${t.title}[Current Status: ${t.status}]`,
431
- value: t.id
432
- }
433
- ))
434
- }
435
- ]);
436
-
437
- const task = tasks.find(t => t.id === Number(id));
438
- task.status = 'done';
439
- await saveTasks(tasks);
440
- console.log(chalk.green('Task marked as done successfully!'));
441
-
442
- displayTasks(tasks);
443
- cleanupAndExit(0);
444
- });
445
-
446
-
447
-
448
- // use command 'delete' with task ID and action
449
- program
450
- .command('delete')
451
- .alias('del')
452
- .description('delete a task by ==> ID <==')
453
- .action(async () => {
454
- const tasks = await loadTasks();
455
- if (!tasks.length) {
456
- console.log(chalk.yellow('No tasks found to delete.'));
457
- cleanupAndExit(0);
458
- }
459
- enableEscExit();
460
- const { id } = await inquirer.prompt([
461
- {
462
- type: 'rawlist',
463
- name: 'id',
464
- message: 'Select the task to delete:',
465
- choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
466
- }
467
- ]);
468
- enableEscExit();
469
- const { confirm } = await inquirer.prompt([
470
- {
471
- type: 'confirm',
472
- name: 'confirm',
473
- message: 'Are you sure you want to delete this task?',
474
- default: false
475
- }
476
- ]);
477
- if (!confirm) {
478
- console.log(chalk.yellow('Task deletion cancelled.'));
479
- cleanupAndExit(0);
480
- }
481
- const taskToDelete = tasks.find(t => t.id === Number(id));
482
- // save deleted task for undo functionality
483
- await saveDeletedTask(taskToDelete);
484
- // filter out the deleted task
485
- const newTasks = tasks.filter(t => t.id !== Number(id));
486
-
487
- await saveTasks(newTasks);
488
- console.log(chalk.green('Task deleted successfully!'));
489
- console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
490
-
491
- displayTasks(newTasks);
492
- cleanupAndExit(0);
493
- });
494
-
495
- // use command 'undo' to restore last deleted task
496
- program
497
- .command('undo')
498
- .alias('un')
499
- .description('Undo the last deleted task')
500
- .action( async () => {
501
- await cleanupExpiredDeletedTasks();
502
-
503
- const deletedTasks = await loadDeletedTask();
504
- if (!deletedTasks || !deletedTasks.length) {
505
- console.log(chalk.yellow('No deleted task to restore.'));
506
- cleanupAndExit(0);
507
- }
508
-
509
- const lastDeletedTask = deletedTasks.pop();
510
-
511
- const tasks = await loadTasks();
512
- tasks.push(lastDeletedTask);
513
- tasks.sort((a, b) => a.id - b.id); // keep tasks sorted by ID
514
- await saveTasks(tasks);
515
- // await clearDeletedTask();
516
-
517
- console.log(chalk.green(`Last deleted task restored successfully!, (Task name: ${lastDeletedTask.title})`));
518
- });
519
-
520
-
521
-
522
- // parse command line arguments
523
- program.parse(process.argv);