Building tree2f: A High-Performance CLI Tool

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:

  1. Parse messy ASCII trees (with mixed spacing, pipes, and elbows)
  2. Create folders and files with zero external dependencies
  3. Handle edge cases like extensionless files (Dockerfile, Makefile)
  4. 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:

  1. Finds its start-of-text index (4 spaces in)
  2. Compares against the stack (last entry: components/ at index 2)
  3. Determines: Button.tsx is a child of components/

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 directory
  • README β†’ 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

  1. Zero dependencies scale: Removing external packages made the tool faster and easier to distribute.
  2. Index-based parsing beats symbol counting: Simple indexing algorithm handles real-world messy input.
  3. Heuristics solve edge cases: Trailing slashes eliminated the need for configuration.
  4. Stack-based algorithms are elegant: Hierarchy tracking without recursion is memory-efficient.
  5. Testing messy input matters: Real trees aren’t perfectly formatted; tests should reflect reality.

πŸ“š Resources