Build a File Organizer CLI with Gemini
Use Gemini CLI prompts to build a Java file organizer tool with picocli.
Introduction
30 Second Summary
Everyone has a Downloads folder that turns into digital chaos. Files pile up with no organization, and sorting them manually feels like a chore you never get around to.
In this project, you will use Gemini CLI to build a Java command-line tool that organizes messy directories by sorting files into categorized subfolders. You will generate the entire tool through structured AI prompts, package it into an executable file, and set it up so you can run it from anywhere on your system.
What You'll Build
A command-line file organizer called tidyfiles that scans a target directory, categorizes files by extension (images, documents, code, audio, video, archives, misc), and moves them into organized subfolders.
By the end of this project, you'll have:
- A working Maven project generated entirely through AI-assisted prompts with Gemini CLI.
- A professional CLI tool with --help, --dry-run, --verbose, and --undo flags powered by picocli.
- An executable uber-JAR you can run from anywhere on your system using a shell alias.
- Secret Mission: Add a custom JSON configuration file so users can define their own file-type-to-folder mappings.
Are there any prerequisites?
This project assumes you already have Gemini CLI installed and authenticated, JDK 21, and Maven set up on your machine. If you need help with any of these, check the official installation guides before starting.
Scaffold the Maven Project with Gemini CLI
Every Java project needs a foundation before you can write code. That foundation is a Maven project with a pom.xml file that declares your dependencies, compiler version, and build plugins.
Instead of writing this configuration by hand, you will use Gemini CLI to generate the entire project scaffold from a single structured prompt. This is the core pattern of AI-assisted development: describe what you want precisely, then review and verify what the AI produces.
In this step, get ready to:
- Prompt Gemini CLI to generate a complete Maven project structure.
- Verify the project compiles with mvn compile.
Use Gemini CLI to generate the Maven project structure
A good prompt for code generation is structured and specific. You will tell Gemini exactly which dependencies, versions, and plugins you need so it produces a working pom.xml on the first pass.
- Start Gemini CLI in your ~/tidyfiles directory by running this command:
gemini
Gemini CLI opens an interactive session in your terminal. You can now type prompts and Gemini will generate files directly in your project directory.
- Paste the following structured prompt into the Gemini CLI session:
Create a Maven project in this directory with the following specifications:
1. pom.xml with:
- groupId: com.tidyfiles
- artifactId: tidyfiles
- version: 1.0-SNAPSHOT
- packaging: jar
- properties: project.build.sourceEncoding UTF-8, maven.compiler.release 21
2. Dependencies:
- info.picocli:picocli:4.7.7
3. Build plugins:
- maven-compiler-plugin 3.13.0 with release 21
- maven-shade-plugin 3.6.2 configured to create an executable JAR with mainClass com.tidyfiles.TidyFiles using ManifestResourceTransformer
4. Create the source directory: src/main/java/com/tidyfiles/
Do NOT create any Java source files yet. Only create the pom.xml and directory structure.
What makes this a good prompt?
This prompt is structured with numbered sections and explicit version numbers. It tells Gemini exactly what to produce and what NOT to produce.
Vague prompts like "set up a Java project" leave too many decisions to the AI. Specific prompts produce predictable, correct output on the first attempt.
- Gemini will ask for permission to create files. Type y to approve when prompted.
- Wait for Gemini to finish generating the files.
Gemini may create extra files
AI output can vary between runs. Gemini might create a sample Java file or a .gitignore even though you asked it not to. That is fine. The important thing is that pom.xml matches the specifications.
- Review the generated pom.xml by asking Gemini to show it:
Show me the contents of pom.xml
- Confirm that pom.xml contains the picocli dependency with version 4.7.7, the maven-shade-plugin with version 3.6.2, and maven.compiler.release set to 21.
What should I look for in the pom.xml?
- The <groupId> should be com.tidyfiles and <artifactId> should be tidyfiles.
- The <dependencies> section should list info.picocli:picocli:4.7.7.
- The <build> section should have both maven-compiler-plugin and maven-shade-plugin configured.
- The shade plugin's <mainClass> should be com.tidyfiles.TidyFiles.
Gemini generated something different?
If the generated pom.xml is missing a dependency or has a different version, ask Gemini to fix it directly. For example: "Update the picocli version in pom.xml to 4.7.7" or "Add the maven-shade-plugin with version 3.6.2".
Iterating on AI output is a normal part of the workflow. You are the reviewer who ensures correctness.
Help me fix my pom.xml to match the specifications
Verify the project compiles
A pom.xml can look correct but still have subtle errors. The real test is whether Maven can parse and compile it successfully.
- Exit the Gemini CLI session by typing:
/quit
- Compile the project to verify the pom.xml is valid by running:
mvn compile
- You should see BUILD SUCCESS in the terminal output. This confirms Maven downloaded the picocli dependency and the project structure is valid.
What does mvn compile do?
The mvn compile command reads your pom.xml, downloads any declared dependencies from Maven Central, and compiles all Java source files in src/main/java/. Since there are no Java files yet, a successful build means the project configuration is correct.
- Verify the directory structure exists by running:
ls src/main/java/com/tidyfiles/
- The directory should exist (even if empty). If you see an error, create it manually by running:
mkdir -p src/main/java/com/tidyfiles/
Seeing BUILD FAILURE?
Open pom.xml in a text editor and check for XML syntax errors like unclosed tags or mismatched brackets. Maven's error message will tell you the line number where parsing failed.
If the error mentions a dependency that could not be resolved, check your internet connection. Maven needs to download picocli from Maven Central on the first compile.
Help me debug my Maven BUILD FAILURE
✔️ Awesome, I've got everything!
Great. Double check you see BUILD SUCCESS and the src/main/java/com/tidyfiles/ directory exists.
ⓧ I'd like to double check the full code
Your pom.xml should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tidyfiles</groupId>
<artifactId>tidyfiles</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.tidyfiles.TidyFiles</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
If your file does not match, you can replace its contents entirely. Open pom.xml in a text editor, delete everything, and paste the code above.
Your project foundation is in place. Next up, you will prompt Gemini CLI to generate the Java classes that power the file organizer logic.
Generate the Core File Organizer Logic
Your Maven project skeleton compiles, but it has no actual Java logic yet. In this step, you'll use structured prompts in Gemini CLI to generate four Java classes that form the core of your file organizer tool.
The approach is intentional: you'll give Gemini CLI a detailed, structured description of what each class needs to do, and it will generate production-quality Java code matching your specification. This is the power of structured prompting.
In this step, get ready to:
- Generate the main TidyFiles entry point class with picocli annotations for CLI argument parsing.
- Generate the FileCategory enum, FileOrganizer class, and UndoManager class.
- Verify all generated code compiles together.
Prompt Gemini CLI to generate the main class
Every CLI tool needs an entry point that parses arguments and routes them to the right logic. Your TidyFiles class will use picocli annotations to declare what arguments and flags the tool accepts.
- Switch back to your terminal where Gemini CLI is running in the ~/tidyfiles directory.
- Enter this prompt to have Gemini generate the main class:
Create the file src/main/java/com/tidyfiles/TidyFiles.java with the following requirements:
- Package: com.tidyfiles
- Implements Callable<Integer>
- Uses picocli @Command annotation with name "tidyfiles", mixinStandardHelpOptions = true, version "tidyfiles 1.0", description "Organizes files in a directory into categorized subfolders."
- @Parameters(index = "0") for a File targetDirectory with defaultValue "."
- @Option -d/--dry-run: boolean, shows what would be done without moving files
- @Option -v/--verbose: boolean, prints detailed output for each file moved
- @Option -u/--undo: boolean, undoes the last organization operation
- main() method creates a CommandLine with new TidyFiles() and calls execute(args), then System.exit with the result
- call() method: validates targetDirectory exists and is a directory (print red error and return 1 if not), if undo flag is set delegates to UndoManager, otherwise creates FileOrganizer and calls organize()
Use ANSI color codes for error messages (red = \u001B[31m, reset = \u001B[0m).
What does this prompt do?
This structured prompt tells Gemini exactly what class to create, what annotations to use, and how the logic should flow. By specifying the exact annotation values, flag names, and behavior, you minimize the back-and-forth iteration needed.
The mixinStandardHelpOptions = true part tells picocli to automatically add --help and --version flags without you writing them manually.
- Gemini CLI will ask for permission to create the file. Accept the file creation.
- Review the generated TidyFiles.java to confirm it has the @Command annotation, three @Option fields, and a call() method.
Gemini's output may vary
Gemini CLI may generate code with slightly different formatting or variable names than the reference. The important thing is that the structure matches: a Callable<Integer> class with the correct annotations and flags.
Gemini didn't create the file?
If Gemini explains the code but doesn't create the file, rephrase your prompt to be explicit: "Create the file at src/main/java/com/tidyfiles/TidyFiles.java" and emphasize you want it written to disk.
help me get Gemini CLI to write files to disk instead of just showing code
Generate the file categorization and organizer logic
The main class delegates to helper classes that do the real work. You need three more files: an enum that maps file extensions to category folders, a class that performs the scanning and moving, and an undo manager that can reverse the operation.
- Enter this prompt in Gemini CLI to generate the remaining three classes:
Create the following three files in src/main/java/com/tidyfiles/:
1. FileCategory.java - an enum with these values:
- IMAGES("images", "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "ico", "tiff")
- DOCUMENTS("documents", "pdf", "doc", "docx", "txt", "rtf", "odt", "xls", "xlsx", "ppt", "pptx", "csv")
- CODE("code", "java", "py", "js", "ts", "html", "css", "cpp", "c", "h", "go", "rs", "rb", "php", "sh", "sql")
- AUDIO("audio", "mp3", "wav", "flac", "aac", "ogg", "wma", "m4a")
- VIDEO("video", "mp4", "avi", "mkv", "mov", "wmv", "flv", "webm")
- ARCHIVES("archives", "zip", "tar", "gz", "rar", "7z", "bz2", "xz", "jar")
- MISC("misc")
Each value has a folderName and extensions array. Include a static HashMap EXTENSION_MAP that maps each extension to its category, and a static fromExtension(String) method returning the category (defaulting to MISC).
2. FileOrganizer.java - a class with:
- Constructor taking File targetDirectory, boolean dryRun, boolean verbose
- A List<String> moveLog field
- organize() method that: lists files in directory, skips non-files, skips hidden files (with verbose message), gets extension, looks up FileCategory, creates category subfolder, moves file (or prints dry-run message), tracks moves in moveLog, saves log via UndoManager at end, prints summary with ANSI colors
- moveFile() helper: creates directory if needed, checks for symlinks (skip them), uses Files.move with REPLACE_EXISTING
- getExtension() helper: returns substring after last dot
3. UndoManager.java - a class with:
- LOG_FILENAME = ".tidyfiles.log"
- Constructor taking File targetDirectory, creates logFile path
- saveLog(List<String> entries): writes entries to log file
- undo(boolean verbose): reads log, splits each line on "|", moves files back from category folders to root, deletes log file, prints summary
Use ANSI colors: green=\u001B[32m, red=\u001B[31m, cyan=\u001B[36m, yellow=\u001B[33m, reset=\u001B[0m.
Why three files in one prompt?
These three classes depend on each other. FileOrganizer references both FileCategory and UndoManager. Generating them together in one prompt lets Gemini see the full picture and produce consistent, compatible code.
- Accept the file creation prompts from Gemini CLI for each of the three files.
- Verify all three files exist by running:
ls src/main/java/com/tidyfiles/
- You should see FileCategory.java, FileOrganizer.java, TidyFiles.java, and UndoManager.java listed.
Missing one of the files?
If Gemini only created some files, prompt it again for the specific missing one. For example: "Create the file src/main/java/com/tidyfiles/UndoManager.java" followed by the specification for that class.
help me get Gemini CLI to create the missing Java files
Validate the generated code compiles
All four classes reference each other, so a clean compile proves they're wired together correctly.
- Run the Maven compiler to check for errors:
mvn compile
- You should see BUILD SUCCESS at the end of the output.
Seeing compilation errors?
If mvn compile fails, read the error message carefully. Common issues include missing import statements or a method signature mismatch between classes.
Copy the full error output and paste it into Gemini CLI with the prompt: "Fix these compilation errors in my project." Gemini will read the errors and update the files automatically.
help me fix Maven compilation errors in my generated Java code
✔️ Awesome, I've got everything!
Great. Make sure you've saved all files and your build shows BUILD SUCCESS.
ⓧ I'd like to double check the full code
Here is the reference implementation for each file. Your Gemini-generated code may differ in style but should have the same structure and behavior.
TidyFiles.java:
package com.tidyfiles;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.util.concurrent.Callable;
@Command(name = "tidyfiles",
mixinStandardHelpOptions = true,
version = "tidyfiles 1.0",
description = "Organizes files in a directory into categorized subfolders.")
public class TidyFiles implements Callable<Integer> {
@Parameters(index = "0", description = "The directory to organize.", defaultValue = ".")
private File targetDirectory;
@Option(names = {"-d", "--dry-run"}, description = "Show what would be done without moving files.")
private boolean dryRun;
@Option(names = {"-v", "--verbose"}, description = "Print detailed output for each file moved.")
private boolean verbose;
@Option(names = {"-u", "--undo"}, description = "Undo the last organization operation.")
private boolean undo;
public static void main(String[] args) {
int exitCode = new CommandLine(new TidyFiles()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() {
if (!targetDirectory.exists() || !targetDirectory.isDirectory()) {
System.err.println("\u001B[31mError: " + targetDirectory.getPath() + " is not a valid directory.\u001B[0m");
return 1;
}
if (undo) {
UndoManager undoManager = new UndoManager(targetDirectory);
return undoManager.undo(verbose) ? 0 : 1;
}
FileOrganizer organizer = new FileOrganizer(targetDirectory, dryRun, verbose);
return organizer.organize();
}
}
FileCategory.java:
package com.tidyfiles;
import java.util.Map;
import java.util.HashMap;
public enum FileCategory {
IMAGES("images", "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "ico", "tiff"),
DOCUMENTS("documents", "pdf", "doc", "docx", "txt", "rtf", "odt", "xls", "xlsx", "ppt", "pptx", "csv"),
CODE("code", "java", "py", "js", "ts", "html", "css", "cpp", "c", "h", "go", "rs", "rb", "php", "sh", "sql"),
AUDIO("audio", "mp3", "wav", "flac", "aac", "ogg", "wma", "m4a"),
VIDEO("video", "mp4", "avi", "mkv", "mov", "wmv", "flv", "webm"),
ARCHIVES("archives", "zip", "tar", "gz", "rar", "7z", "bz2", "xz", "jar"),
MISC("misc");
private final String folderName;
private final String[] extensions;
FileCategory(String folderName, String... extensions) {
this.folderName = folderName;
this.extensions = extensions;
}
public String getFolderName() {
return folderName;
}
private static final Map<String, FileCategory> EXTENSION_MAP = new HashMap<>();
static {
for (FileCategory category : values()) {
for (String ext : category.extensions) {
EXTENSION_MAP.put(ext.toLowerCase(), category);
}
}
}
public static FileCategory fromExtension(String extension) {
return EXTENSION_MAP.getOrDefault(extension.toLowerCase(), MISC);
}
}
FileOrganizer.java:
package com.tidyfiles;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class FileOrganizer {
private final File targetDirectory;
private final boolean dryRun;
private final boolean verbose;
private final List<String> moveLog;
public FileOrganizer(File targetDirectory, boolean dryRun, boolean verbose) {
this.targetDirectory = targetDirectory;
this.dryRun = dryRun;
this.verbose = verbose;
this.moveLog = new ArrayList<>();
}
public int organize() {
File[] files = targetDirectory.listFiles();
if (files == null || files.length == 0) {
System.out.println("No files found in " + targetDirectory.getPath());
return 0;
}
int movedCount = 0;
int skippedCount = 0;
for (File file : files) {
if (!file.isFile()) {
continue;
}
if (file.isHidden()) {
if (verbose) {
System.out.println(" Skipping hidden file: " + file.getName());
}
skippedCount++;
continue;
}
String extension = getExtension(file.getName());
FileCategory category = FileCategory.fromExtension(extension);
File categoryDir = new File(targetDirectory, category.getFolderName());
if (dryRun) {
System.out.println(" [DRY RUN] " + file.getName() + " -> " + category.getFolderName() + "/");
movedCount++;
} else {
if (moveFile(file, categoryDir)) {
movedCount++;
moveLog.add(file.getName() + "|" + category.getFolderName());
if (verbose) {
System.out.println(" \u001B[32mMoved:\u001B[0m " + file.getName() + " -> " + category.getFolderName() + "/");
}
} else {
skippedCount++;
}
}
}
if (!dryRun && !moveLog.isEmpty()) {
UndoManager undoManager = new UndoManager(targetDirectory);
undoManager.saveLog(moveLog);
}
System.out.println("\n\u001B[36mDone!\u001B[0m Organized " + movedCount + " files, skipped " + skippedCount + ".");
if (dryRun) {
System.out.println("(Dry run - no files were actually moved.)");
}
return 0;
}
private boolean moveFile(File file, File categoryDir) {
if (!categoryDir.exists() && !categoryDir.mkdirs()) {
System.err.println(" \u001B[31mError:\u001B[0m Could not create directory: " + categoryDir.getPath());
return false;
}
Path source = file.toPath();
Path destination = categoryDir.toPath().resolve(file.getName());
try {
if (Files.isSymbolicLink(source)) {
if (verbose) {
System.out.println(" Skipping symlink: " + file.getName());
}
return false;
}
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
return true;
} catch (IOException e) {
System.err.println(" \u001B[31mError moving\u001B[0m " + file.getName() + ": " + e.getMessage());
return false;
}
}
private String getExtension(String filename) {
int lastDot = filename.lastIndexOf('.');
if (lastDot == -1 || lastDot == filename.length() - 1) {
return "";
}
return filename.substring(lastDot + 1);
}
}
UndoManager.java:
package com.tidyfiles;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
public class UndoManager {
private static final String LOG_FILENAME = ".tidyfiles.log";
private final File targetDirectory;
private final File logFile;
public UndoManager(File targetDirectory) {
this.targetDirectory = targetDirectory;
this.logFile = new File(targetDirectory, LOG_FILENAME);
}
public void saveLog(List<String> entries) {
try {
Files.write(logFile.toPath(), entries);
} catch (IOException e) {
System.err.println("Warning: Could not save undo log: " + e.getMessage());
}
}
public boolean undo(boolean verbose) {
if (!logFile.exists()) {
System.err.println("\u001B[31mNo undo log found.\u001B[0m Run tidyfiles first to create one.");
return false;
}
try {
List<String> entries = Files.readAllLines(logFile.toPath());
int restoredCount = 0;
for (String entry : entries) {
String[] parts = entry.split("\\|");
if (parts.length != 2) {
continue;
}
String fileName = parts[0];
String categoryFolder = parts[1];
Path source = targetDirectory.toPath().resolve(categoryFolder).resolve(fileName);
Path destination = targetDirectory.toPath().resolve(fileName);
if (Files.exists(source)) {
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
restoredCount++;
if (verbose) {
System.out.println(" \u001B[33mRestored:\u001B[0m " + categoryFolder + "/" + fileName + " -> " + fileName);
}
}
}
Files.deleteIfExists(logFile.toPath());
System.out.println("\n\u001B[36mUndo complete!\u001B[0m Restored " + restoredCount + " files.");
return true;
} catch (IOException e) {
System.err.println("\u001B[31mError during undo:\u001B[0m " + e.getMessage());
return false;
}
}
}
All four classes compile together. Next up, you'll package everything into an executable JAR and run your tool against real files.
Build, Package, and Run Your Tool
Your code compiles cleanly, which means all four Java classes are syntactically correct and their dependencies resolve. Compiling is only half the story though.
To actually run your tool from the command line, you need a single executable file that bundles your code and all its dependencies together. This is called an uber-JAR, and it is what makes your tool portable.
In this step, get ready to:
- Package your project into an executable uber-JAR.
- Test the tool against a directory full of sample files.
- Create a shell alias so you can run tidyfiles from anywhere.
Package the project into an uber-JAR
An uber-JAR (also called a fat JAR) packs all your compiled classes and the picocli library into a single .jar file. The maven-shade-plugin you configured in pom.xml handles this automatically during the package phase.
- Build and package your project by running this command:
mvn package
The build takes a moment. You should see output ending with BUILD SUCCESS and a line mentioning "Replacing original artifact with shaded artifact."
What does this command do?
Maven's package phase compiles your source code, runs any tests, and then produces the JAR artifact.
Because you configured the maven-shade-plugin to run during the package phase, it automatically bundles the picocli dependency into the output JAR. The result is target/tidyfiles-1.0-SNAPSHOT.jar containing everything needed to run your tool.
- Verify the JAR is executable by running the built-in help flag:
java -jar target/tidyfiles-1.0-SNAPSHOT.jar --help
You should see usage text describing the tidyfiles command, its parameters, and its options (--dry-run, --verbose, --undo).
Seeing "no main manifest attribute"?
This means the shade plugin did not run properly. Make sure your pom.xml has the maven-shade-plugin with ManifestResourceTransformer setting the mainClass to com.tidyfiles.TidyFiles.
Also confirm you ran mvn package (not mvn compile). The shade plugin only runs during the package phase.
Help me fix the "no main manifest attribute" error in my uber-JAR.
Create a test directory and run the tool
Your JAR works, but you need to see it organize real files. You will create a temporary directory with sample files of different types, then point tidyfiles at it.
- Create a test directory with sample files by running these commands:
mkdir -p ~/test-tidy
touch ~/test-tidy/photo.jpg
touch ~/test-tidy/report.pdf
touch ~/test-tidy/song.mp3
touch ~/test-tidy/backup.zip
touch ~/test-tidy/app.java
touch ~/test-tidy/notes.txt
touch ~/test-tidy/clip.mp4
What does this set up?
This creates a ~/test-tidy folder with seven empty files spanning different categories: images (.jpg), documents (.pdf, .txt), audio (.mp3), video (.mp4), archives (.zip), and code (.java).
The files are empty because tidyfiles categorizes by extension, not content. This is enough to prove the tool works.
- Run tidyfiles against the test directory in verbose mode:
java -jar target/tidyfiles-1.0-SNAPSHOT.jar --verbose ~/test-tidy
You should see colored output showing each file being moved into its category folder. The final line should report organizing 7 files.
- Confirm the files were sorted correctly by listing the test directory:
ls ~/test-tidy
You should see category subfolders: archives, audio, code, documents, images, and video.
- Check that files ended up in the right places by listing a specific folder:
ls ~/test-tidy/images
You should see photo.jpg listed inside images/.
Files not moving?
Check that you passed the full path ~/test-tidy as the argument, not a relative path from a different directory.
If you see "No files found", make sure the touch commands completed successfully by running ls ~/test-tidy before running tidyfiles.
Help me debug why tidyfiles isn't moving my test files.
Create a shell alias for tidyfiles
Typing java -jar ~/tidyfiles/target/tidyfiles-1.0-SNAPSHOT.jar every time is tedious. A shell alias lets you run tidyfiles as a single command from any directory on your system.
- Add the alias to your ~/.bashrc by running this command:
echo "alias tidyfiles='java -jar ~/tidyfiles/target/tidyfiles-1.0-SNAPSHOT.jar'" >> ~/.bashrc
- Reload your shell configuration so the alias takes effect:
source ~/.bashrc
What does this do?
The echo command appends a line to ~/.bashrc, which is a file that runs every time you open a new terminal. The alias maps the word tidyfiles to the full java -jar command.
Running source ~/.bashrc applies the change to your current terminal session without needing to close and reopen it.
- Test the alias by undoing your previous test run:
tidyfiles --undo ~/test-tidy
You should see output confirming that files were restored to ~/test-tidy. The category subfolders are now empty and the original files are back at the top level.
Command not found?
Make sure you ran source ~/.bashrc after adding the alias. Without sourcing, your current session does not know about the new alias.
Also verify the path in the alias points to your actual JAR location. Run ls ~/tidyfiles/target/tidyfiles-1.0-SNAPSHOT.jar to confirm the file exists.
Help me fix the tidyfiles alias not being recognized.
Your tool is built, tested, and accessible from anywhere on your system. Next up, you will use Gemini CLI to add professional-grade error handling and polished terminal output.
Iterate and Improve with Gemini CLI
You have a working file organizer that compiles, packages, and runs. That is the foundation.
Real CLI tools need to handle the unexpected gracefully. What happens when a file has no read permission? What about symbolic links or empty directories? In this step, you will use Gemini CLI to iteratively improve your tool with proper error handling, an undo system, and polished terminal output.
In this step, get ready to:
- Add error handling for edge cases using Gemini CLI prompts.
- Refactor the code for Java best practices and ANSI-colored output.
- Test all features against your test directory.
Add error handling and the undo flag
Your tool already moves files, but it does not yet handle edge cases that real file systems throw at you. Symlinks, hidden files, permission errors, and empty directories all need graceful handling.
- Switch back to your terminal in the ~/tidyfiles directory.
- Launch Gemini CLI by running:
gemini
- Give Gemini the following prompt to add error handling and the undo feature:
@src/main/java/com/tidyfiles/ Review the project source files. Add robust error handling for these edge cases: permission denied errors when moving files, symbolic links (skip them with a verbose message), hidden files (skip them with a verbose message), and empty directories (print a message and return 0). Also add an --undo flag to TidyFiles.java that reads a .tidyfiles.log file to reverse the last organization. Create a new UndoManager.java class in the same package that saves a pipe-delimited log of moved files and can restore them. Use ANSI color codes for error messages (red) and success messages (cyan/green).
What does this prompt do?
The @src/main/java/com/tidyfiles/ prefix tells Gemini CLI to read all Java files in that directory and include them as context. This means Gemini sees your existing code before proposing changes.
The rest of the prompt is a structured request listing specific behaviors you want. Being explicit about error types, skip behavior, and file formats produces more accurate code on the first pass.
- Gemini CLI will show you a diff of proposed changes for each file. Review each diff carefully.
- Press y to accept each change, or n to reject and refine your prompt.
What should the generated code include?
After accepting the changes, your code should have:
- A FileOrganizer.java that checks Files.isSymbolicLink() before moving and skips hidden files via file.isHidden().
- A TidyFiles.java with a --undo option annotated with @Option.
- A new UndoManager.java class that writes a .tidyfiles.log file and can reverse moves.
- ANSI escape codes like \u001B[31m (red) for errors and \u001B[36m (cyan) for success messages.
- Verify the updated code compiles by running:
mvn compile
- You should see BUILD SUCCESS in the terminal output.
Seeing compilation errors?
If mvn compile fails, the AI-generated code may have a typo or missing import. Go back to Gemini CLI and ask it to fix the error.
Try a prompt like: "The code you generated has this compilation error: [paste the error]. Please fix it."
Help me fix a compilation error in my tidyfiles project after Gemini generated code changes.
Refactor for best practices and polished output
Working code is good. Professional code is better. Now you will ask Gemini CLI to refine the generated code with Java conventions and a polished user experience.
- In the same Gemini CLI session, give this follow-up prompt:
@src/main/java/com/tidyfiles/ Refactor this code to follow Java best practices: use final on fields that don't change, add proper Javadoc-style inline comments explaining what each section does, and make sure all ANSI-colored output resets with \u001B[0m after each colored segment. Make the terminal output user-friendly with clear summary counts of moved and skipped files.
- Review the proposed diffs and press y to accept improvements that look correct.
- Verify everything still compiles cleanly by running:
mvn compile
- You should see BUILD SUCCESS again with no warnings.
Why iterate in separate prompts?
Breaking improvements into focused prompts gives you more control over what changes. Each prompt targets one concern (error handling, then polish), so you can review diffs that are small enough to understand at a glance.
This is the core pattern of AI-assisted development: scaffold first, then refine in focused passes.
Gemini made changes you did not want?
If Gemini restructured code in a way that breaks your existing logic, you can press n on any proposed diff to reject it. Then refine your prompt to be more specific about what you want changed.
How do I get Gemini CLI to make smaller, more targeted changes to my code?
Test all features against your test directory
Your code is refined and compiling. Time to package the final version and prove every feature works.
- Exit Gemini CLI by pressing Ctrl+C or typing /quit.
- Rebuild the uber-JAR with your improved code by running:
mvn package
- You should see BUILD SUCCESS and a fresh target/tidyfiles-1.0-SNAPSHOT.jar file.
What does mvn package do?
The mvn package command compiles your code, runs any tests, and then uses the maven-shade-plugin to bundle everything into a single executable JAR. This updated JAR now includes all your error handling and undo improvements.
- Reset your test directory so there are loose files to organize by running:
# Remove old category folders from previous test runs
rm -rf ~/test-tidy/images ~/test-tidy/documents ~/test-tidy/code ~/test-tidy/audio ~/test-tidy/video ~/test-tidy/archives ~/test-tidy/misc
# Create fresh test files
touch ~/test-tidy/photo.png ~/test-tidy/report.pdf ~/test-tidy/app.js ~/test-tidy/song.mp3 ~/test-tidy/backup.zip ~/test-tidy/.hidden-file
- Test the --dry-run flag to preview what would happen without moving files:
java -jar target/tidyfiles-1.0-SNAPSHOT.jar --dry-run --verbose ~/test-tidy
- You should see output listing each file with [DRY RUN] and its target folder, plus a message about skipping the hidden file.
- Now run the tool for real to organize your files:
java -jar target/tidyfiles-1.0-SNAPSHOT.jar --verbose ~/test-tidy
- You should see green Moved: messages for each file and a cyan Done! summary at the end.
- Verify the files landed in the correct folders:
ls ~/test-tidy/images ~/test-tidy/documents ~/test-tidy/code ~/test-tidy/audio ~/test-tidy/archives
- You should see photo.png in images, report.pdf in documents, app.js in code, song.mp3 in audio, and backup.zip in archives.
- Test the --undo flag to reverse the organization:
java -jar target/tidyfiles-1.0-SNAPSHOT.jar --undo --verbose ~/test-tidy
- You should see yellow Restored: messages for each file and a cyan Undo complete! summary.
- Confirm the files are back in the root of ~/test-tidy:
ls ~/test-tidy
- You should see photo.png, report.pdf, app.js, song.mp3, backup.zip, and .hidden-file all back at the top level.
Undo not restoring files?
The --undo flag reads a .tidyfiles.log file that gets created during the organize step. If you see a "No undo log found" error, it means the organize step did not create the log successfully.
Run the organize command again without --dry-run first, then try --undo immediately after.
Help me debug why the undo feature is not finding the log file.
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files and that mvn package produces a clean build.
ⓧ I'd like to double check the full code
Here is the complete code for each file at this point in the project.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tidyfiles</groupId>
<artifactId>tidyfiles</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.tidyfiles.TidyFiles</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package com.tidyfiles;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.util.concurrent.Callable;
@Command(name = "tidyfiles",
mixinStandardHelpOptions = true,
version = "tidyfiles 1.0",
description = "Organizes files in a directory into categorized subfolders.")
public class TidyFiles implements Callable<Integer> {
@Parameters(index = "0", description = "The directory to organize.", defaultValue = ".")
private File targetDirectory;
@Option(names = {"-d", "--dry-run"}, description = "Show what would be done without moving files.")
private boolean dryRun;
@Option(names = {"-v", "--verbose"}, description = "Print detailed output for each file moved.")
private boolean verbose;
@Option(names = {"-u", "--undo"}, description = "Undo the last organization operation.")
private boolean undo;
public static void main(String[] args) {
int exitCode = new CommandLine(new TidyFiles()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() {
if (!targetDirectory.exists() || !targetDirectory.isDirectory()) {
System.err.println("\u001B[31mError: " + targetDirectory.getPath() + " is not a valid directory.\u001B[0m");
return 1;
}
if (undo) {
UndoManager undoManager = new UndoManager(targetDirectory);
return undoManager.undo(verbose) ? 0 : 1;
}
FileOrganizer organizer = new FileOrganizer(targetDirectory, dryRun, verbose);
return organizer.organize();
}
}
package com.tidyfiles;
import java.util.Map;
import java.util.HashMap;
public enum FileCategory {
IMAGES("images", "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "ico", "tiff"),
DOCUMENTS("documents", "pdf", "doc", "docx", "txt", "rtf", "odt", "xls", "xlsx", "ppt", "pptx", "csv"),
CODE("code", "java", "py", "js", "ts", "html", "css", "cpp", "c", "h", "go", "rs", "rb", "php", "sh", "sql"),
AUDIO("audio", "mp3", "wav", "flac", "aac", "ogg", "wma", "m4a"),
VIDEO("video", "mp4", "avi", "mkv", "mov", "wmv", "flv", "webm"),
ARCHIVES("archives", "zip", "tar", "gz", "rar", "7z", "bz2", "xz", "jar"),
MISC("misc");
private final String folderName;
private final String[] extensions;
FileCategory(String folderName, String... extensions) {
this.folderName = folderName;
this.extensions = extensions;
}
public String getFolderName() {
return folderName;
}
private static final Map<String, FileCategory> EXTENSION_MAP = new HashMap<>();
static {
for (FileCategory category : values()) {
for (String ext : category.extensions) {
EXTENSION_MAP.put(ext.toLowerCase(), category);
}
}
}
public static FileCategory fromExtension(String extension) {
return EXTENSION_MAP.getOrDefault(extension.toLowerCase(), MISC);
}
}
package com.tidyfiles;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class FileOrganizer {
private final File targetDirectory;
private final boolean dryRun;
private final boolean verbose;
private final List<String> moveLog;
public FileOrganizer(File targetDirectory, boolean dryRun, boolean verbose) {
this.targetDirectory = targetDirectory;
this.dryRun = dryRun;
this.verbose = verbose;
this.moveLog = new ArrayList<>();
}
public int organize() {
File[] files = targetDirectory.listFiles();
if (files == null || files.length == 0) {
System.out.println("No files found in " + targetDirectory.getPath());
return 0;
}
int movedCount = 0;
int skippedCount = 0;
for (File file : files) {
if (!file.isFile()) {
continue;
}
if (file.isHidden()) {
if (verbose) {
System.out.println(" Skipping hidden file: " + file.getName());
}
skippedCount++;
continue;
}
String extension = getExtension(file.getName());
FileCategory category = FileCategory.fromExtension(extension);
File categoryDir = new File(targetDirectory, category.getFolderName());
if (dryRun) {
System.out.println(" [DRY RUN] " + file.getName() + " -> " + category.getFolderName() + "/");
movedCount++;
} else {
if (moveFile(file, categoryDir)) {
movedCount++;
moveLog.add(file.getName() + "|" + category.getFolderName());
if (verbose) {
System.out.println(" \u001B[32mMoved:\u001B[0m " + file.getName() + " -> " + category.getFolderName() + "/");
}
} else {
skippedCount++;
}
}
}
if (!dryRun && !moveLog.isEmpty()) {
UndoManager undoManager = new UndoManager(targetDirectory);
undoManager.saveLog(moveLog);
}
System.out.println("\n\u001B[36mDone!\u001B[0m Organized " + movedCount + " files, skipped " + skippedCount + ".");
if (dryRun) {
System.out.println("(Dry run - no files were actually moved.)");
}
return 0;
}
private boolean moveFile(File file, File categoryDir) {
if (!categoryDir.exists() && !categoryDir.mkdirs()) {
System.err.println(" \u001B[31mError:\u001B[0m Could not create directory: " + categoryDir.getPath());
return false;
}
Path source = file.toPath();
Path destination = categoryDir.toPath().resolve(file.getName());
try {
if (Files.isSymbolicLink(source)) {
if (verbose) {
System.out.println(" Skipping symlink: " + file.getName());
}
return false;
}
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
return true;
} catch (IOException e) {
System.err.println(" \u001B[31mError moving\u001B[0m " + file.getName() + ": " + e.getMessage());
return false;
}
}
private String getExtension(String filename) {
int lastDot = filename.lastIndexOf('.');
if (lastDot == -1 || lastDot == filename.length() - 1) {
return "";
}
return filename.substring(lastDot + 1);
}
}
package com.tidyfiles;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
public class UndoManager {
private static final String LOG_FILENAME = ".tidyfiles.log";
private final File targetDirectory;
private final File logFile;
public UndoManager(File targetDirectory) {
this.targetDirectory = targetDirectory;
this.logFile = new File(targetDirectory, LOG_FILENAME);
}
public void saveLog(List<String> entries) {
try {
Files.write(logFile.toPath(), entries);
} catch (IOException e) {
System.err.println("Warning: Could not save undo log: " + e.getMessage());
}
}
public boolean undo(boolean verbose) {
if (!logFile.exists()) {
System.err.println("\u001B[31mNo undo log found.\u001B[0m Run tidyfiles first to create one.");
return false;
}
try {
List<String> entries = Files.readAllLines(logFile.toPath());
int restoredCount = 0;
for (String entry : entries) {
String[] parts = entry.split("\\|");
if (parts.length != 2) {
continue;
}
String fileName = parts[0];
String categoryFolder = parts[1];
Path source = targetDirectory.toPath().resolve(categoryFolder).resolve(fileName);
Path destination = targetDirectory.toPath().resolve(fileName);
if (Files.exists(source)) {
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
restoredCount++;
if (verbose) {
System.out.println(" \u001B[33mRestored:\u001B[0m " + categoryFolder + "/" + fileName + " -> " + fileName);
}
}
}
Files.deleteIfExists(logFile.toPath());
System.out.println("\n\u001B[36mUndo complete!\u001B[0m Restored " + restoredCount + " files.");
return true;
} catch (IOException e) {
System.err.println("\u001B[31mError during undo:\u001B[0m " + e.getMessage());
return false;
}
}
}
Secret mission
Add Custom Category Configuration
Your tool organizes files into fixed categories. But what if a designer wants .psd files in a "design" folder instead of "misc"? In this secret mission, you'll add user-configurable category mappings via a JSON config file at ~/.tidyfiles.json.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. The Gemini CLI free tier covered all usage during development.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local project folder at ~/tidyfiles containing all source code, Maven config, and the packaged JAR.
- Shell alias tidyfiles added to ~/.bashrc.
- Custom config file at ~/.tidyfiles.json (if you completed the Secret Mission).
Keep everything running
No action needed. Choose this if you want to keep using tidyfiles to organize directories on your system.
- Your tidyfiles alias remains available in any terminal session.
- The project folder stays at ~/tidyfiles if you want to add features later.
- Your custom config at ~/.tidyfiles.json stays active for any future runs.
Pause - I'll come back to this later
Remove the shell alias to free up the command name, but keep the project files so you can rebuild later.
- Open ~/.bashrc in a text editor and remove the line containing the tidyfiles alias.
- Reload your shell by running:
source ~/.bashrc
- Your project files remain at ~/tidyfiles. To rebuild later, run mvn package and re-add the alias.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Open ~/.bashrc in a text editor and remove the line containing the tidyfiles alias.
- Delete the project folder by running:
rm -rf ~/tidyfiles
- Delete the custom config file by running:
rm -f ~/.tidyfiles.json
- Reload your shell to apply the alias removal:
source ~/.bashrc
- Confirm the alias is gone by running tidyfiles --help. You should see a "command not found" message.
Nice Work!
Nice Work!
Nice work! You just built a fully functional command-line file organizer using Gemini CLI to generate every line of Java code.
You've learned how to:
- Built a complete CLI tool (tidyfiles) by writing structured prompts in Gemini CLI that produce working Java code on the first attempt.
- Used picocli annotations for professional argument parsing with --help, --dry-run, --verbose, and --undo flags.
- Packaged an executable uber-JAR with maven-shade-plugin and created a shell alias to run tidyfiles from anywhere on your system.
- Secret Mission: Added user-configurable category mappings via a JSON config file (~/.tidyfiles.json) so users can define their own file-type-to-folder rules.
Ready to quiz yourself?