// node_modules/vmvv/lib/project_files.js
const fs = require('fs');
const path = require('path');
const util = require('util');
const dotenv = require('dotenv');
const inquirer = require('inquirer');
const { get_encoding } = require('tiktoken'); // Importar la librería tiktoken
// Promisify fs methods for using async/await
const readdir = util.promisify(fs.readdir);
const stat = util.promisify(fs.stat);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const access = util.promisify(fs.access);
const appendFile = util.promisify(fs.appendFile);
const mkdir = util.promisify(fs.mkdir);
// Extensiones de archivos de imagen y video para excluir completamente
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.svg'];
const VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.mp3', '.wav', '.flac', '.aac', '.ogg'];
// La ruta principal es la raíz del proyecto
const DIRECTORY_PATH = process.cwd();
const ENV_FILE_PATH = path.join(DIRECTORY_PATH, '.env');
const OUTPUT_DIRECTORY = path.join(DIRECTORY_PATH, 'project_scanners'); // Directorio de salida
const OUTPUT_FILE_PATH_BASE = path.join(OUTPUT_DIRECTORY, 'project_files'); // Nombre base del archivo de salida
// Cargar variables de entorno usando dotenv
function loadEnvVariables(envFilePath) {
if (fs.existsSync(envFilePath)) {
dotenv.config({ path: envFilePath });
}
}
// Función para asegurar que el archivo .env exista
async function ensureEnvFile(envFilePath) {
try {
await access(envFilePath, fs.constants.F_OK);
return false; // El archivo ya existe
} catch (err) {
await writeFile(envFilePath, '', 'utf-8');
return true; // El archivo fue creado
}
}
// Función para asegurar que las variables de entorno requeridas estén establecidas
async function ensureEnvVariables(envFilePath, requiredVariables) {
let envConfig = '';
try {
envConfig = await readFile(envFilePath, 'utf-8');
} catch (err) {
// Si el archivo no existe, ya ha sido creado en ensureEnvFile
}
const envLines = envConfig.split('\n');
const existingKeys = envLines.map(line => line.split('=')[0].trim());
let newVariablesAdded = false;
for (const variable of requiredVariables) {
const [key, value] = variable.split('=');
if (key && value && !existingKeys.includes(key)) {
await appendFile(envFilePath, `${variable}\n`, 'utf-8');
process.env[key.trim()] = value.trim();
newVariablesAdded = true;
// Removed individual log messages
}
}
return newVariablesAdded;
}
// Función para convertir patrones de exclusión en expresiones regulares
function convertPatternsToRegex(patterns) {
return patterns.map(pattern => {
// Escapar caracteres especiales excepto '*'
const escaped = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&');
// Reemplazar '*' por '.*' para el regex
const regexPattern = '^' + escaped.replace(/\*/g, '.*') + '$';
return new RegExp(regexPattern, 'i'); // 'i' para insensible a mayúsculas
});
}
// Función para listar directorios en una ruta dada
async function listDirectories(basePath) {
try {
const items = await readdir(basePath, { withFileTypes: true });
const directories = items
.filter(item => item.isDirectory())
.map(dir => dir.name);
return directories;
} catch (err) {
console.error(`❌ Error listing directories: ${err.message}`);
return [];
}
}
// Función para listar archivos en una ruta dada
async function listFiles(basePath) {
try {
const items = await readdir(basePath, { withFileTypes: true });
const files = items
.filter(item => item.isFile())
.map(file => file.name);
return files;
} catch (err) {
console.error(`❌ Error listing files: ${err.message}`);
return [];
}
}
// Función para preguntar el tipo de acción en el directorio actual
async function askAction(currentPath) {
const actions = [
{ name: '📄 Scan and generate JSON', value: 'json' },
{ name: '📃 Scan and generate TXT', value: 'txt' },
{ name: '📁 Enter a subdirectory', value: 'enter' },
{ name: '🔙 Go back', value: 'back' },
{ name: '🚪 Exit', value: 'exit' }
];
const actionQuestion = [
{
type: 'list',
name: 'action',
message: `🔍 What would you like to do in "${path.basename(currentPath) || 'root'}"?`,
choices: actions
}
];
const { action } = await inquirer.prompt(actionQuestion);
return action;
}
// Función asíncrona para leer directorios
async function readDirectory(currentPath, basePath, included, excludedFilesRegex, excludedDirsList, filesContent) {
let items;
try {
items = await readdir(currentPath, { withFileTypes: true });
} catch (err) {
console.error(`❌ Error reading directory ${currentPath}: ${err.message}`);
return [];
}
const contents = [];
for (const item of items) {
const fullPath = path.join(currentPath, item.name);
const relativePath = path.relative(basePath, fullPath);
let itemStat;
// Manejar enlaces simbólicos para prevenir recursión infinita
try {
itemStat = await stat(fullPath);
} catch (err) {
console.error(`❌ Error stating path ${fullPath}: ${err.message}`);
continue;
}
const ext = path.extname(item.name).toLowerCase();
if (itemStat.isSymbolicLink()) {
// Salta enlaces simbólicos
continue;
}
if (itemStat.isDirectory()) {
if (!process.env.EXCLUDE_DIRS.split(',').map(dir => dir.trim()).includes(item.name)) {
included.push({ path: relativePath + '/' });
const subDir = await readDirectory(
fullPath,
basePath,
included,
excludedFilesRegex,
excludedDirsList,
filesContent
);
contents.push({
path: relativePath + '/',
contents: subDir
});
} else {
excludedDirsList.push(relativePath + '/');
}
} else if (itemStat.isFile()) {
// Verificar si el archivo es una imagen o video y omitirlo completamente
if (IMAGE_EXTENSIONS.includes(ext) || VIDEO_EXTENSIONS.includes(ext)) {
continue; // Omitir este archivo sin añadirlo a ninguna lista
}
// Verificar si el archivo coincide con algún patrón de exclusión
const isExcluded = excludedFilesRegex.some(regex => regex.test(item.name));
if (!isExcluded) {
try {
let fileContent = await readFile(fullPath, 'utf-8');
fileContent = fileContent.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ');
contents.push({
path: relativePath
});
filesContent.push({
path: relativePath,
content: fileContent
});
included.push({ path: relativePath });
} catch (err) {
console.error(`❌ Error reading file ${relativePath}: ${err.message}`);
}
} else {
// Excluir el archivo
// Puedes optar por registrar estos archivos si lo deseas
// excludedFilesList.push(relativePath); // Mostrar ruta relativa completa
}
}
}
return contents;
}
// Función para envolver la lectura de directorios
async function readDirectoryWrapper(targetPath, excludedFilesRegex) {
const included = [];
const excludedFiles = [];
const excludedDirs = [];
const filesContent = [];
const structure = await readDirectory(
targetPath,
targetPath,
included,
excludedFilesRegex,
excludedDirs,
filesContent
);
return {
details: [
{ this_content: "This JSON contains the structure and content of a Node.js project, which needs to be analyzed,project_description: provides a general overview of the project,main_technologies: lists the primary technologies used,exclusion_note: offers information regarding any exclusions applied." },
{ project_description: process.env.PROJECT_DESCRIPTION },
{ main_technologies: process.env.MAIN_TECHNOLOGIES },
{ exclusion_note: "The directories and files listed in excluded are existing files that have been excluded because their content is not relevant." }
],
structure: structure, // Estructura protegida con archivos y directorios incluidos en formato de árbol
excluded: {
directories: excludedDirs, // Solo rutas
files: excludedFiles // Rutas relativas completas
},
filesContent: filesContent // Solo archivos incluidos con su contenido
};
}
// Función para contar tokens en una cadena de texto
function contarTokens(texto) {
// Obtener el codificador para el modelo (ajusta el nombre del encoding según tus necesidades)
const enc = get_encoding('cl100k_base'); // Puedes usar 'gpt2' u otro encoding compatible
// Codificar el texto para obtener los tokens
const tokens = enc.encode(texto);
// Liberar el encodificador después de usarlo
enc.free();
return tokens.length;
}
// Función para generar el archivo de salida
async function generateFile(targetPath, fileType, directoryName = null) {
try {
// Convertir los patrones de exclusión en expresiones regulares
const excludePatterns = process.env.EXCLUDE_FILES.split(',').map(pattern => pattern.trim()).filter(pattern => pattern !== '');
const excludedFilesRegex = convertPatternsToRegex(excludePatterns);
const result = await readDirectoryWrapper(targetPath, excludedFilesRegex);
let outputFilePathWithExtension;
let fileContent; // Declarar aquí para poder usarlo después
if (directoryName) {
// Reemplazar caracteres no permitidos en nombres de archivos
const safeDirName = directoryName.replace(/[<>:"/\\|?*]+/g, '_');
// Si se está escaneando un directorio específico, incluir su nombre en el archivo de salida
outputFilePathWithExtension = `${path.join(OUTPUT_DIRECTORY, `project_files_${safeDirName}`)}.${fileType}`;
} else {
// De lo contrario, usar el nombre base del archivo de salida
outputFilePathWithExtension = `${OUTPUT_FILE_PATH_BASE}.${fileType}`;
}
if (fileType === 'json') {
fileContent = JSON.stringify(result, null, 2);
await writeFile(outputFilePathWithExtension, fileContent, 'utf-8');
} else if (fileType === 'txt') {
fileContent = `📄 Details:\nProject Description: ${process.env.PROJECT_DESCRIPTION}\nMain Technologies: ${process.env.MAIN_TECHNOLOGIES}\n`;
fileContent += `📝 Exclusion Note: The directories and files listed in excluded are existing files that have been excluded because their content is not relevant.\n`;
fileContent += `\n📁 Structure:\n${formatStructure(result.structure)}`;
fileContent += `\n\n📂 Excluded Directories:\n${result.excluded.directories.join('\n')}`;
// fileContent += `\n\n📄 Excluded Files:\n${result.excluded.files.join('\n')}`; // Opcional: Mostrar archivos excluidos
fileContent += `\n\n🗂️ Files Content:\n${result.filesContent.map(item => `Path: ${item.path}\nContent: ${item.content}`).join('\n\n')}`;
await writeFile(outputFilePathWithExtension, fileContent, 'utf-8');
}
// Contar los tokens del contenido generado
const tokenCount = contarTokens(fileContent);
console.log(`✅ All set! Your file is located at: ${outputFilePathWithExtension}`);
console.log(`🧮 Number of tokens in the generated file: ${tokenCount}`);
} catch (error) {
console.error(`❌ Error generating the file: ${error.message}`);
}
}
// Función para formatear la estructura de directorios en texto
function formatStructure(structure, indent = 0) {
let result = '';
const indentation = ' '.repeat(indent);
for (const item of structure) {
result += `${indentation}📂 Path: ${item.path}\n`;
if (item.contents && item.contents.length > 0) {
result += formatStructure(item.contents, indent + 1);
}
}
return result;
}
// Función para preguntar el tipo de escaneo y generar el archivo
async function askScanTypeAndGenerate(currentPath, actionType) {
const scanType = actionType; // Ya sabemos el tipo de escaneo
if (scanType === 'json' || scanType === 'txt') {
await generateFile(currentPath, scanType, path.basename(currentPath));
// Finalizar el programa después de generar el archivo
console.log("👋 Goodbye! Exiting the scanner.");
process.exit(0);
}
}
// Función principal para ejecutar el escáner
async function execute(currentPath = DIRECTORY_PATH, history = []) {
try {
const userAction = await askAction(currentPath);
switch (userAction) {
case 'json':
case 'txt':
await askScanTypeAndGenerate(currentPath, userAction);
// El programa finalizará dentro de askScanTypeAndGenerate
break;
case 'enter':
const directories = await listDirectories(currentPath);
if (directories.length === 0) {
// Si no hay directorios, mostrar opciones "Go back" y "Exit"
const noDirChoices = [
{ name: '🔙 Go back', value: 'back' },
{ name: '🚪 Exit', value: 'exit' }
];
const noDirQuestion = [
{
type: 'list',
name: 'noDirAction',
message: '❗ No directories found to enter. Choose an action:',
choices: noDirChoices
}
];
const { noDirAction } = await inquirer.prompt(noDirQuestion);
if (noDirAction === 'back') {
if (history.length > 0) {
const previousPath = history.pop();
await execute(previousPath, history);
} else {
console.log("❗ You are at the root directory.");
await execute(currentPath, history);
}
} else if (noDirAction === 'exit') {
console.log("👋 Goodbye! Exiting the scanner.");
process.exit(0);
}
} else {
const directoryChoices = [
...directories.map(dir => ({ name: dir, value: dir })),
new inquirer.Separator(),
{ name: '🔙 Go back', value: 'back' },
{ name: '🚪 Exit', value: 'exit' }
];
const directoryQuestion = [
{
type: 'list',
name: 'selectedDir',
message: '📂 Select a directory to enter:',
choices: directoryChoices
}
];
const { selectedDir } = await inquirer.prompt(directoryQuestion);
if (selectedDir === 'back') {
if (history.length > 0) {
const previousPath = history.pop();
await execute(previousPath, history);
} else {
console.log("❗ You are at the root directory.");
await execute(currentPath, history);
}
} else if (selectedDir === 'exit') {
console.log("👋 Goodbye! Exiting the scanner.");
process.exit(0);
} else {
const newPath = path.join(currentPath, selectedDir);
history.push(currentPath); // Guardar la ruta actual para poder volver
await execute(newPath, history);
}
}
break;
case 'back':
if (history.length > 0) {
const previousPath = history.pop();
await execute(previousPath, history);
} else {
console.log("❗ You are at the root directory.");
await execute(currentPath, history);
}
break;
case 'exit':
console.log("👋 Goodbye! Exiting the scanner.");
process.exit(0);
break;
default:
console.log("❗ Invalid option selected.");
await execute(currentPath, history);
}
} catch (error) {
console.error(`❌ Error: ${error.message}`);
}
}
// Función para asegurar que el directorio de salida exista
async function ensureOutputDirectory(outputDirPath) {
try {
await access(outputDirPath, fs.constants.F_OK);
// El directorio ya existe
} catch (err) {
// El directorio no existe, crearlo
try {
await mkdir(outputDirPath, { recursive: true });
console.log(`✅ Created output directory at: ${outputDirPath}`);
} catch (mkdirErr) {
console.error(`❌ Error creating output directory: ${mkdirErr.message}`);
process.exit(1);
}
}
}
// Asegurar la creación del archivo .env y las variables necesarias
(async () => {
try {
// Verificar si se creó el archivo .env
const envFileCreated = await ensureEnvFile(ENV_FILE_PATH);
const REQUIRED_VARIABLES = [
'EXCLUDE_DIRS=node_modules,.git,.vscode,dist,build,.nuxt,public,project_scanners',
'EXCLUDE_FILES=package-lock.json,yarn.lock,.env,.env.example,project_files.json,project_files.txt,project_files_*.json,project_files_*.txt,*.json,*.db,*.db-journal,*.txt,.DS_Store',
'PROJECT_DESCRIPTION=This is a project description',
'MAIN_TECHNOLOGIES=node.js'
];
// Asegurar las variables necesarias en el archivo .env
const variablesAdded = await ensureEnvVariables(ENV_FILE_PATH, REQUIRED_VARIABLES);
// Cargar las variables del archivo .env
loadEnvVariables(ENV_FILE_PATH);
// Asegurar que el directorio de salida exista
await ensureOutputDirectory(OUTPUT_DIRECTORY);
// Mostrar un único mensaje si el archivo .env fue creado o modificado
if (envFileCreated || variablesAdded) {
console.log("✅ The .env file has been created or updated with necessary variables to manage the scanner.");
}
await execute(); // Iniciar el escáner en la ruta raíz
} catch (error) {
console.error(`❌ Error: ${error.message}`);
}
})();