UNPKG

code-tree-cli

Version:
185 lines (159 loc) 5.02 kB
#!/usr/bin/env node // 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); } })();