Have you ever wanted to build your own command-line tools? Those handy utilities that let you interact with your computer using text-based commands? They’re everywhere, from Git for version control to tools for managing your projects. Creating your own CLI can seem daunting, but with TypeScript, it’s surprisingly accessible, even for beginners. This tutorial will guide you step-by-step through building a simple, interactive CLI using TypeScript. We’ll cover everything from setting up your project to handling user input and displaying output.
Why Build a CLI?
Command-line interfaces offer several advantages:
- Automation: Automate repetitive tasks with scripts.
- Efficiency: Quickly perform actions without a graphical interface.
- Cross-Platform: Run on various operating systems.
- Learning: Understand how software interacts with the operating system.
Building a CLI is an excellent way to learn about system interaction, scripting, and software design principles. Plus, you can create custom tools tailored to your specific needs.
What We’ll Build
In this tutorial, we’ll create a simple CLI that allows users to manage a list of tasks. The CLI will provide functionalities to:
- Add tasks
- View tasks
- Mark tasks as complete
- Remove tasks
This will give you a solid foundation for building more complex CLIs.
Prerequisites
Before we start, make sure you have the following installed:
- Node.js and npm: (Node Package Manager) – Used to run JavaScript code and manage project dependencies. Download from nodejs.org.
- TypeScript: Globally installed (we’ll cover this).
- A code editor: (VS Code, Sublime Text, etc.)
Setting Up Your Project
Let’s get started!
- Create a project directory: Open your terminal or command prompt and navigate to the location where you want to create your project. Then, create a new directory for your CLI and change into it:
mkdir task-cli
cd task-cli
- Initialize a Node.js project: Run the following command to create a
package.jsonfile. This file will store your project’s metadata and dependencies. You can accept the defaults by pressing Enter for each prompt, or customize as you see fit.
npm init -y
- Install TypeScript: Install TypeScript and the TypeScript compiler as development dependencies:
npm install --save-dev typescript @types/node
The @types/node package provides type definitions for Node.js built-in modules, which is essential for working with Node.js in TypeScript.
- Create a TypeScript configuration file: Generate a
tsconfig.jsonfile. This file tells the TypeScript compiler how to compile your TypeScript code. Run the following command in your terminal:
npx tsc --init
This command creates a tsconfig.json file with default settings. You can customize this file to control how your TypeScript code is compiled. We will adjust some settings.
- Configure
tsconfig.json: Open thetsconfig.jsonfile in your code editor and modify the following settings (you can find these settings by searching in the file and uncommenting them and changing the values):
{
"compilerOptions": {
"target": "es2016", // or a later version like es2018 or es2020
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
target: Specifies the JavaScript version to compile to.module: Specifies the module system to use (CommonJS is suitable for Node.js).outDir: Specifies the output directory for the compiled JavaScript files (we’ll use “dist”).rootDir: Specifies the root directory of your TypeScript source files (we’ll use “src”).strict: Enables strict type-checking.esModuleInterop: Enables interoperability between CommonJS and ES modules.skipLibCheck: Skips type checking of declaration files.forceConsistentCasingInFileNames: Enforces consistent casing in filenames.include: Specifies the files and directories to include in the compilation.
- Create project folders: Create two folders in your project directory:
src: This will contain your TypeScript source code.dist: This will contain the compiled JavaScript files.
mkdir src dist
Writing the CLI Code
Now, let’s write the TypeScript code for our CLI. We’ll start with a simple “Hello, World!” example and then build on that.
- Create the main file: Inside the
srcdirectory, create a file namedindex.ts. This will be the entry point for your CLI.
- Write the “Hello, World!” code: Open
src/index.tsand add the following code:
console.log("Hello, World!");
- Compile the code: In your terminal, run the following command to compile your TypeScript code into JavaScript:
npx tsc
This command will read your tsconfig.json file and compile the TypeScript files in the src directory into JavaScript files in the dist directory.
- Run the compiled code: To run the compiled JavaScript code, use Node.js. You’ll need to specify the path to the compiled file. Add a script to your
package.jsonfile to make this easier. Openpackage.jsonand add the following to the “scripts” section:
"scripts": {
"start": "node dist/index.js"
},
Now, run the CLI using the following command:
npm start
You should see “Hello, World!” printed in your terminal.
Adding User Input
Now, let’s make our CLI interactive by adding the ability to accept user input. We’ll use the readline module provided by Node.js.
- Import the
readlinemodule: Insrc/index.ts, import thereadlinemodule.
import * as readline from 'readline';
- Create a
readlineinterface: Create areadlineinterface to read input from the console and write output to the console.
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
This code creates a readline interface that reads input from the standard input (process.stdin) and writes output to the standard output (process.stdout).
- Ask for user input: Use the
rl.question()method to ask the user a question and get their input.
rl.question('What is your name? ', (name) => {
console.log(`Hello, ${name}!`);
rl.close(); // Close the interface after getting the input
});
This code prompts the user to enter their name and then prints a greeting. The callback function is executed after the user enters their name and presses Enter. The rl.close() method closes the readline interface.
- Compile and run: Compile the TypeScript code using
npx tscand run it usingnpm start. You should be prompted to enter your name, and then you’ll see a personalized greeting.
Implementing Task Management
Now, let’s build the core functionality of our CLI: task management. We’ll implement the following features:
- Adding tasks
- Viewing tasks
- Marking tasks as complete
- Removing tasks
- Define a
Taskinterface: Create an interface to represent a task. This will help with type safety and code organization.
interface Task {
id: number;
text: string;
completed: boolean;
}
- Initialize an array to store tasks: Create an array to store the tasks.
let tasks: Task[] = [];
let nextTaskId = 1;
- Implement the
addTaskfunction: This function adds a new task to thetasksarray.
function addTask(text: string): void {
const newTask: Task = {
id: nextTaskId,
text: text,
completed: false,
};
tasks.push(newTask);
nextTaskId++;
console.log("Task added!");
}
- Implement the
viewTasksfunction: This function displays the current tasks.
function viewTasks(): void {
if (tasks.length === 0) {
console.log("No tasks yet.");
return;
}
console.log("Tasks:");
tasks.forEach((task) => {
const status = task.completed ? '[x]' : '[ ]';
console.log(`${task.id}. ${status} ${task.text}`);
});
}
- Implement the
markTaskCompletefunction: This function marks a task as complete based on its ID.
function markTaskComplete(taskId: number): void {
const taskIndex = tasks.findIndex((task) => task.id === taskId);
if (taskIndex === -1) {
console.log("Task not found.");
return;
}
tasks[taskIndex].completed = true;
console.log("Task marked as complete!");
}
- Implement the
removeTaskfunction: This function removes a task based on its ID.
function removeTask(taskId: number): void {
tasks = tasks.filter((task) => task.id !== taskId);
console.log("Task removed!");
}
- Create a function to handle user input: This function will handle the user’s commands.
function handleInput(input: string): void {
const [command, ...args] = input.split(' ');
switch (command) {
case 'add':
addTask(args.join(' '));
break;
case 'view':
viewTasks();
break;
case 'complete':
const completeTaskId = parseInt(args[0], 10);
if (!isNaN(completeTaskId)) {
markTaskComplete(completeTaskId);
}
break;
case 'remove':
const removeTaskId = parseInt(args[0], 10);
if (!isNaN(removeTaskId)) {
removeTask(removeTaskId);
}
break;
case 'exit':
rl.close();
return;
default:
console.log('Invalid command.');
}
promptForInput();
}
- Create a function to prompt for input: This function will display the prompt and handle user input.
function promptForInput(): void {
rl.question('> ', (input) => {
handleInput(input);
});
}
- Call the prompt function to start the CLI: Call
promptForInput()to start the CLI.
promptForInput();
Your complete src/index.ts file should now look like this:
import * as readline from 'readline';
interface Task {
id: number;
text: string;
completed: boolean;
}
let tasks: Task[] = [];
let nextTaskId = 1;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function addTask(text: string): void {
const newTask: Task = {
id: nextTaskId,
text: text,
completed: false,
};
tasks.push(newTask);
nextTaskId++;
console.log("Task added!");
}
function viewTasks(): void {
if (tasks.length === 0) {
console.log("No tasks yet.");
return;
}
console.log("Tasks:");
tasks.forEach((task) => {
const status = task.completed ? '[x]' : '[ ]';
console.log(`${task.id}. ${status} ${task.text}`);
});
}
function markTaskComplete(taskId: number): void {
const taskIndex = tasks.findIndex((task) => task.id === taskId);
if (taskIndex === -1) {
console.log("Task not found.");
return;
}
tasks[taskIndex].completed = true;
console.log("Task marked as complete!");
}
function removeTask(taskId: number): void {
tasks = tasks.filter((task) => task.id !== taskId);
console.log("Task removed!");
}
function handleInput(input: string): void {
const [command, ...args] = input.split(' ');
switch (command) {
case 'add':
addTask(args.join(' '));
break;
case 'view':
viewTasks();
break;
case 'complete':
const completeTaskId = parseInt(args[0], 10);
if (!isNaN(completeTaskId)) {
markTaskComplete(completeTaskId);
}
break;
case 'remove':
const removeTaskId = parseInt(args[0], 10);
if (!isNaN(removeTaskId)) {
removeTask(removeTaskId);
}
break;
case 'exit':
rl.close();
return;
default:
console.log('Invalid command.');
}
promptForInput();
}
function promptForInput(): void {
rl.question('> ', (input) => {
handleInput(input);
});
}
promptForInput();
- Compile and test: Compile the code using
npx tscand run it usingnpm start. Test the following commands:
add <task description>: Adds a new task.view: Displays the list of tasks.complete <task id>: Marks a task as complete.remove <task id>: Removes a task.exit: Exits the CLI.
Common Mistakes and How to Fix Them
Here are some common mistakes and how to fix them:
- Incorrect file paths: Double-check your file paths in the
tsconfig.jsonfile and when running your code. - Typos: Carefully check your code for typos, especially in variable names and function names. TypeScript can help catch some of these, but not all.
- Incorrect module imports: Make sure you are importing the correct modules. The most common mistake is forgetting to import a module or importing it incorrectly.
- Uninitialized variables: Always initialize variables before using them, especially when working with TypeScript’s strict mode.
- Incorrect data types: Ensure that you are using the correct data types for your variables and function parameters. TypeScript will help you catch these errors during compilation.
- Not handling user input properly: Validate user input to prevent unexpected behavior. For example, check that task IDs are valid numbers before attempting to complete or remove tasks. Use try-catch blocks to handle potential errors.
- Forgetting to close the readline interface: Always close the
readlineinterface usingrl.close()when you’re done with it to prevent your CLI from hanging.
Adding Features and Improvements
Once you have a working CLI, you can expand its functionality. Here are some ideas for improvements:
- Persisting data: Save tasks to a file (e.g., JSON file) or a database so they are not lost when the CLI is closed.
- Error handling: Implement more robust error handling to handle invalid user input and unexpected situations.
- More commands: Add more commands, such as editing tasks, sorting tasks, or filtering tasks.
- Color-coding: Use color-coding in the terminal output to make the CLI more visually appealing. You can use libraries like
chalkfor this. - Help messages: Implement a help command to display information about the available commands and their usage.
- Autocompletion: Implement autocompletion to make the CLI easier to use.
- Testing: Write unit tests to ensure your CLI works correctly.
Key Takeaways
- TypeScript makes building CLIs more manageable with its type safety and code organization.
- The
readlinemodule allows you to interact with the user via the command line. - Break down the problem into smaller, manageable functions.
- Always validate user input.
- Start simple and gradually add more features.
FAQ
- Can I use this CLI on different operating systems?
Yes, Node.js and TypeScript are cross-platform, so your CLI should work on Windows, macOS, and Linux. - How do I install this CLI globally?
You can install your CLI globally using npm. First, add a “bin” entry in yourpackage.jsonfile. For example:
"bin": {
"task-cli": "dist/index.js"
},
Then, run npm install -g . from your project directory. This will install your CLI globally, and you can run it by typing task-cli in your terminal.
- How can I add color to the output?
You can use libraries likechalkto add color to your terminal output. Install it usingnpm install chalk. - Where can I learn more about TypeScript?
The official TypeScript documentation is an excellent resource: typescriptlang.org/docs/. You can also find many tutorials and courses online. - How do I handle arguments passed to the CLI?
You can access command-line arguments using theprocess.argvarray. The first two elements of the array are typically the Node.js executable path and the script path. Arguments passed to the CLI start from the third element.
Building a CLI with TypeScript provides a practical and rewarding learning experience. By following this tutorial, you’ve created a functional task management CLI. The skills you’ve gained in understanding user input, managing data, and designing command-line interactions are valuable for any software developer. As you continue to build upon this foundation, you’ll discover new ways to streamline your workflow and create tools that enhance your productivity, and that is where the true power of custom-built software lies.
