Build a File Organizer CLI with Node.js
Build a CLI tool that organizes files into folders by type using Node.js.
Introduction
30 Second Summary
Your Downloads folder is a mess. Screenshots mixed with PDFs, old zip files buried under music tracks. You know you should organize it, but dragging files into folders one by one is tedious.
In this project, you will build a command-line tool called tidyfiles that automatically sorts files into categorized subdirectories by their extension. You will also make it globally runnable on your system so you can use it from any directory.
What You'll Build
A CLI tool powered by Node.js that scans a target directory, categorizes files by extension, and moves them into organized subdirectories with colored output and a loading spinner.
By the end of this project, you'll have:
- A working CLI tool (tidyfiles) that organizes files into categorized subdirectories by extension, with a --dry-run flag to preview changes safely.
- A reusable module separating file categorization logic from the CLI interface.
- A globally runnable command registered on your system via npm link.
- Secret Mission: A custom rules system that lets users define their own extension-to-folder mappings via a config file.
Are there any prerequisites?
This project assumes intermediate JavaScript and Node.js experience. You should be comfortable with functions, arrays, objects, and basic terminal commands.
Set Up the Project
Before writing any code, you need a project directory with the right configuration. A Node.js project starts with a package.json file that defines its metadata, dependencies, and how it runs.
In this step, you'll create the project folder, configure it for ES Modules, and install the three packages that power your CLI's interface.
In this step, get ready to:
- Initialize the tidyfiles project with npm.
- Install Commander.js, chalk, and ora.
- Create the project's source files.
Create the project directory and configure package.json
Your CLI tool needs its own folder with a properly configured package.json that tells Node.js to use ES Module syntax and registers the command name tidyfiles.
- Open your terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and move into it by running these commands:
mkdir tidyfiles
cd tidyfiles
- Initialize the project by running:
npm init -y
What does npm init -y do?
This command creates a package.json file with default values. The -y flag skips all the interactive prompts and accepts the defaults.
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the tidyfiles folder, and click Open.
VS Code reloads with the tidyfiles project open. You should see package.json listed in the Explorer sidebar on the left.
The default package.json has extra fields you don't need. You'll replace its contents with a clean configuration.
- Click package.json in the Explorer sidebar to open it in the editor.
- Select all the existing content (Cmd+A on macOS or Ctrl+A on Windows) and replace it with this:
{
"name": "tidyfiles",
"version": "1.0.0",
"description": "A CLI tool to organize files in a directory by type",
"type": "module",
"bin": {
"tidyfiles": "./index.js"
}
}
What does this configuration do?
- "type": "module" tells Node.js to treat all .js files as ES Modules, so you can use import and export syntax.
- "bin" registers tidyfiles as a command name that points to ./index.js. This is what makes your tool runnable as a named command later.
- Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing a JSON syntax error?
Make sure every key and string value is wrapped in double quotes. JSON does not allow single quotes or trailing commas after the last item in an object.
help me fix a JSON syntax error in my package.json
Install the dependencies
Your CLI uses three packages: Commander.js for argument parsing, chalk for colored terminal output, and ora for loading spinners.
- Open the terminal in VS Code by pressing Ctrl+`.
- Install all three packages by running:
npm install commander@15.0.0 chalk@5.6.2 ora@9.4.0
Why pin exact versions?
Specifying @15.0.0 after each package name ensures everyone gets the same version. This prevents unexpected breaking changes from newer releases.
- Confirm the install succeeded by running:
ls node_modules
You should see folders named commander, chalk, and ora in the output.
Don't see the packages?
Make sure your terminal is inside the tidyfiles folder. Run pwd to check your current directory. If you're somewhere else, use cd ~/Desktop/tidyfiles to navigate back.
help me troubleshoot npm install failing in my tidyfiles project
You'll also notice that npm updated your package.json with a dependencies section listing the three packages.
Create the source files
Your project needs two JavaScript files. One handles the CLI interface, and the other holds the core file-organizing logic.
- Create the two empty source files by running:
touch index.js organizer.js
- Confirm the files exist by running:
ls
You should see index.js, organizer.js, package.json, and the node_modules folder in the output.
touch command not found on Windows?
The touch command works in Git Bash and VS Code's default terminal on Windows. If it doesn't work in your terminal, create the files manually by clicking the New File icon in the Explorer sidebar and naming them index.js and organizer.js.
help me create empty files in my tidyfiles project on Windows
✔️ Awesome, I've got everything!
Great. Make sure you've saved package.json and that both index.js and organizer.js appear in your Explorer sidebar.
ⓧ I'd like to double check the full code
Your package.json should look like this (npm may have added a caret ^ before version numbers, which is fine):
{
"name": "tidyfiles",
"version": "1.0.0",
"description": "A CLI tool to organize files in a directory by type",
"type": "module",
"bin": {
"tidyfiles": "./index.js"
},
"dependencies": {
"commander": "15.0.0",
"chalk": "5.6.2",
"ora": "9.4.0"
}
}
Your index.js and organizer.js files should be empty.
Your project is configured and ready to go. Next up, you'll build the file organizer module that handles scanning directories and categorizing files by extension.
Build the File Organizer Module
Your project structure is ready with all three dependencies installed. Now it's time to build the core logic that powers your CLI tool.
The organizer module handles everything related to files: scanning a directory, deciding which category each file belongs to, and moving files into sorted subdirectories. Keeping this logic in its own module (separate from the CLI interface) makes it reusable and easy to test.
In this step, get ready to:
- Define a category map linking file extensions to folder names.
- Build functions for scanning, categorizing, and moving files.
- Export everything from a standalone module separate from CLI concerns.
Define the file categories and categorization function
Every file your tool encounters needs to land in the right folder. You'll start by defining which extensions belong to which category, then write the function that performs the lookup.
- In VS Code, open organizer.js and add the imports and the CATEGORIES object by pasting this code:
import { readdir, mkdir, rename } from 'node:fs/promises';
import { join, extname } from 'node:path';
// Maps category names to their associated file extensions
export const CATEGORIES = {
Images: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'],
Documents: ['.pdf', '.doc', '.docx', '.txt', '.xls', '.xlsx', '.ppt', '.pptx', '.csv'],
Videos: ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm'],
Audio: ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma'],
Archives: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
Code: ['.js', '.ts', '.py', '.html', '.css', '.json', '.xml', '.md'],
};
What does this code do?
The two import lines pull in functions from Node.js built-in modules. node:fs/promises provides async file system operations (reading directories, creating folders, renaming/moving files). node:path provides helpers for working with file paths and extensions.
The CATEGORIES object is a lookup table. Each key is a folder name your tool will create, and each value is an array of extensions that belong in that folder.
You won't see a change yet. The next chunk uses this map to categorize files.
- Below the CATEGORIES object, add the categorizeFile function:
export function categorizeFile(filename) {
// Extract and normalize the file extension
const ext = extname(filename).toLowerCase();
// Search each category for a matching extension
for (const [category, extensions] of Object.entries(CATEGORIES)) {
if (extensions.includes(ext)) {
return category;
}
}
// Default if no category matches
return 'Other';
}
What does this code do?
The categorizeFile function takes a filename (like photo.jpg), extracts its extension using extname, and loops through every category to find a match.
If no category contains the extension, the function returns 'Other' as a fallback. This ensures every file gets placed somewhere.
- Save organizer.js.
- Verify the function works by running this command in your terminal:
node -e "import('./organizer.js').then(m => console.log(m.categorizeFile('photo.jpg')))"
You should see Images printed in the terminal.
Seeing an error instead of Images?
- Check that organizer.js is saved. Unsaved changes won't be picked up by Node.
- Make sure your package.json still has "type": "module". Without it, the export keyword causes a syntax error.
- Confirm you're running the command from inside the tidyfiles folder. The relative path ./organizer.js won't resolve from anywhere else.
Ask for help: Why am I getting an error when I try to import organizer.js with node -e?
Implement the directory scanner
Before you can categorize files, you need to know what files exist in the target directory. The scanner reads a directory and filters out subdirectories so you only work with actual files.
- In organizer.js, add the scanDirectory function below categorizeFile:
export async function scanDirectory(directory) {
// Read directory contents as Dirent objects (includes file type info)
const entries = await readdir(directory, { withFileTypes: true });
// Keep only files (skip subdirectories), return just the names
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
}
What does this code do?
The { withFileTypes: true } option tells readdir to return Dirent objects instead of plain strings. Each Dirent has a method called .isFile() that distinguishes files from subdirectories.
This is important because your tool should only move files. It should never accidentally try to move an existing subdirectory into a category folder.
- Save organizer.js.
- Test the scanner on your current project folder by running:
node -e "import('./organizer.js').then(m => m.scanDirectory('.').then(f => console.log(f)))"
You should see an array listing the files in your tidyfiles directory (like index.js, organizer.js, package.json). Subdirectories like node_modules should not appear.
Seeing an empty array or an error?
- If you see [], double check the path you passed. A period (.) means "current directory". Make sure your terminal is inside the tidyfiles folder.
- If you see a "not a function" error, make sure you added the export keyword before async function scanDirectory.
Ask for help: My scanDirectory function isn't returning the expected file list.
Implement the file organizer
This is the function that does the heavy lifting. It ties together categorization and file system operations to actually move files into their target folders.
- In organizer.js, add the organizeFiles function below scanDirectory:
export async function organizeFiles(directory, files, dryRun = false) {
const results = {};
for (const file of files) {
const category = categorizeFile(file);
const targetDir = join(directory, category);
const sourcePath = join(directory, file);
const targetPath = join(targetDir, file);
// Only move files if this is not a dry run
if (!dryRun) {
await mkdir(targetDir, { recursive: true });
await rename(sourcePath, targetPath);
}
// Log each file's destination regardless of dry run mode
console.log(` ${file} -> ${category}/`);
results[category] = (results[category] || 0) + 1;
}
return results;
}
What does this code do?
For each file, the function determines its category, builds the source and target paths, and (unless dryRun is true) creates the target folder with mkdir and moves the file with rename.
The { recursive: true } option means mkdir won't throw an error if the folder already exists. This is safer than checking first.
The dryRun parameter is the key safety feature. When enabled, the function logs what it would do without actually moving anything. The results object tracks how many files land in each category for the summary.
- Save organizer.js.
You won't test this function directly right now. It moves real files, so you'll create a test directory with sample files in Step 4.
Seeing a syntax error on save?
- Check that every opening brace { has a matching closing brace }. The function has nested for and if blocks.
- Make sure backticks (`) surround the template literal in the console.log line, not regular quotes.
Ask for help: I'm seeing a syntax error in my organizeFiles function.
✔️ Awesome, I've got everything!
Great. Double check you've saved organizer.js before moving on.
ⓧ I'd like to double check the full code
Here's the complete organizer.js file. Make sure yours matches exactly:
import { readdir, mkdir, rename } from 'node:fs/promises';
import { join, extname } from 'node:path';
export const CATEGORIES = {
Images: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'],
Documents: ['.pdf', '.doc', '.docx', '.txt', '.xls', '.xlsx', '.ppt', '.pptx', '.csv'],
Videos: ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm'],
Audio: ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma'],
Archives: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
Code: ['.js', '.ts', '.py', '.html', '.css', '.json', '.xml', '.md'],
};
export function categorizeFile(filename) {
const ext = extname(filename).toLowerCase();
for (const [category, extensions] of Object.entries(CATEGORIES)) {
if (extensions.includes(ext)) {
return category;
}
}
return 'Other';
}
export async function scanDirectory(directory) {
const entries = await readdir(directory, { withFileTypes: true });
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
}
export async function organizeFiles(directory, files, dryRun = false) {
const results = {};
for (const file of files) {
const category = categorizeFile(file);
const targetDir = join(directory, category);
const sourcePath = join(directory, file);
const targetPath = join(targetDir, file);
if (!dryRun) {
await mkdir(targetDir, { recursive: true });
await rename(sourcePath, targetPath);
}
console.log(` ${file} -> ${category}/`);
results[category] = (results[category] || 0) + 1;
}
return results;
}
Your organizer module is complete. Next up, you'll build the CLI interface that connects this logic to terminal commands with colored output and a loading spinner.
Create the CLI Interface
Your organizer module handles all the file-scanning and moving logic. Now you need a front door for users to interact with through the terminal.
In this step, you'll build the CLI entry point in index.js that wires together argument parsing, colored output, and loading spinners into a polished command-line experience.
In this step, get ready to:
- Set up the CLI entry point with a shebang line and imports.
- Define an organize command with spinner feedback and colored output.
- Define a list command and activate argument processing.
Add the shebang, imports, and program setup
Every CLI tool needs a special first line called a shebang that tells the operating system which interpreter to use. After that, you import all the libraries and set up the program's identity.
- In VS Code, open index.js and add the following code:
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import { scanDirectory, organizeFiles, CATEGORIES } from './organizer.js';
// Create and configure the CLI program
const program = new Command();
program
.name('tidyfiles')
.description('A CLI tool to organize files in a directory by type')
.version('1.0.0');
What does this code do?
The shebang #!/usr/bin/env node lets your script run directly as a command on macOS and Linux. On Windows, npm link creates a wrapper that handles this automatically.
The imports pull in Commander.js for argument parsing, chalk for colored text, ora for loading spinners, and your own organizer functions. The .name(), .description(), and .version() calls set the program identity that appears in help output.
- Save index.js.
- Test the setup by running this command in your terminal:
node index.js --help
You should see help output showing the program name tidyfiles and its description. No commands will be listed yet because you have not defined any.
Seeing a syntax error?
Make sure package.json has "type": "module". Without it, Node.js treats your file as CommonJS and the import statements will fail.
Also check that the shebang is on the very first line with no blank lines above it.
Help me fix a syntax error in my index.js CLI file
Define the organize command
The organize command is the core of your CLI. It accepts a directory path, scans it for files, and moves them into categorized subdirectories with real-time feedback.
- In index.js, add the following code below the .version('1.0.0') line:
program
.command('organize')
.description('Organize files in a directory into categorized subdirectories')
.argument('<directory>', 'target directory to organize')
.option('-d, --dry-run', 'preview changes without moving files', false)
.action(async (directory, options) => {
// Start a spinner while scanning the directory
const spinner = ora('Scanning directory...').start();
try {
const files = await scanDirectory(directory);
if (files.length === 0) {
spinner.info(chalk.yellow('No files found to organize.'));
return;
}
spinner.succeed(chalk.green(`Found ${files.length} file(s) to organize.`));
if (options.dryRun) {
console.log(chalk.yellow('\n--- Dry Run (no files will be moved) ---\n'));
} else {
console.log('');
}
// Organize files and collect results
const results = await organizeFiles(directory, files, options.dryRun);
// Print a colored summary
console.log(chalk.bold('\n--- Summary ---\n'));
for (const [category, count] of Object.entries(results)) {
if (count > 0) {
console.log(chalk.cyan(` ${category}: ${count} file(s)`));
}
}
const total = Object.values(results).reduce((sum, count) => sum + count, 0);
const action = options.dryRun ? 'would be organized' : 'organized';
console.log(chalk.green(`\n Total: ${total} file(s) ${action}.`));
} catch (error) {
spinner.fail(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
What does this code do?
- .argument('<directory>') makes the directory path a required argument. Commander.js will show an error if the user omits it.
- .option('-d, --dry-run') adds an optional flag that lets users preview changes safely without actually moving files.
- The ora spinner gives visual feedback during the scan. It switches to a success checkmark or info icon depending on the result.
- The try/catch block handles errors gracefully. If the directory does not exist, the spinner shows a red error message instead of an ugly stack trace.
Why async in the action handler?
The scanDirectory and organizeFiles functions both use await for file system operations. Marking the handler as async lets you use await inside it. This is also why the program uses parseAsync() instead of parse() at the end of the file.
- Save index.js.
You will not see a visible change yet. The next chunk adds the list command and activates parsing so you can test everything together.
Define the list command and activate parsing
The list command gives users a quick reference of which file extensions map to which categories. After defining it, you activate argument processing so Commander.js actually reads what the user typed.
- In index.js, add the following code below the organize command's closing });:
program
.command('list')
.description('List supported file categories and their extensions')
.action(() => {
console.log(chalk.bold('\nSupported Categories:\n'));
// Loop through each category and print its extensions
for (const [category, extensions] of Object.entries(CATEGORIES)) {
console.log(chalk.cyan(` ${category}:`));
console.log(` ${extensions.join(', ')}\n`);
}
});
// Activate argument processing (parseAsync for async action handlers)
program.parseAsync();
What does this code do?
- The list command iterates over the CATEGORIES object you imported from organizer.js and prints each category name in cyan with its extensions listed below.
- program.parseAsync() reads the command-line arguments and routes them to the matching command handler. It returns a Promise because your organize handler is async.
- Save index.js.
- Test the list command by running:
node index.js list
You should see all six categories (Images, Documents, Videos, Audio, Archives, Code) printed in cyan with their associated extensions listed below each one.
- Also verify the full help output by running:
node index.js --help
You should see both the organize and list commands listed under the help output.
Commands not showing in help?
Make sure program.parseAsync() is the very last line in the file. Without it, Commander.js never processes the arguments.
Check that both command definitions use program.command() (on the program instance), not Command() directly.
Help me debug why my CLI commands aren't appearing
✔️ Awesome, I've got everything!
Great. Double check you have saved index.js and that both commands show up when you run node index.js --help.
ⓧ I'd like to double check the full code
Here is the complete index.js file for comparison:
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import { scanDirectory, organizeFiles, CATEGORIES } from './organizer.js';
const program = new Command();
program
.name('tidyfiles')
.description('A CLI tool to organize files in a directory by type')
.version('1.0.0');
program
.command('organize')
.description('Organize files in a directory into categorized subdirectories')
.argument('<directory>', 'target directory to organize')
.option('-d, --dry-run', 'preview changes without moving files', false)
.action(async (directory, options) => {
const spinner = ora('Scanning directory...').start();
try {
const files = await scanDirectory(directory);
if (files.length === 0) {
spinner.info(chalk.yellow('No files found to organize.'));
return;
}
spinner.succeed(chalk.green(`Found ${files.length} file(s) to organize.`));
if (options.dryRun) {
console.log(chalk.yellow('\n--- Dry Run (no files will be moved) ---\n'));
} else {
console.log('');
}
const results = await organizeFiles(directory, files, options.dryRun);
console.log(chalk.bold('\n--- Summary ---\n'));
for (const [category, count] of Object.entries(results)) {
if (count > 0) {
console.log(chalk.cyan(` ${category}: ${count} file(s)`));
}
}
const total = Object.values(results).reduce((sum, count) => sum + count, 0);
const action = options.dryRun ? 'would be organized' : 'organized';
console.log(chalk.green(`\n Total: ${total} file(s) ${action}.`));
} catch (error) {
spinner.fail(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
program
.command('list')
.description('List supported file categories and their extensions')
.action(() => {
console.log(chalk.bold('\nSupported Categories:\n'));
for (const [category, extensions] of Object.entries(CATEGORIES)) {
console.log(chalk.cyan(` ${category}:`));
console.log(` ${extensions.join(', ')}\n`);
}
});
program.parseAsync();
Your CLI is fully wired up with two commands, colored output, and spinner feedback. Next up, you will test it on real files and make it globally runnable from any directory on your system.
Test and Go Global
Your CLI tool is fully built. The organizer module can scan, categorize, and move files. The Commander.js interface parses arguments and prints colored output.
Now it's time to prove it works on real files. You'll create a test directory, preview changes safely with --dry-run, run the actual organization, and then register tidyfiles as a global command available from anywhere on your system.
In this step, get ready to:
- Create sample test files and preview how they'd be organized with --dry-run.
- Run the organize command to sort files into categorized subdirectories.
- Register tidyfiles as a global command with npm link.
Create test files and preview with --dry-run
You need some real files for the CLI to organize. Empty files work perfectly since the tool categorizes by extension, not content.
- Create a test-files directory with sample files by running these commands:
macOS
mkdir test-files
touch test-files/photo.jpg test-files/image.png test-files/document.pdf test-files/song.mp3 test-files/archive.zip
Windows
mkdir test-files
New-Item -Path test-files/photo.jpg, test-files/image.png, test-files/document.pdf, test-files/song.mp3, test-files/archive.zip -ItemType File
- Confirm the files exist by running:
ls test-files
You should see photo.jpg, image.png, document.pdf, song.mp3, and archive.zip listed in the output.
Don't see the files?
Make sure your terminal is in the tidyfiles project folder. Run pwd (macOS/Linux) or cd (Windows) to check your current directory.
help me figure out why my test files weren't created
Now test the CLI without actually moving anything. The --dry-run flag lets you preview what would happen.
- Preview the file organization by running:
node index.js organize ./test-files --dry-run
You should see a yellow "Dry Run" banner, each file mapped to its category, and a summary showing 5 files that would be organized.
What does --dry-run show you?
The output lists each file with an arrow pointing to its target category folder. For example, photo.jpg -> Images/. The summary at the bottom counts files per category.
Because --dry-run is active, no files are moved and no folders are created. This is a safety mechanism you'd find in professional CLI tools.
Seeing an error instead of the preview?
If you see an error mentioning the directory not being found, double check the path. Make sure test-files is a directory inside your tidyfiles project folder.
help me debug why my tidyfiles organize command is failing
Run the organize command for real
The dry run confirmed everything looks correct. Now run the command without the flag to actually move the files into categorized subdirectories.
- Organize the test files for real by running:
node index.js organize ./test-files
You should see the same file-to-category mapping, but this time the summary says "organized" instead of "would be organized".
- Verify that subdirectories were created by running:
ls test-files
You should see Images, Documents, Audio, and Archives listed as subdirectories.
- Confirm the files landed in the right places by checking one subdirectory:
ls test-files/Images
You should see photo.jpg and image.png inside the Images folder.
Files didn't move?
If you still see the original files in test-files instead of subdirectories, make sure you ran the command without the --dry-run flag.
If you see a permission error, check that your user has write access to the tidyfiles folder.
help me figure out why my files weren't moved into subdirectories
Make tidyfiles a global command
Running node index.js works, but a real CLI tool should be available from any directory without specifying the full path. The npm link command registers your package globally using the bin field from package.json.
- Register tidyfiles as a global command by running:
npm link
What does npm link do?
It creates a symlink (macOS/Linux) or directory junction (Windows) from your global node_modules to your project folder. The bin entry in package.json tells npm to expose ./index.js under the command name tidyfiles.
After linking, you can type tidyfiles in any terminal window and it will run your index.js file.
Getting a permission error?
On macOS/Linux, if your global node_modules directory is owned by root, you may need to prefix with sudo. A better long-term fix is to configure npm to use a user-owned directory.
On Windows, close and reopen your terminal if the command isn't recognized immediately after linking.
help me fix permission issues with npm link
- Verify that tidyfiles is now globally available by running:
tidyfiles --version
You should see 1.0.0 printed. This confirms the global command resolves to your project.
- Test the tool from any location by previewing a real directory with --dry-run:
macOS
tidyfiles organize ~/Downloads --dry-run
Windows
tidyfiles organize $HOME\Downloads --dry-run
You should see a preview of how files in your Downloads folder would be categorized. No files are moved because of --dry-run.
Why use --dry-run on real folders?
Your Downloads folder likely has files you care about. Always preview with --dry-run before organizing a real directory. This is a pattern you'll see in professional tools like rsync and terraform plan.
tidyfiles command not found?
Try closing and reopening your terminal. Some shells need a fresh session to pick up new global commands.
If it still doesn't work, run npm prefix -g and verify the output path is in your system's PATH environment variable.
help me fix tidyfiles not being recognized as a global command
Secret mission
Add Custom Rules via Config File
Your tidyfiles tool works great with the built-in categories, but what about file types it doesn't recognize? In this extension, you'll add a custom rules system that lets users define their own extension-to-folder mappings via a .tidyfilesrc.json config file.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The tidyfiles project folder on your Desktop.
- A globally linked tidyfiles command (via npm link).
- The test-files directory with sample files and .tidyfilesrc.json.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep tidyfiles available as a global command.
- Your tidyfiles command stays available globally in any terminal.
- The project folder and all source files remain on your Desktop.
Pause - I'll come back to this later
Since this is a local project with no running processes, there's nothing to pause. Your files and the global link will stay in place until you return.
- Close VS Code to free up memory.
- The globally linked tidyfiles command will remain available whenever you open a new terminal.
Delete - I don't want to use this again
Remove all project resources and start fresh. This will unlink the global command and delete all project files.
- Remove the global tidyfiles command by running this from inside the tidyfiles project folder:
- Delete the entire project folder from your Desktop.
npm uninstall -g tidyfiles
Then navigate to your Desktop and remove the project folder:
cd ~/Desktop
rm -rf tidyfiles
Windows users
If you're on Windows, use this command instead to delete the folder:
cd ~\Desktop
rmdir /s /q tidyfiles
Nice Work!
Nice Work!
Nice work! You just built a complete, globally-runnable CLI tool that organizes messy directories by file type.
You've learned how to:
- Built a complete CLI tool (tidyfiles) with Commander.js for argument parsing, including subcommands, options, and auto-generated help output.
- Created a reusable file categorization module with clean separation of concerns, and implemented safe file operations with a --dry-run preview mode.
- Added polished terminal UX with chalk colors and ora spinners, then made the tool globally available via npm link.
- Secret Mission: Added a custom rules system via .tidyfilesrc.json config files, letting users define their own extension-to-folder mappings.
Ready to quiz yourself?