Building tree2f: A High-Performance CLI Tool
How I built a zero-dependency CLI tool that converts ASCII directory trees into actual file systems using stack-based parsing.
Building tree2f: A High-Performance CLI Tool
Developers often see beautiful ASCII directory structures in README files or documentation. Manually creating those folders and files is tedious and error-prone. I built tree2f (Tree-to-Filesystem)βa zero-dependency CLI that manifests ASCII trees into real file systems instantly.
π The Problem: Manual File Scaffolding
Copy-pasting a 50-line ASCII tree and then manually creating 30 folders and 50 files? It shouldnβt exist as a workflow. I needed a tool that could:
- Parse messy ASCII trees (with mixed spacing, pipes, and elbows)
- Create folders and files with zero external dependencies
- Handle edge cases like extensionless files (
Dockerfile,Makefile) - Run in milliseconds
π οΈ Solution: Stack-Based Indentation Parsing
The Breakthrough: Start-of-Text Indexing
Initial attempts counted raw character positions. This failed on βmessyβ trees with mixed tabs and spaces.
The key insight: instead of counting visual symbols (pipes and elbows), find the index of the first alphanumeric character in each line. By comparing this index against a persistent Hierarchy Stack, the parser determines parent-child relationships regardless of symbol variation.
Example:
src/
components/
Button.tsx
Button.stories.tsx
utils/
helpers.ts
When parsing Button.tsx, the algorithm:
- Finds its start-of-text index (4 spaces in)
- Compares against the stack (last entry:
components/at index 2) - Determines:
Button.tsxis a child ofcomponents/
Handling Edge Cases: Smart File Detection
Most parsers assume files have extensions. Professional projects have:
Dockerfile(no extension)Makefile(no extension)LICENSE(no extension)
Solution: Trailing slash heuristic
folder/β Always a directoryREADMEβ Always a file
This simple rule made the tool feel intelligent and eliminated configuration overhead.
π Core Features
Fast Manifesting: 50-line tree β complete project structure in milliseconds.
tree2f create structure.txt --output ./my-app
Dry Run Mode: Preview exactly what will be created without touching the filesystem.
tree2f create structure.txt --dry-run
Integrity Validation: Compare existing project against a blueprint.
tree2f validate structure.txt --against ./existing-project
Smart Formatting: Clean up messy ASCII trees into standardized, beautiful output.
tree2f format messy-tree.txt > clean-tree.txt
ποΈ Technical Implementation
Parser Architecture
interface TreeNode {
name: string;
type: 'file' | 'directory';
children: TreeNode[];
depth: number;
}
function parseTree(input: string): TreeNode {
const lines = input.split('\n');
const stack: TreeNode[] = [];
const root = { name: '', type: 'directory', children: [], depth: -1 };
stack.push(root);
for (const line of lines) {
if (!line.trim()) continue;
const startIndex = line.search(/[a-zA-Z0-9]/);
const name = line.trim().replace(/[\/ββββ\s]/g, '');
const isDir = name.endsWith('/') || line.includes('/');
// Pop stack until we find the correct parent depth
while (stack.length > 1 && stack[stack.length - 1].depth >= startIndex) {
stack.pop();
}
const node: TreeNode = {
name: name.replace(/\/$/, ''),
type: isDir ? 'directory' : 'file',
children: [],
depth: startIndex,
};
stack[stack.length - 1].children.push(node);
stack.push(node);
}
return root;
}
File System Creation
async function createFileSystem(node: TreeNode, basePath: string) {
const fullPath = join(basePath, node.name);
if (node.type === 'directory') {
await mkdir(fullPath, { recursive: true });
} else {
await writeFile(fullPath, '');
}
for (const child of node.children) {
await createFileSystem(child, node.type === 'directory' ? fullPath : basePath);
}
}
π¦ Distribution and Testing
NPM Publication
Published as a globally-installable CLI:
npm install -g tree2f
t2f create mystructure.txt
Integration Tests
Custom test suite validates βmessyβ tree handling:
describe('Parser', () => {
it('handles mixed indentation', () => {
const tree = `
src/
\tcomponents/
\t Button.tsx
utils/
\t helpers.ts
`;
expect(parse(tree).children.length).toBe(2);
});
it('ignores common tree symbols', () => {
const tree = `
βββ src/
β βββ app.ts
β βββ types.ts
βββ tests/
βββ app.test.ts
`;
expect(parse(tree).children.length).toBe(2);
});
});
CI/CD Automation
GitHub Actions runs tests and CodeRabbit audits code on every push.
π‘ Key Takeaways
- Zero dependencies scale: Removing external packages made the tool faster and easier to distribute.
- Index-based parsing beats symbol counting: Simple indexing algorithm handles real-world messy input.
- Heuristics solve edge cases: Trailing slashes eliminated the need for configuration.
- Stack-based algorithms are elegant: Hierarchy tracking without recursion is memory-efficient.
- Testing messy input matters: Real trees arenβt perfectly formatted; tests should reflect reality.