Welcome back! In Chapter 2: Interactive Command UI, we built the interface that accepts user input.
However, right now, our "Waiter" (the UI) is very gullible. If the user orders a dish that doesn't exist (like add-dir ./ghost-folder) or tries to order a fork instead of food (adding a file instead of a directory), our code crashes or behaves unexpectedly.
We need a Gatekeeper.
Imagine a nightclub. Before you can enter, a bouncer checks your ID.
Directory Validation is that bouncer. It ensures that only valid, useful data enters our system.
We want to handle this scenario safely:
my-cli add-dir ./my-file.txt
Desired Outcome: Instead of crashing, the system should gently reply: "./my-file.txt is not a directory. Did you mean to add the parent directory?"
In programming, a simple true or false is often not enough. If validation fails, we need to know why so we can tell the user.
We use a pattern called a Discriminated Union. Think of it as a "Report Card" that always has a resultType.
// The possible outcomes of our validation
export type AddDirectoryResult =
| { resultType: 'success'; absolutePath: string }
| { resultType: 'pathNotFound'; absolutePath: string }
| { resultType: 'notADirectory'; absolutePath: string }
| { resultType: 'alreadyInWorkingDirectory'; workingDir: string };
Explanation:
absolutePath.
We will write a function called validateDirectoryForWorkspace. It takes the raw input string and runs it through our "Bouncer" checks.
First, we need to convert the user's text (which might include ~ or ..) into a real system path.
// Inside validation.ts
import { resolve } from 'path';
import { expandPath } from '../../utils/path.js';
// Convert "~/docs" -> "/Users/alice/docs"
const absolutePath = resolve(expandPath(directoryPath));
Explanation:
expandPath: Handles shortcuts like ~ (Home directory).resolve: Turns relative paths (./src) into full absolute paths (/project/src).Now we ask the operating system: "What is this thing?"
import { stat } from 'fs/promises';
try {
const stats = await stat(absolutePath); // Ask OS for info
if (!stats.isDirectory()) {
// It exists, but it is NOT a directory
return { resultType: 'notADirectory', absolutePath, directoryPath };
}
} catch (error) {
// If stat throws an error, the path likely doesn't exist
return { resultType: 'pathNotFound', absolutePath, directoryPath };
}
Explanation:
stat: A Node.js function that gets file details.isDirectory(): Returns true if it's a folder.stat crashes (throws an error). We catch that crash and return a formatted pathNotFound result instead.
This is the smartest part of our Bouncer. If you already have access to /Projects, you implicitly have access to /Projects/Startups. We shouldn't add the sub-folder again.
// Get list of folders we already have
const currentWorkingDirs = allWorkingDirectories(permissionContext);
for (const workingDir of currentWorkingDirs) {
// Check if our new path is inside an existing one
if (pathInWorkingPath(absolutePath, workingDir)) {
return {
resultType: 'alreadyInWorkingDirectory',
directoryPath,
workingDir, // Tell them which parent folder owns it
};
}
}
Explanation:
pathInWorkingPath: A helper that checks if Child is inside Parent.
Finally, the UI needs to print a message. We use a helper function to translate the resultType into English.
export function addDirHelpMessage(result: AddDirectoryResult): string {
switch (result.resultType) {
case 'pathNotFound':
return `Path ${result.absolutePath} was not found.`;
case 'notADirectory':
return `${result.directoryPath} is not a directory.`;
case 'alreadyInWorkingDirectory':
return `Already accessible via ${result.workingDir}.`;
case 'success':
return `Success! Added ${result.absolutePath}.`;
}
}
Explanation:
Let's visualize the flow when a user tries to add a file instead of a folder.
One tricky part of validation is handling different types of "Missing" errors.
In Node.js, checking a file can fail for multiple reasons:
ENOENT).ENOTDIR).EACCES).Our implementation groups these together carefully:
// Inside the catch block
const code = getErrnoCode(e);
// We treat permission errors (EACCES) similar to "Not Found".
// This prevents the CLI from crashing just because
// we touched a system folder.
if (code === 'ENOENT' || code === 'EACCES') {
return {
resultType: 'pathNotFound',
// ... details
};
}
throw e; // Unknown error? Re-throw it (Crash safely).
This makes the tool feel robust. It doesn't panic; it just informs the user that the path isn't usable.
In this chapter, we created the Directory Validation logic. We learned:
resultType) to return detailed status reports.stat.Now we have a valid path! The Bouncer has let the user in. The next step is to actually store this permission in the application's memory so other tools can use it.
Next Chapter: State & Permission Management
Generated by Code IQ