<!-- Archivo: byteweaver.ts --> #!/usr/bin/env node import { concatenateFiles } from '../index'; import { CliOptions, ByteWeaverOptions } from '../types'; import path from 'path'; const DEFAULT_CONFIG: ByteWeaverOptions = { recursive: false, exclude: [], include: [], minify: false, outputTemplate: null, header: '', footer: '', }; function parseArguments(): CliOptions { const args = process.argv.slice(2); const options = { ...DEFAULT_CONFIG, directoryPath: '', outputFile: '' } as CliOptions; let i = 0; while (i < args.length) { const arg = args[i]; if (arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) { const flags = arg.substring(1).split(''); let requiresValue = false; let flagWithValue = ''; for (const flag of flags) { switch (flag) { case 'r': options.recursive = true; break; case 'm': options.minify = true; break; case 'd': options.debug = true; break; case 'e': requiresValue = true; flagWithValue = 'e'; break; case 'i': requiresValue = true; flagWithValue = 'i'; break; case 't': requiresValue = true; flagWithValue = 't'; break; case 'v': try { const packageJson = require('../../package.json'); console.log(`ByteWeaver v${packageJson.version}`); process.exit(0); } catch (err) { console.error('Error: Could not read package.json'); process.exit(1); } break; case 'h': showUsage(); process.exit(0); break; default: console.error(`Error: Unknown option -${flag}`); showUsage(); process.exit(1); } } if (requiresValue) { if (i + 1 < args.length) { const value = args[i + 1]; switch (flagWithValue) { case 'e': options.exclude = [...(options.exclude || []), ...value.split(',')]; break; case 'i': options.include = [...(options.include || []), ...value.split(',')]; break; case 't': options.outputTemplate = value; break; } i += 2; } else { console.error(`Error: -${flagWithValue} requires a value`); process.exit(1); } } else { i++; } } else { switch (arg) { case '-r': case '--recursive': options.recursive = true; i++; break; case '-e': case '--exclude': if (i + 1 < args.length) { options.exclude = [...(options.exclude || []), ...args[i + 1].split(',')]; i += 2; } else { console.error('Error: --exclude requires a pattern'); process.exit(1); } break; case '-i': case '--include': if (i + 1 < args.length) { options.include = [...(options.include || []), ...args[i + 1].split(',')]; i += 2; } else { console.error('Error: --include requires a pattern'); process.exit(1); } break; case '-m': case '--minify': options.minify = true; i++; break; case '-t': case '--template': if (i + 1 < args.length) { options.outputTemplate = args[i + 1]; i += 2; } else { console.error('Error: --template requires a template file'); process.exit(1); } break; case '-d': case '--debug': options.debug = true; i++; break; case '--header': if (i + 1 < args.length) { options.header = args[i + 1]; i += 2; } else { console.error('Error: --header requires a value'); process.exit(1); } break; case '--footer': if (i + 1 < args.length) { options.footer = args[i + 1]; i += 2; } else { console.error('Error: --footer requires a value'); process.exit(1); } break; case '--image-mode': if (i + 1 < args.length) { const mode = args[i + 1]; if (['base64-html', 'base64-markdown', 'none'].includes(mode)) { options.imageMode = mode as 'base64-html' | 'base64-markdown' | 'none'; } else { console.error('Error: --image-mode debe ser "base64-html", "base64-markdown" o "none"'); process.exit(1); } i += 2; } else { console.error('Error: --image-mode requiere un valor'); process.exit(1); } break; case '-v': case '--version': try { const packageJson = require('../../package.json'); console.log(`ByteWeaver v${packageJson.version}`); process.exit(0); } catch (err) { console.error('Error: Could not read package.json'); process.exit(1); } break; case '-h': case '--help': showUsage(); process.exit(0); break; default: if (!options.directoryPath) { options.directoryPath = arg; } else if (!options.outputFile) { options.outputFile = arg; } i++; break; } } } if (!options.directoryPath || !options.outputFile) { showUsage(); process.exit(1); } return options; } function showUsage(): void { const options = [ { flag: '-r, --recursive', desc: 'Search recursively through subdirectories' }, { flag: '-e, --exclude <pattern>', desc: 'Files to exclude (comma separated, can use *.ext for extensions)' }, { flag: '-i, --include <pattern>', desc: 'Files to include (comma separated, can use *.ext for extensions)' }, { flag: '-m, --minify', desc: 'Minify the output content' }, { flag: '-t, --template <file>', desc: 'Use a template file for the output' }, { flag: '-d, --debug', desc: 'Show detailed debug information during processing' }, { flag: '--header <text>', desc: 'Add header text at the beginning of the output file' }, { flag: '--footer <text>', desc: 'Add footer text at the end of the output file' }, { flag: '--image-mode <mode>', desc: 'Modo de procesamiento de imágenes: "base64-html", "base64-markdown" o "none"' }, { flag: '-v, --version', desc: 'Show version information' }, { flag: '-h, --help', desc: 'Show this help message' }, { flag: '.bwignore', desc: 'File in the working directory to specify patterns to exclude (similar to .gitignore)' }, ]; const examples = [ { cmd: 'byteweaver src output.js', desc: '' }, { cmd: 'byteweaver -r src output.js', desc: '' }, { cmd: 'byteweaver -e "node_modules,*.json" src output.js', desc: '' }, { cmd: 'byteweaver -r -i "*.js,*.ts" -e "test,*.md" src output.js', desc: '' }, { cmd: 'byteweaver -m -r -i "*.js" src output.min.js', desc: '' }, { cmd: 'byteweaver --header="" --footer="" src output.js', desc: '' }, { cmd: 'byteweaver -r src output.js', desc: 'With .bwignore excluding node_modules and *.log' }, ]; const combinedFlags = [ { cmd: 'bw -rmi "*.ts" src output.js', desc: 'Equivalent to -r -m -i "*.ts"' }, { cmd: 'bw -rmd src output.js', desc: 'Equivalent to -r -m -d' }, ]; console.log('Usage: byteweaver [options] <directory-path> <output-file>'); console.log('Options:'); options.forEach(opt => { console.log(` ${opt.flag.padEnd(25)} ${opt.desc}`); }); console.log(''); console.log('Combined flags are supported:'); combinedFlags.forEach(flag => { console.log(` ${flag.cmd.padEnd(30)} ${flag.desc}`); }); console.log(''); console.log('Examples:'); examples.forEach(ex => { console.log(` ${ex.cmd}`); }); } async function main(): Promise<void> { const options = parseArguments(); try { const result = await concatenateFiles(options.directoryPath, options.outputFile, options); console.log(`✅ Successfully concatenated ${result.fileCount} files to ${options.outputFile}`); if (result.processedFiles && result.processedFiles.length > 0) { console.log('\nFiles processed:'); result.processedFiles.forEach(file => { const fileName = path.basename(file); console.log(`- ${fileName}`); }); } } catch (error) { console.error('❌ Error concatenating files:', (error as Error).message); process.exit(1); } } main(); <!-- Archivo: index.ts --> import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; import { ByteWeaverOptions, ConcatenateResult } from './types'; const readdir = promisify(fs.readdir); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const stat = promisify(fs.stat); const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']; function isImageFile(filePath: string): boolean { const ext = path.extname(filePath).toLowerCase(); return IMAGE_EXTENSIONS.includes(ext); } function matchesPattern(filePath: string, patterns: string[], isExcludePattern: boolean = false): boolean { if (patterns.length === 0 && !isExcludePattern) { return true; } const fileName = path.basename(filePath); const relativePath = path.relative(process.cwd(), filePath); return patterns.some(pattern => { if (pattern.startsWith('*.')) { const extension = pattern.slice(1); return fileName.endsWith(extension); } return fileName === pattern || fileName.includes(pattern) || relativePath.includes(pattern) || filePath.includes(pattern); }); } async function readBwIgnore(directory: string): Promise<string[]> { const bwIgnorePath = path.join(directory, '.bwignore'); try { const bwIgnoreContent = await readFile(bwIgnorePath, 'utf8'); return bwIgnoreContent .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } catch (error) { return []; } } export async function getFilesRecursively( directory: string, excludePatterns: string[] = [], includePatterns: string[] = [], fileList: string[] = [] ): Promise<string[]> { const bwIgnorePatterns = await readBwIgnore(directory); const combinedExcludePatterns = [...excludePatterns, ...bwIgnorePatterns]; const files = await readdir(directory); for (const file of files) { const filePath = path.join(directory, file); const stats = await stat(filePath); if (stats.isDirectory()) { await getFilesRecursively(filePath, combinedExcludePatterns, includePatterns, fileList); } else { if (matchesPattern(filePath, combinedExcludePatterns, true)) { continue; } if (includePatterns.length > 0 && !matchesPattern(filePath, includePatterns)) { continue; } fileList.push(filePath); } } return fileList; } async function imageToBase64(filePath: string): Promise<string> { const fileData = await readFile(filePath); const base64Data = fileData.toString('base64'); const mimeType = getMimeType(filePath); return `data:${mimeType};base64,${base64Data}`; } function getMimeType(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); switch (ext) { case '.jpg': case '.jpeg': return 'image/jpeg'; case '.png': return 'image/png'; case '.gif': return 'image/gif'; case '.svg': return 'image/svg+xml'; case '.webp': return 'image/webp'; default: return 'application/octet-stream'; } } export function minifyContent(content: string): string { return content .replace(/\/\/.*$/gm, '') .replace(/\/\*[\s\S]*?\*\ .replace(/^\s*\n/gm, '') .replace(/\s+/g, ' ') .trim(); } function formatOutput(fileName: string, content: string, isImage: boolean): string { if (isImage) { return `\n\n<!-- Imagen: ${fileName} -->\n<img src="${content}" alt="${fileName}" />\n`; } else { return `\n\n<!-- Archivo: ${fileName} -->\n${content}\n`; } } export async function concatenateFiles( directoryPath: string, outputFile: string, options: ByteWeaverOptions = {}, ): Promise<ConcatenateResult> { const opts: Required<ByteWeaverOptions> = { recursive: options.recursive || false, exclude: options.exclude || [], include: options.include || [], minify: options.minify || false, outputTemplate: options.outputTemplate || null, debug: options.debug || false, header: options.header || '', footer: options.footer || '', imageMode: options.imageMode || 'base64-html', }; try { let filePaths: string[]; const outputFilePath = path.resolve(outputFile); if (opts.debug) { console.log(`🔍 Searching in: ${directoryPath}`); console.log(`🔍 Include patterns: ${opts.include.join(', ') || 'none'}`); console.log(`🔍 Exclude patterns: ${opts.exclude.join(', ') || 'none'}`); console.log(`🔍 Image mode: ${opts.imageMode}`); if (opts.header) console.log(`🔍 Adding header: ${opts.header}`); if (opts.footer) console.log(`🔍 Adding footer: ${opts.footer}`); } if (opts.recursive) { filePaths = await getFilesRecursively(directoryPath, opts.exclude, opts.include); } else { const files = await readdir(directoryPath); filePaths = []; for (const file of files) { const filePath = path.join(directoryPath, file); const stats = await stat(filePath); if (!stats.isFile()) continue; if ( matchesPattern(filePath, opts.exclude, true) || (opts.include.length > 0 && !matchesPattern(filePath, opts.include)) ) { continue; } filePaths.push(filePath); } } filePaths = filePaths.filter(filePath => path.resolve(filePath) !== outputFilePath); if (opts.debug) { console.log(`🔍 Found ${filePaths.length} files:`); } let concatenatedContent = ''; if (opts.header) { concatenatedContent += `${opts.header}\n\n`; } for (const filePath of filePaths) { if (opts.debug) { console.log(` - ${filePath}`); } const fileName = path.basename(filePath); const isImage = isImageFile(filePath); try { if (isImage) { const imageContent = await imageToBase64(filePath); concatenatedContent += formatOutput(fileName, imageContent, true); } else { const content = await readFile(filePath, 'utf8'); const fileContent = opts.minify ? minifyContent(content) : content; concatenatedContent += formatOutput(fileName, fileContent, false); } } catch (error) { console.warn(`Warning: Could not process file ${filePath}: ${(error as Error).message}`); } } if (opts.footer) { concatenatedContent += `\n\n${opts.footer}`; } if (opts.minify) { concatenatedContent = minifyContent(concatenatedContent); } if (opts.outputTemplate) { try { const templateContent = await readFile(opts.outputTemplate, 'utf8'); concatenatedContent = templateContent.replace('{{content}}', concatenatedContent); } catch (error) { throw new Error(`Error reading template file: ${(error as Error).message}`); } } await writeFile(outputFile, concatenatedContent); return { success: true, fileCount: filePaths.length, outputFile, processedFiles: filePaths, }; } catch (error) { throw new Error(`Error concatenating files: ${(error as Error).message}`); } } <!-- Archivo: types.ts --> export interface ByteWeaverOptions { recursive?: boolean; exclude?: string[]; include?: string[]; minify?: boolean; outputTemplate?: string | null; debug?: boolean; header?: string; footer?: string; imageMode?: 'base64-html' | 'base64-markdown' | 'none'; } export interface ConcatenateResult { success: boolean; fileCount: number; outputFile: string; processedFiles?: string[]; } export interface CliOptions extends ByteWeaverOptions { directoryPath: string; outputFile: string; }