@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
122 lines • 4.18 kB
JavaScript
import { lstat, readdir } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { loadGitignore } from '../utils/gitignore-loader.js';
/**
* Build a file tree from a root directory
* Respects gitignore and excludes hidden files by default
*/
export async function buildFileTree(rootPath, options = {}) {
const { maxDepth = 10, showHidden = false } = options;
const ig = loadGitignore(rootPath);
const walkDirectory = async (currentPath, relativeTo, depth) => {
if (depth > maxDepth)
return [];
try {
const items = await readdir(currentPath, { withFileTypes: true });
const nodes = [];
for (const item of items) {
// Skip hidden files unless showHidden is true
if (!showHidden && item.name.startsWith('.')) {
continue;
}
// Check gitignore
const itemPath = relativeTo ? join(relativeTo, item.name) : item.name;
if (ig.ignores(itemPath)) {
continue;
}
const fullPath = join(currentPath, item.name);
const isDirectory = item.isDirectory();
let size;
if (!isDirectory) {
try {
const stats = await lstat(fullPath);
size = stats.size;
}
catch {
size = undefined;
}
}
const node = {
name: item.name,
path: itemPath,
absolutePath: fullPath,
isDirectory,
size,
};
// Recursively build children for directories
if (isDirectory && depth < maxDepth) {
node.children = await walkDirectory(fullPath, itemPath, depth + 1);
}
nodes.push(node);
}
// Sort: directories first, then alphabetically
nodes.sort((a, b) => {
if (a.isDirectory && !b.isDirectory)
return -1;
if (!a.isDirectory && b.isDirectory)
return 1;
return a.name.localeCompare(b.name);
});
return nodes;
}
catch (error) {
if (error instanceof Error &&
'code' in error &&
error.code === 'EACCES') {
return [];
}
throw error;
}
};
return walkDirectory(rootPath, '', 0);
}
/**
* Flatten a file tree into a list for rendering
* Only includes visible nodes based on expanded state
*/
export function flattenTree(nodes, expanded, depth = 0) {
const result = [];
for (const node of nodes) {
const isExpanded = expanded.has(node.path);
const hasChildren = node.isDirectory && (node.children?.length ?? 0) > 0;
result.push({
node,
depth,
isExpanded,
hasChildren,
});
// Add children if directory is expanded
if (node.isDirectory && isExpanded && node.children) {
result.push(...flattenTree(node.children, expanded, depth + 1));
}
}
return result;
}
/**
* Flatten entire file tree (all nodes regardless of expansion)
* Used for searching across all files
*/
export function flattenTreeAll(nodes, depth = 0) {
const result = [];
for (const node of nodes) {
const hasChildren = node.isDirectory && (node.children?.length ?? 0) > 0;
result.push({
node,
depth,
isExpanded: false,
hasChildren,
});
// Always recurse into children
if (node.isDirectory && node.children) {
result.push(...flattenTreeAll(node.children, depth + 1));
}
}
return result;
}
/**
* Get relative path from cwd
*/
export function getRelativePath(absolutePath) {
return relative(process.cwd(), absolutePath);
}
//# sourceMappingURL=file-tree.js.map