taskninja 1.0.3 → 1.1.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/README.md CHANGED
@@ -1,30 +1,27 @@
1
1
  # TaskNinja
2
2
 
3
- ![TaskNinja Logo](https://via.placeholder.com/150?text=TaskNinja) <!-- Optional: Replace with actual logo if available -->
3
+ **Version:** 1.1.0
4
+ **Description:** A simple CLI To-Do Application to manage your tasks directly from the terminal. Includes colored tables, search, soft delete, undo, and interactive sort.
4
5
 
5
- A simple Command-Line Interface (CLI) application built with JavaScript for managing your to-do tasks. It allows users to add, list, update, and delete tasks interactively, with features like status tracking, priority levels, due dates, and descriptions. Tasks are stored in a local JSON file for persistence.
6
+ ---
6
7
 
7
- This project is designed for developers and users who prefer a lightweight, terminal-based task manager without the need for complex setups or databases.
8
+ ## Table of Contents
8
9
 
9
- ## Features
10
+ * [Installation](#installation)
11
+ * [Running the CLI](#running-the-cli)
12
+ * [Commands](#commands)
10
13
 
11
- - **Add Tasks**: Create new tasks with title, status (todo, in-progress, done), priority (low, medium, high), due date, and optional description.
12
- - **List Tasks**: View all tasks or filter by status in a tabular format.
13
- - **Update Tasks**: Change the status of existing tasks via interactive selection.
14
- - **Delete Tasks**: Remove tasks with confirmation to prevent accidental deletion.
15
- - **Persistent Storage**: Tasks are saved to a local `todos.json` file.
16
- - **Interactive Prompts**: Uses Inquirer for user-friendly input.
17
- - **Validation**: Ensures valid statuses, priorities, and date formats.
14
+ * [Add Task](#add-task)
15
+ * [List Tasks](#list-tasks)
16
+ * [Update Task](#update-task)
17
+ * [Delete Task](#delete-task)
18
+ * [Undo Delete Task](#undo-delete-task)
19
+ * [Search Tasks](#search-tasks)
20
+ * [Sort Tasks](#sort-tasks)
21
+ * [Task Fields & Allowed Values](#task-fields--allowed-values)
22
+ * [Examples](#examples)
18
23
 
19
- ## Technologies Used
20
-
21
- - **Node.js**: Runtime environment.
22
- - **Commander**: For parsing command-line arguments and defining commands.
23
- - **Inquirer**: For interactive CLI prompts.
24
- - **fs/promises**: For asynchronous file system operations to handle task storage.
25
- - **ECMAScript Modules (ESM)**: Modern JavaScript module system.
26
-
27
- The application is written in pure JavaScript (ES6+), with no additional frameworks or databases required.
24
+ ---
28
25
 
29
26
  ## Installation
30
27
 
@@ -36,198 +33,350 @@ TaskNinja is published on npm as `taskninja`. Install it globally to use the CLI
36
33
  npm install -g taskninja
37
34
  ```
38
35
 
39
- After installation, you can run the CLI using the `dotask` command (as defined in `package.json`).
40
-
41
- ### From Source
42
-
43
- 1. Clone the repository:
44
- ```bash
45
- git clone https://github.com/yourusername/taskninja.git
46
- cd taskninja
47
- ```
48
-
49
- 2. Install dependencies:
50
- ```bash
51
- npm install
52
- ```
53
-
54
- 3. Link the CLI globally (optional, for testing):
55
- ```bash
56
- npm link
57
- ```
36
+ or use it instantly with:
37
+
38
+ ```bash
39
+ npx taskninja
40
+ npx taskninja <command>
41
+ ```
42
+
43
+ Clone the repository and install dependencies:
44
+
45
+ ```bash
46
+ git clone <your-repo-url>
47
+ cd taskninja
48
+ npm install
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Running the CLI
54
+
55
+ You can run the CLI using either `node` or the bin aliases:
56
+
57
+ ```bash
58
+ # Using node
59
+ node app.js <command>
60
+
61
+ # Using bin aliases
62
+ dotask <command>
63
+ taskninja <command>
64
+ tn <command>
65
+ ```
66
+
67
+ Replace `<command>` with any of the available commands: `add`, `list`, `update`, `delete`, `undo`, `search`, `sort`.
68
+
69
+ ---
70
+
71
+ ## Commands
58
72
 
59
- 4. Run the application:
60
- ```bash
61
- npm start
62
- ```
63
- Or directly:
64
- ```bash
65
- node app.js
66
- ```
73
+ ### 1. Add Task
67
74
 
68
- ## Usage
75
+ **Alias:** `a`
76
+ **Description:** Add a new task interactively.
69
77
 
70
- Run the CLI with the `dotask` command (after global installation) or `node app.js` from the source.
78
+ ```bash
79
+ tn a
80
+ ```
71
81
 
72
- ### Commands
82
+ **Prompts:**
73
83
 
74
- - **Add a Task**:
75
- ```bash
76
- dotask add
77
- ```
78
- - This will prompt you interactively for title, status, priority, due date, and description.
79
- - Example output: "Task added successfully!"
84
+ 1. **Task Title:** Enter any text (cannot be empty).
80
85
 
81
- - **List Tasks**:
82
- ```bash
83
- dotask list
84
- ```
85
- - Lists all tasks in a table format.
86
- - Optional filter by status: `dotask list --status todo` (or `-s todo`).
87
- - Aliases: `dotask ls`.
86
+ 2. **Task Status:** Select from allowed values using arrows:
88
87
 
89
- - **Update a Task**:
90
- ```bash
91
- dotask update
92
- ```
93
- - Prompts to select a task by ID and update its status.
94
- - Displays the updated task in a table.
95
- - Aliases: `dotask up`.
88
+ * todo
89
+ * in-progress
90
+ * done
96
91
 
97
- - **Delete a Task**:
98
- ```bash
99
- dotask delete
100
- ```
101
- - Prompts to select a task by ID and confirm deletion.
102
- - Aliases: `dotask del`.
92
+ 3. **Task Priority:** Select from allowed values:
103
93
 
104
- - **Help**:
105
- ```bash
106
- dotask --help
107
- ```
108
- - Shows all available commands and options.
94
+ * low
95
+ * medium
96
+ * high
109
97
 
110
- - **Version**:
111
- ```bash
112
- dotask --version
113
- ```
114
- - Displays the current version (1.0.0).
98
+ 4. **Due Date:** Enter in `YYYY-MM-DD` format. Invalid dates will show a validation error.
115
99
 
116
- ### Examples
100
+ 5. **Description:** Optional text.
117
101
 
118
- 1. Adding a task:
119
- ```
120
- dotask add
121
- ```
122
- - Follow prompts: Enter "Buy groceries" as title, select "todo" status, "medium" priority, "2024-12-31" due date, and "Milk, eggs, bread" as description.
102
+ **Behavior:**
123
103
 
124
- 2. Listing tasks filtered by status:
125
- ```
126
- dotask list -s in-progress
127
- ```
104
+ * Task is saved in `todos.json`.
105
+ * Confirmation message: `Task added successfully!`
128
106
 
129
- 3. Updating a task:
130
- ```
131
- dotask update
132
- ```
133
- - Select task ID from the list and choose new status (e.g., "done").
107
+ ---
134
108
 
135
- 4. Deleting a task:
136
- ```
137
- dotask delete
138
- ```
139
- - Select task ID and confirm.
109
+ ### 2. List Tasks
140
110
 
141
- Tasks are stored in `./todos.json` relative to where the command is run. Ensure write permissions in the directory.
111
+ **Alias:** `ls`
112
+ **Description:** List all tasks, optionally filtered by status.
142
113
 
143
- ## Configuration
114
+ ```bash
115
+ tn ls
116
+ tn ls --status todo
117
+ ```
144
118
 
145
- - **Task File**: Defaults to `./todos.json`. You can modify `TASKS_FILE` in `app.js` if needed.
146
- - **Statuses**: Limited to "todo", "in-progress", "done".
147
- - **Priorities**: Limited to "low", "medium", "high".
148
- - **Due Date Format**: YYYY-MM-DD (validated during input).
119
+ **Behavior:**
149
120
 
150
- No external configuration files are required.
121
+ * Displays tasks in a **colored table** using chalk:
151
122
 
152
- ## Development
123
+ | # | ID | Title | Status | Priority | DueDate | Description |
124
+ | - | -- | ----- | ------ | -------- | ------- | ----------- |
125
+
126
+ * Status colors: `todo` → blue, `in-progress` → yellow, `done` → green
127
+ * Priority colors: `low` → green, `medium` → yellow, `high` → red
128
+
129
+ ---
130
+
131
+ ### 3. Update Task
132
+
133
+ **Alias:** `up`
134
+ **Description:** Update an existing task by selecting its ID.
153
135
 
154
- To run in development mode:
155
136
  ```bash
156
- npm run dev
137
+ tn up
157
138
  ```
158
139
 
159
- This executes `node app.js` for quick testing.
140
+ **Steps:**
141
+
142
+ 1. Select task by ID from a list.
143
+ 2. Confirm which fields to change (title, status, priority, due date, description).
144
+ 3. Enter new values where applicable.
145
+
146
+ **Behavior:**
147
+
148
+ * Only fields selected for change are updated.
149
+ * Confirmation message: `Task updated successfully!`
150
+ * Shows updated **colored task table**.
151
+
152
+ ---
153
+
154
+ ### 4. Delete Task
155
+
156
+ **Alias:** `del`
157
+ **Description:** Soft delete a task by selecting its ID.
158
+
159
+ ```bash
160
+ tn del
161
+ ```
162
+
163
+ **Steps:**
164
+
165
+ 1. Select task by ID from a list.
166
+ 2. Confirm deletion.
167
+
168
+ **Behavior:**
169
+
170
+ * Task is removed from `todos.json` **but saved in `deleted-todos.json`**.
171
+ * Confirmation: `Task deleted successfully!`
172
+ * You can restore it using the `undo` command.
173
+
174
+ ---
175
+
176
+ ### 5. Undo Delete Task
177
+
178
+ **Alias:** `un`
179
+ **Description:** Restore the last deleted task.
180
+
181
+ ```bash
182
+ tn un
183
+ ```
184
+
185
+ **Behavior:**
186
+
187
+ * Restores the last deleted task from `deleted-todos.json`.
188
+ * Confirmation: `Last deleted task restored successfully!, (Task name: taskName)`
189
+
190
+ ---
191
+
192
+ Here’s the **Search Tasks** section in the README updated in English after your CLI changes:
193
+
194
+ ---
195
+
196
+ ### 6. Search Tasks
197
+
198
+ **Alias:** `sr`
199
+ **Description:** Search tasks by keyword in title or description.
200
+
201
+ ```bash
202
+ # Search interactively
203
+ tn search
204
+
205
+ # Or search directly using --find
206
+ tn search --find "meeting"
207
+ ```
208
+
209
+ **Behavior:**
210
+
211
+ * If you run `tn search` without a keyword, the CLI will ask you:
160
212
 
161
- ### Scripts
213
+ 1. Enter the keyword you want to search for.
214
+ 2. Choose where to search: in the title, description, or both.
162
215
 
163
- - `npm start`: Runs the app.
164
- - `npm test`: Placeholder for tests (currently errors out; add tests as needed).
216
+ * If you use `--find <keyword>` directly, the CLI will return all tasks containing that keyword in the title or description.
217
+
218
+ **Example:**
219
+
220
+ ```bash
221
+ tn search
222
+ ```
223
+
224
+ ```
225
+ Enter the keyword you want to search for: meeting
226
+ Where do you want to search? (Use arrow keys)
227
+ 1. title
228
+ 2. description
229
+ 3. both
230
+ ```
231
+
232
+ ```bash
233
+ tn search --find "meeting"
234
+ ```
235
+
236
+ ┌─┬────┬────────────┬─────────────┬─────────┬────────────┬───────────────────┐
237
+ │#│ ID │ Title │ Status │ Priority│ DueDate │ Description │
238
+ ├─┼────┼────────────┼─────────────┼─────────┼────────────┼───────────────────┤
239
+ │1│ 3 │ Team meeting│ todo │ medium │ 2026-02-01 │ Discuss project │
240
+ └─┴────┴────────────┴─────────────┴─────────┴────────────┴───────────────────┘
241
+
242
+ ---
243
+
244
+ ### 7. Sort Tasks
245
+
246
+ **Alias:** `so`
247
+ **Description:** Sort tasks interactively by due date, priority, or status.
248
+
249
+ ```bash
250
+ tn so
251
+ ```
252
+
253
+ **Behavior:**
254
+
255
+ * Prompts: `Sort tasks by:` → select from `dueDate`, `priority`, `status`.
256
+ * Displays **sorted colored table**.
257
+ * Can also pass `--by` option: `tn so --by priority`
258
+
259
+ ---
260
+
261
+ ## Task Fields & Allowed Values
262
+
263
+ | Field | Allowed Values / Description |
264
+ | ----------- | ---------------------------- |
265
+ | title | Any non-empty string |
266
+ | status | todo, in-progress, done |
267
+ | priority | low, medium, high |
268
+ | dueDate | YYYY-MM-DD |
269
+ | description | Optional text |
270
+
271
+ ---
272
+
273
+ ## Examples
274
+
275
+ **Adding a Task:**
276
+
277
+ ```bash
278
+ tn a
279
+ ```
280
+
281
+ ```
282
+ Task Title: Buy groceries
283
+ Task Status: → todo
284
+ Task Priority: → medium
285
+ Due Date (YYYY-MM-DD): 2026-02-01
286
+ Task Description (optional): Buy fruits and vegetables
287
+ ```
288
+
289
+ **Listing Tasks:**
290
+
291
+ ```bash
292
+ tn ls
293
+ ```
294
+
295
+ ```
296
+ ┌─┬────┬────────────────┬─────────────┬─────────┬────────────┬─────────────────────────────┐
297
+ │#│ ID │ Title │ Status │ Priority│ DueDate │ Description │
298
+ ├─┼────┼────────────────┼─────────────┼─────────┼────────────┼─────────────────────────────┤
299
+ │1│ 1 │ Buy groceries │ todo │ medium │ 2026-02-01 │ Buy fruits and vegetables │
300
+ └─┴────┴────────────────┴─────────────┴─────────┴────────────┴─────────────────────────────┘
301
+ ```
302
+
303
+ **Updating a Task:**
304
+
305
+ ```bash
306
+ tn up
307
+ ```
308
+
309
+ **Deleting a Task:**
310
+
311
+ ```bash
312
+ tn del
313
+ ```
314
+
315
+ **Undo Last Deleted Task:**
316
+
317
+ ```bash
318
+ tn un
319
+ ```
320
+
321
+ **Search Tasks:**
322
+
323
+ ```bash
324
+ tn sr groceries
325
+ ```
326
+
327
+ **Sort Tasks:**
328
+
329
+ ```bash
330
+ tn so
331
+ ```
165
332
 
166
- ## Contribution Guidelines
333
+ * Select `dueDate`, `priority`, or `status` interactively
334
+ * Or use `--by` option: `tn so --by priority`
167
335
 
168
- We welcome contributions to improve TaskNinja! Whether it's bug fixes, new features, or documentation enhancements, follow these steps:
336
+ ---
169
337
 
170
- ### How to Contribute
338
+ ## Notes
171
339
 
172
- 1. **Fork the Repository**:
173
- - Go to the [GitHub repo](https://github.com/yourusername/taskninja) and click "Fork".
340
+ * All selection prompts use **arrow keys** in terminal.
341
+ * Task table always shows **numbered index (`#`) starting from 1**.
342
+ * Status and priority are displayed in **colors** for better readability.
174
343
 
175
- 2. **Clone Your Fork**:
176
- ```bash
177
- git clone https://github.com/yourusername/taskninja.git
178
- cd taskninja
179
- ```
344
+ ---
180
345
 
181
- 3. **Create a Branch**:
182
- - Use a descriptive name: `git checkout -b feature/new-feature` or `git checkout -b fix/bug-fix`.
346
+ ---
183
347
 
184
- 4. **Make Changes**:
185
- - Follow the existing code style (ESLint can be added later for consistency).
186
- - Add or update tests if applicable.
187
- - Ensure your code handles errors gracefully and maintains validation logic.
348
+ ## Screenshots / Demo
188
349
 
189
- 5. **Commit Changes**:
190
- - Use clear commit messages: e.g., "feat: add search functionality" or "fix: resolve date validation issue".
191
- - Reference issues if relevant: e.g., "Closes #123".
350
+ **Adding a Task:**
192
351
 
193
- 6. **Push to Your Fork**:
194
- ```bash
195
- git push origin feature/new-feature
196
- ```
352
+ ![Add Task](images/add.png "Adding a Task in TaskNinja CLI")
197
353
 
198
- 7. **Open a Pull Request (PR)**:
199
- - Go to the original repo and click "New Pull Request".
200
- - Provide a detailed description of your changes, including why they're needed and any relevant screenshots.
201
- - Link to any related issues.
354
+ **Listing Tasks:**
202
355
 
203
- ### Guidelines
356
+ ![List Tasks](images/list.png "Listing Tasks in TaskNinja CLI")
204
357
 
205
- - **Code Style**: Use consistent indentation (2 spaces), semicolons, and follow ES6+ best practices.
206
- - **Testing**: Add unit tests using a framework like Jest if expanding features. Currently, no tests are implemented—contributions here are encouraged!
207
- - **Dependencies**: Keep them minimal. Justify any new additions.
208
- - **Features**: New commands or options should align with the simple CLI philosophy. Discuss major changes in an issue first.
209
- - **Documentation**: Update README.md with any new features or changes.
210
- - **Issues**: Check for existing issues before creating a new one. Use labels like "bug", "enhancement", or "question".
358
+ **Updating a Task:**
211
359
 
212
- ### Code of Conduct
360
+ ![Update Task](images/update.png "Updating a Task")
213
361
 
214
- - Be respectful and inclusive.
215
- - No harassment or discrimination.
216
- - Report issues to the maintainer (Mohamed Bakr).
362
+ **Deleting a Task:**
217
363
 
218
- By contributing, you agree that your contributions will be licensed under the ISC License.
364
+ ![Delete Task](images/delete.png "Deleting a Task")
365
+ ![Delete Task](images/delete2.png "Deleting a Task")
219
366
 
220
- ## License
367
+ **Restore Last-Deleted Task:**
221
368
 
222
- This project is licensed under the ISC License.
369
+ ![Undo](images/undo.png "Restore Last-deleted task")
223
370
 
224
- ## Author
371
+ **Search / Sort Tasks:**
225
372
 
226
- - **Mohamed Bakr** - Just a Developer - [GitHub Profile](https://github.com/MoBMoCaffeine)
373
+ ![Search Tasks](images/search.png "Searching Tasks")
374
+ ![Sort Tasks](images/sort.png "Sorting Tasks")
375
+ ![Sort Tasks-2](images/sort2.png "Sorting Tasks")
227
376
 
228
- ## Acknowledgments
377
+ **Mark as Done:**
229
378
 
230
- - Inspired by simple CLI tools like Todoist CLI.
231
- - Thanks to the open-source community for libraries like Commander and Inquirer.
379
+ ![Mark Task As Done](images/done.png "Mark as Done In one step")
380
+ ![Mark Task As Done](images/done2.png "Mark as Done In one step")
232
381
 
233
- If you encounter issues or have suggestions, open an issue on GitHub!
382
+ ---
package/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env node
2
-
3
2
  /**
3
+ * TASKNINJA -
4
4
  * Task Manager CLI Application
5
5
  * This application allows users to manage their tasks via command line interface.
6
6
  * It supports adding, listing, and removing tasks.
@@ -9,78 +9,78 @@
9
9
  * - inquirer: For interactive prompts
10
10
  * - fs: For file system operations
11
11
  * Author: Mohamed Bakr
12
- * Date: June 2024
13
- * Version: 1.0.0
12
+ * Date: January 2024
13
+ * Version: 1.1.0
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
- // for file system operations
21
- import fs from "fs/promises";
20
+ // supporting colors in table forms
21
+ import Table from 'cli-table3';
22
+ // for colored text
23
+ import chalk from 'chalk';
22
24
 
23
25
  // assigning Commander to a variable
24
26
  const program = new Command();
25
27
 
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).");
28
+ // importing validators and allowed values
29
+ import { validateDueDate, ALLOWED_PRIORITIES, ALLOWED_STATUSES } from "./utils/validators.js";
30
+ // 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;
68
39
  }
69
- };
70
40
 
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, 12, 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
+ });
71
69
 
70
+ console.log(table.toString());
71
+ };
72
72
  // setting up
73
73
  program
74
- .name("todo-cli")
74
+ .name("taskninja")
75
75
  .description("A simple CLI application to manage your tasks")
76
- .version("1.0.0");
76
+ .version("1.1.0");
77
77
 
78
78
  // use command 'add' with title + status + priority + dueDate + description and action
79
79
  program
80
80
  .command('add')
81
81
  .alias('a')
82
82
  .description('Add a new task')
83
- .action(async (title, status, priority, dueDate, description = "") => {
83
+ .action(async () => {
84
84
  const answers = await inquirer.prompt([
85
85
  {
86
86
  type: 'input',
@@ -89,14 +89,14 @@ program
89
89
  validate : input => input ? true : 'Title cannot be empty!'
90
90
  },
91
91
  {
92
- type: 'list',
92
+ type: 'rawlist',
93
93
  name: 'status',
94
94
  message : 'Task Status:',
95
95
  choices : ALLOWED_STATUSES,
96
96
  default : 'todo'
97
97
  },
98
98
  {
99
- type: 'list',
99
+ type: 'rawlist',
100
100
  name: 'priority',
101
101
  message : 'Task Priority:',
102
102
  choices : ALLOWED_PRIORITIES,
@@ -119,7 +119,7 @@ program
119
119
  type: 'input',
120
120
  name: 'description',
121
121
  message : 'Task Description (optional):'
122
- }
122
+ },
123
123
  ]);
124
124
  // load existing tasks
125
125
  const tasks = await loadTasks();
@@ -137,7 +137,7 @@ program
137
137
  tasks.push(newTask);
138
138
  // save updated tasks to file
139
139
  await saveTasks(tasks);
140
- console.log("Task added successfully!");
140
+ console.log(chalk.green('Task added successfully!'));
141
141
  });
142
142
 
143
143
 
@@ -146,102 +146,332 @@ program
146
146
  .command('list')
147
147
  .alias('ls')
148
148
  .description('List all tasks')
149
- .option('-s, --status <status>', 'Filter tasks by status')
149
+ .option('-s, --status <status>', 'Filter tasks by status (todo, in-progress, done)')
150
150
  .action(async (options) => {
151
151
  let tasks = await loadTasks();
152
152
 
153
153
  if (options.status) {
154
154
  if (!ALLOWED_STATUSES.includes(options.status)){
155
- console.error(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`);
156
- process.exit(1);
155
+ console.log(chalk.red(`Invalid status filter. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`));
156
+ return;
157
157
  }
158
158
  tasks = tasks.filter(task => task.status === options.status);
159
159
  }
160
- if (tasks.length === 0) {
161
- console.log('No tasks found.');
160
+ // display all tasks in table format
161
+ displayTasks(tasks);
162
+ });
163
+
164
+
165
+
166
+ // use command 'search' to find tasks by keyword in title or description
167
+ program
168
+ .command('search [keyword]')
169
+ .alias('sr')
170
+ .option('-f, --find <keyword>', 'Keyword to search in title or description')
171
+ .description('Search tasks by keyword in title or description')
172
+ .action(async (keyword, options) => {
173
+ const tasks = await loadTasks();
174
+ let searchTerm = keyword || options.find;
175
+
176
+ if (!searchTerm) {
177
+ const answers = await inquirer.prompt([
178
+ {
179
+ type: 'input',
180
+ name: 'term',
181
+ message: 'Enter the keyword you want to search for:',
182
+ validate: input => input ? true : 'Keyword cannot be empty!'
183
+ },
184
+ {
185
+ type: 'rawlist',
186
+ name: 'field',
187
+ message: 'Where do you want to search?',
188
+ choices: ['title', 'description', 'both'],
189
+ default: 'both'
190
+ }
191
+ ]);
192
+ searchTerm = answers.term;
193
+ const field = answers.field;
194
+
195
+ const founded = tasks.filter(task => {
196
+ if (field === 'title') return task.title.toLowerCase().includes(searchTerm.toLowerCase());
197
+ if (field === 'description') return task.description.toLowerCase().includes(searchTerm.toLowerCase());
198
+ return task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
199
+ task.description.toLowerCase().includes(searchTerm.toLowerCase());
200
+ });
201
+
202
+ if (founded.length === 0) {
203
+ console.log(chalk.red('No task matched your search!'));
204
+ return;
205
+ }
206
+
207
+ displayTasks(founded);
162
208
  return;
163
209
  }
164
- // map all tasks to displayable objects
165
- const tableData = tasks.map(task => ({
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
210
 
174
- // display all tasks in table format
175
- console.table(tableData);
211
+ const founded = tasks.filter(task =>
212
+ task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
213
+ task.description.toLowerCase().includes(searchTerm.toLowerCase())
214
+ );
215
+
216
+ if (founded.length === 0) {
217
+ console.log(chalk.red('No task matched your search!'));
218
+ return;
219
+ }
220
+
221
+ displayTasks(founded);
222
+ });
223
+
224
+
225
+
226
+
227
+ // use command 'sort' to sort tasks by due date, priority, or status
228
+ program
229
+ .command('sort')
230
+ .alias('so')
231
+ .option('--by <criteria>', 'Sort tasks by criteria (dueDate, priority, status)')
232
+ .description('Sort tasks by due date, priority, or status')
233
+ .action( async (options) => {
234
+
235
+ const tasks = await loadTasks();
236
+ let criteria = options.by;
237
+
238
+ if (!criteria) {
239
+ const answer = await inquirer.prompt([
240
+ {
241
+ type: 'rawlist',
242
+ name: 'criteria',
243
+ message: 'Sort tasks by:',
244
+ choices: ['dueDate', 'priority', 'status']
245
+ }
246
+ ]);
247
+ criteria = answer.criteria;
248
+ }
249
+
250
+
251
+ if (!['dueDate', 'priority', 'status'].includes(criteria)) {
252
+ console.log(chalk.red('Invalid sort criteria. Use --by with dueDate, priority, or status.'));
253
+ return;
254
+ }
255
+ // sorting logic
256
+ const sortedTasks = [...tasks];
257
+
258
+ switch (criteria) {
259
+ case 'dueDate' || 'duedate' || 'DueDate' || 'Duedate' || 'due date' || 'Due date' || 'due-date':
260
+ sortedTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
261
+ break;
262
+ case 'priority' || 'Priority':
263
+ const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
264
+ sortedTasks.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
265
+ break;
266
+ case 'status' || 'Status':
267
+ const statusOrder = { 'todo': 1, 'in-progress': 2, 'done': 3 };
268
+ sortedTasks.sort((a, b) => statusOrder[a.priority] - statusOrder[b.priority]);
269
+ break;
270
+ default:
271
+ console.log(chalk.red('Invalid sort criteria. Use dueDate, priority, or status.'));
272
+ return;
273
+ }
274
+
275
+ displayTasks(sortedTasks);
176
276
  });
177
277
 
278
+
178
279
  // use command 'update' with task ID and action
179
280
  program
180
281
  .command('update')
181
282
  .alias('up')
182
- .description('Update a task by ID')
283
+ .description('Update a task by ==> ID <==')
183
284
  .action(async () =>{
184
285
  const tasks = await loadTasks();
185
286
  if (tasks.length === 0) {
186
- console.log('No tasks found to update.');
287
+ console.log(chalk.red('No tasks found to update.'));
187
288
  return;
188
289
  }
189
290
 
190
291
  const { id } = await inquirer.prompt([
191
292
  {
192
- type: 'list',
293
+ type: 'rawlist',
193
294
  name: 'id',
194
- message: 'Select the task to update:',
295
+ message: 'Select the task to update (By ID):',
195
296
  choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
196
297
  }
197
298
  ]);
198
- const { status } = await inquirer.prompt([
199
- {
200
- type: 'list',
201
- name: 'status',
202
- message: 'Select the new status:',
203
- choices: ALLOWED_STATUSES
204
- }
205
- ]);
206
299
 
300
+ // find the task to update
207
301
  const task = tasks.find(t => t.id === Number(id));
208
302
  if (!task) {
209
- console.error('Task not found!');
303
+ console.log(chalk.red('Task not found!'));
210
304
  return;
211
305
  }
212
- task.status = status;
213
306
 
214
- await saveTasks(tasks);
215
- console.log('Task updated successfully!');
307
+ const answers = await inquirer.prompt([
308
+ // for title
309
+ {
310
+ type: 'confirm',
311
+ name: 'changeTitle',
312
+ message: 'Do you want to change the title?',
313
+ default: false
314
+ },
315
+ {
316
+ type: 'input',
317
+ name: 'title',
318
+ message: 'Enter the new title:',
319
+ when: answers => answers.changeTitle,
320
+ validate: input => input ? true : 'Title cannot be empty!'
321
+ },
216
322
 
217
- const tableData = tasks.map(task => ({
218
- ID: task.id,
219
- Title: task.title,
220
- Status: task.status,
221
- Priority: task.priority,
222
- DueDate: task.dueDate,
223
- Description: task.description || ''
224
- }));
323
+ // for status
324
+ {
325
+ type: 'confirm',
326
+ name: 'changeStatus',
327
+ message: 'Do you want to change the status?',
328
+ default: false
329
+ },
330
+ {
331
+ type: 'rawlist',
332
+ name: 'status',
333
+ message: 'Select the new status:',
334
+ choices: ALLOWED_STATUSES,
335
+ when: answers => answers.changeStatus
336
+ },
225
337
 
226
- // display all tasks in table format
227
- console.table(tableData);
338
+ // for priority
339
+ {
340
+ type: 'confirm',
341
+ name: 'changePriority',
342
+ message: 'Do you want to change the priority?',
343
+ default: false
344
+ },
345
+ {
346
+ type: 'rawlist',
347
+ name: 'priority',
348
+ message: 'Select the new priority:',
349
+ choices: ALLOWED_PRIORITIES,
350
+ when: answers => answers.changePriority
351
+ },
352
+
353
+ // for due date
354
+ {
355
+ type: 'confirm',
356
+ name: 'changeDueDate',
357
+ message: 'Do you want to change the due date?',
358
+ default: false
359
+ },
360
+ {
361
+ type: 'input',
362
+ name: 'dueDate',
363
+ message: 'Enter the new due date (YYYY-MM-DD):',
364
+ when: answers => answers.changeDueDate,
365
+ validate: input => {
366
+ try {
367
+ validateDueDate(input);
368
+ return true;
369
+ } catch (error) {
370
+ return error.message;
371
+ }
372
+ }
373
+ },
374
+
375
+ // for description
376
+ {
377
+ type: 'confirm',
378
+ name: 'changeDescription',
379
+ message: 'Do you want to change the description?',
380
+ default: false
381
+ },
382
+ {
383
+ type: "rawlist",
384
+ name: 'description',
385
+ message: 'Enter the new description:',
386
+ when: answers => answers.changeDescription
387
+ }
388
+ ]);
389
+
390
+ // apply updates only if user chose to change them
391
+ if (answers.changeTitle) task.title = answers.title;
392
+ if (answers.changeStatus) task.status = answers.status;
393
+ if (answers.changePriority) task.priority = answers.priority;
394
+ if (answers.changeDueDate) task.dueDate = answers.dueDate;
395
+ if (answers.changeDescription)
396
+ task.description = answers.description || '';
397
+
398
+ // is there any change?
399
+ const hasChanges = [
400
+ answers.changeTitle,
401
+ answers.changeStatus,
402
+ answers.changePriority,
403
+ answers.changeDueDate,
404
+ answers.changeDescription
405
+ ].some(Boolean);
406
+
407
+ if (!hasChanges) {
408
+ console.log(chalk.yellow('No changes were made.'));
409
+ }else {
410
+ // save updated tasks to file
411
+ await saveTasks(tasks);
412
+ console.log(chalk.green('Task updated successfully!'));
413
+ }
414
+
415
+ displayTasks(tasks);
416
+ });
417
+
418
+
419
+ // use command 'done' with task ID instead of 'update' + confirm to mark task as done
420
+ program
421
+ .command('done')
422
+ .description('Mark a task as done by ==> ID <==')
423
+ .action(async () => {
424
+ const tasks = await loadTasks();
425
+ if (tasks.length === 0) {
426
+ console.log(chalk.magenta('Congratulations! All tasks are already done.'));
427
+ return;
428
+ }
429
+
430
+ const activeTasks = tasks.filter(t => t.status !== 'done');
431
+ if (activeTasks.length === 0) {
432
+ console.log(chalk.magenta('Congratulations! All tasks are already done.'));
433
+ return;
434
+ }
435
+
436
+ const { id } = await inquirer.prompt([
437
+ {
438
+ type: 'rawlist',
439
+ name: 'id',
440
+ message: 'Select the task to mark as done:',
441
+ choices: activeTasks.map(t => (
442
+ {
443
+ name: `${t.id}: ${t.title}[Current Status: ${t.status}]`,
444
+ value: t.id
445
+ }
446
+ ))
447
+ }
448
+ ]);
449
+
450
+ const task = tasks.find(t => t.id === Number(id));
451
+ task.status = 'done';
452
+ await saveTasks(tasks);
453
+ console.log(chalk.green('Task marked as done successfully!'));
454
+
455
+ displayTasks(tasks);
228
456
  });
229
457
 
458
+
459
+
230
460
  // use command 'delete' with task ID and action
231
461
  program
232
462
  .command('delete')
233
463
  .alias('del')
234
- .description('delete a task by ID')
464
+ .description('delete a task by ==> ID <==')
235
465
  .action(async () => {
236
466
  const tasks = await loadTasks();
237
467
  if (tasks.length === 0) {
238
- console.log('No tasks found to delete.');
468
+ console.log(chalk.yellow('No tasks found to delete.'));
239
469
  return;
240
470
  }
241
471
 
242
472
  const { id } = await inquirer.prompt([
243
473
  {
244
- type: 'list',
474
+ type: 'rawlist',
245
475
  name: 'id',
246
476
  message: 'Select the task to delete:',
247
477
  choices: tasks.map(task => ({ name: `${task.id}: ${task.title}`, value: task.id }))
@@ -257,26 +487,44 @@ program
257
487
  }
258
488
  ]);
259
489
  if (!confirm) {
260
- console.log('Task deletion cancelled.');
490
+ console.log(chalk.yellow('Task deletion cancelled.'));
261
491
  return;
262
492
  }
493
+ const taskToDelete = tasks.find(t => t.id === Number(id));
494
+ // save deleted task for undo functionality
495
+ await saveDeletedTask(taskToDelete);
496
+ // filter out the deleted task
497
+ const newTasks = tasks.filter(t => t.id !== Number(id));
263
498
 
264
- const newTasks = tasks.filter(t => t.id !== id);
265
499
  await saveTasks(newTasks);
266
- console.log('Task deleted successfully!');
267
-
268
- const tableData = tasks.map(task => ({
269
- ID: task.id,
270
- Title: task.title,
271
- Status: task.status,
272
- Priority: task.priority,
273
- DueDate: task.dueDate,
274
- Description: task.description || ''
275
- }));
500
+ console.log(chalk.green('Task deleted successfully!'));
501
+ console.log(chalk.cyan('You can undo this action by using the `undo` command.'));
276
502
 
277
- // display all tasks in table format
278
- console.table(tableData);
503
+ displayTasks(newTasks);
279
504
  });
280
505
 
506
+ // use command 'undo' to restore last deleted task
507
+ program
508
+ .command('undo')
509
+ .alias('un')
510
+ .description('Undo the last deleted task')
511
+ .action( async () => {
512
+ const lastDeletedTask = await loadDeletedTask();
513
+ if (!lastDeletedTask) {
514
+ console.log(chalk.yellow('No deleted task to restore.'));
515
+ return;
516
+ }
517
+
518
+ const tasks = await loadTasks();
519
+ tasks.push(lastDeletedTask);
520
+ tasks.sort((a, b) => a.id - b.id); // keep tasks sorted by ID
521
+ await saveTasks(tasks);
522
+ await clearDeletedTask();
523
+
524
+ console.log(chalk.green(`Last deleted task restored successfully!, (Task name: ${lastDeletedTask.title})`));
525
+ });
526
+
527
+
528
+
281
529
  // parse command line arguments
282
530
  program.parse(process.argv);
package/images/add.png ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "taskninja",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "a simple CLI application with JS as a (To-Do Application)",
5
5
  "main": "app.js",
6
- "type" : "module",
6
+ "type": "module",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
9
  "start": "node app.js",
10
10
  "dev": "node app.js"
11
11
  },
12
- "bin" : {
12
+ "bin": {
13
13
  "dotask": "./app.js",
14
14
  "taskninja": "./app.js",
15
15
  "tn": "./app.js"
@@ -17,6 +17,8 @@
17
17
  "author": "Moahmed Bakr",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
+ "chalk": "^5.6.2",
21
+ "cli-table3": "^0.6.5",
20
22
  "commander": "^14.0.2",
21
23
  "inquirer": "^13.2.1"
22
24
  }
@@ -0,0 +1,63 @@
1
+ // for file system operations
2
+ import fs from "fs/promises";
3
+
4
+
5
+ // file to store tasks
6
+ const TASKS_FILE = "./todos.json";
7
+
8
+ // file to temporarily store deleted tasks for undo functionality
9
+ export const DELETED_TASKS_FILE = "./deleted_todos.json";
10
+
11
+
12
+ /**
13
+ * @description this section responsible to store tasks in todo.json
14
+ */
15
+ // function to load tasks from file
16
+ export const loadTasks = async () => {
17
+ try{
18
+ const data = await fs.readFile(TASKS_FILE, 'utf8');
19
+ return JSON.parse(data);
20
+ } catch (error) {
21
+ if (error.code === 'ENOENT') return [];
22
+ throw error;
23
+ }
24
+ };
25
+ // function to save tasks to file
26
+ export const saveTasks = async (tasks) => {
27
+ await fs.writeFile(TASKS_FILE, JSON.stringify(tasks, null, 2));
28
+ };
29
+ // get next task ID
30
+ export const getNextId = (tasks) => {
31
+ return tasks.length > 0 ? Math.max(...tasks.map(task => task.id)) + 1 : 1;
32
+ };
33
+
34
+ /**
35
+ * @description this section responsible to store deleted tasks in deleted_todos.json
36
+ * for undo functionality
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
+
43
+ // to load deleted last-deleted task
44
+ export const loadDeletedTask = async () => {
45
+ try {
46
+ const data = await fs.readFile(DELETED_TASKS_FILE, 'utf8');
47
+ return JSON.parse(data);
48
+ } catch (error) {
49
+ if (error.code === 'ENOENT') return null;
50
+ throw error;
51
+ }
52
+ };
53
+
54
+ // to clear deleted tasks file after undo
55
+ export const clearDeletedTask = async () => {
56
+ try {
57
+ await fs.unlink(DELETED_TASKS_FILE);
58
+ } catch (error) {
59
+ if (error.code !== 'ENOENT') {
60
+ throw error;
61
+ }
62
+ }
63
+ };
@@ -0,0 +1,26 @@
1
+
2
+ // allowed task statuses
3
+ const ALLOWED_STATUSES = ["todo", "in-progress", "done"];
4
+ // allowed task priorities
5
+ const ALLOWED_PRIORITIES = ["low", "medium", "high"];
6
+
7
+ // validate task status
8
+ export const validateStatus = (status) => {
9
+ if (!ALLOWED_STATUSES.includes(status)) {
10
+ throw new Error(`Invalid status. Allowed statuses are: ${ALLOWED_STATUSES.join(", ")}`);
11
+ }
12
+ };
13
+ // validate task priority
14
+ export const validatePriority = (priority) => {
15
+ if (!ALLOWED_PRIORITIES.includes(priority)) {
16
+ throw new Error(`Invalid priority. Allowed priorities are: ${ALLOWED_PRIORITIES.join(", ")}`);
17
+ }
18
+ };
19
+ // verify due date format
20
+ export const validateDueDate = (dueDate) => {
21
+ if (isNaN(Date.parse(dueDate))) {
22
+ throw new Error("Invalid due date. Please use a valid date format (YYYY-MM-DD).");
23
+ }
24
+ };
25
+
26
+ export { ALLOWED_PRIORITIES, ALLOWED_STATUSES };