code-tree-cli
Version:
Code Tree CLI
185 lines (159 loc) • 5.02 kB
JavaScript
// Imports.
import fs from 'fs';
import path from 'path';
import readline from 'readline';
import ignore from 'ignore';
import { promisify } from 'util';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import micromatch from 'micromatch';
// Promisify fs methods
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
/**
* Build ignore filter.
* @param {string} dir The directory to search in.
* @return {Promise<Ignore>} An ignore filter.
*/
export async function buildIgnoreFilter(dir) {
const ig = ignore();
const gitignorePath = path.join(dir, '.gitignore');
// Import default ignore patterns.
const { getDefaultIgnorePatterns } = await import('./defaults.js');
const ignorePatterns = getDefaultIgnorePatterns();
// Add all default patterns.
Object.values(ignorePatterns).forEach((category) => {
ig.add(category);
});
// If there's a .gitignore, parse it.
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
ig.add(gitignoreContent.split(/\r?\n/));
}
return ig;
}
/**
* Recursively get all files that match a pattern and are not ignored.
* @param {string} baseDir The base directory.
* @param {function} filter A function that tests whether a file path is ignored.
* @param {string} pattern The glob pattern to match files.
* @return {Promise<string[]>} A list of file paths.
*/
export async function getAllFiles(baseDir, filter, pattern) {
const filesList = [];
async function recurse(currentDir) {
let items;
try {
items = await readdir(currentDir);
} catch (err) {
return;
}
for (const item of items) {
const fullPath = path.join(currentDir, item);
const relativePath = path.relative(baseDir, fullPath);
if (filter.ignores(relativePath)) {
continue;
}
let stats;
try {
stats = await stat(fullPath);
} catch {
continue;
}
if (stats.isDirectory()) {
await recurse(fullPath);
} else if (stats.isFile() && micromatch.isMatch(relativePath, pattern)) {
filesList.push(fullPath);
}
}
}
await recurse(baseDir);
return filesList;
}
/**
* Prompt user for yes/no input in the console.
* @param {string} question
* @returns {Promise<boolean>} True if "y", false otherwise.
*/
export async function askYesNo(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${question} (y/n) `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y');
});
});
}
/**
* Main CLI logic.
*/
(async function main() {
try {
const argv = yargs(hideBin(process.argv))
.command('$0 [pattern]', 'Recursively search for files matching [pattern] and print file contents.', (yargs) => {
yargs.positional('pattern', {
describe: 'File pattern to match (default: "**/*")',
default: '**/*',
});
})
.option('dir', {
alias: 'd',
type: 'string',
describe: 'Directory to search in (default: ".")',
default: '.',
})
.option('limit', {
alias: 'l',
type: 'number',
describe: 'Max number of files to process without prompting',
default: 100,
})
.help()
.alias('help', 'h')
.version()
.alias('version', 'v')
.argv;
let targetDir = argv.dir || '.';
const fileLimit = argv.limit;
const pattern = argv.pattern || '**/*';
targetDir = path.resolve(process.cwd(), targetDir);
const ig = await buildIgnoreFilter(targetDir);
const files = await getAllFiles(targetDir, ig, pattern);
if (files.length > fileLimit) {
const proceed = await askYesNo(`Found ${files.length} files matching pattern "${pattern}". Continue printing?`);
if (!proceed) {
console.error('Aborted.');
process.exit(1);
}
}
for (const filePath of files) {
// Print a separator before each file, except the first one.
const isFirstFile = files.indexOf(filePath) === 0;
if (!isFirstFile) {
console.log('\n');
}
// Print file path.
const SEPARATOR = '/'.repeat(10);
console.log(`${SEPARATOR}${SEPARATOR}${SEPARATOR}`);
console.log(`/// File path: ${path.relative(process.cwd(), filePath)}`);
console.log(`${SEPARATOR}${SEPARATOR}${SEPARATOR}\n`);
// Define file content.
const content = await readFile(filePath, 'utf-8');
// Check if binary file.
if (content.includes('\u0000')) {
console.log('/// Binary file. Skipping content.');
continue;
}
// Trim trailing whitespace and print content.
console.log(`${content.trimEnd()}`);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
})();