cpsf_utilite
Version:
A CLI tool to generate a project's directory and file structure tree, with advanced filtering and configuration.
894 lines (826 loc) • 27.2 kB
JavaScript
const fs = require("fs");
const path = require("path");
const os = require("os");
const { exec } = require("child_process");
const inquirer = require("inquirer");
let ora;
try {
ora = require("ora");
} catch (e) {
ora = null;
}
let open;
try {
open = require("open");
} catch (e) {
open = null;
}
// --- ДЕФОЛТНІ НАЛАШТУВАННЯ ---
// Ці налаштування будуть використовуватися, якщо користувач не створить свій .structurerc.json
const defaultConfig = {
// Універсальний список файлів, які завжди включаються, навіть якщо вони в інших списках виключень
exceptions: [
"cmd.sh",
"path.sh",
"run_adaptation.sh",
"run_create_speaker_profile.sh",
"run_finetune.sh",
],
// Налаштування для скорочення виводу великих директорій
largeDirectorySummary: {
enabled: true, // Чи увімкнена функція
threshold: 10, // Вважати директорію великою, якщо в ній більше N файлів
first: 3, // Скільки перших файлів показувати
last: 3 // Скільки останніх файлів показувати
},
// Директорії, які показуються тільки назвою, без розгортання вмісту
collapsedDirectories: [
// --- Залежності та віртуальні середовища ---
"node_modules", // Стандарт для Node.js, величезна кількість файлів.
"venv", // Стандарт для віртуальних середовищ Python.
".venv", // Інший поширений варіант назви для venv.
"vendor", // Часто використовується для залежностей Go, PHP (Composer), Ruby.
"bower_components", // Застарілий, але ще зустрічається у старих front-end проєктах.
// --- Кеш та артефакти збірки ---
".cache",
"cache", // Загальний кеш для різних інструментів (pip, pytest).
".parcel-cache", // Специфічний кеш для збирача Parcel.
".next", // Кеш та артефакти збірки для Next.js.
".nuxt", // Кеш та артефакти збірки для Nuxt.js.
".svelte-kit", // Кеш та артефакти збірки для SvelteKit.
"__pycache__", // Кешовані байт-код файли Python.
"dist", // Стандартна назва для дистрибутивів (зібраний код).
"build", // Інша поширена назва для зібраних файлів.
"out", // Ще один варіант для результатів збірки.
"target", // Стандартна папка для результатів збірки в Java/Rust проєктах.
// --- Тимчасові, генеровані та лог-файли ---
"tmp", // Загальна папка для тимчасових файлів.
"temp", // Інший варіант назви для тимчасових файлів.
"logs", // Стандартна папка для логів.
"coverage", // Звіти про покриття коду тестами.
".pytest_cache", // Кеш для фреймворку pytest.
".mypy_cache", // Кеш для статичного аналізатора mypy.
// --- Специфічні для проєкту (Kaldi/TTS) ---
// Ці папки містять тимчасові або проміжні результати,
// які генеруються скриптами і не є вихідним кодом.
"exp", // Стандартна папка для експериментів у Kaldi. Її вміст дуже динамічний.
"mfcc", // Зазвичай тут зберігаються вираховані MFCC-фічі.
"data/lang/tmp", // Тимчасова папка, що створюється `prepare_lang.sh`.
"data/local", // Часто використовується для проміжних файлів підготовки даних.
"kaldi_setup/data/lang_input_for_prepare_lang", // Судячи з назви, це проміжна папка.
// --- Інше ---
"eggs", // Папки, пов'язані з Python eggs (застарілий формат).
".eggs",
".idea", // Налаштування для IDE від JetBrains (IntelliJ IDEA, PyCharm).
".vscode" // Налаштування для VS Code.
],
// Директорії, де копіюється лише структура папок без вмісту файлів
onlyStructureDirectories: [
"model",
"model-adapt",
"model_base",
"public",
"assets",
"components",
"config",
"controllers",
"data",
"database",
"doc",
"docs",
"downloads",
"different_models",
"fonts",
"icons",
"images",
"examples",
"exp",
"features",
"helpers",
"videos",
"am",
"conf",
"graph",
"ivector",
"vosk-api",
"tmp_kaldi_download",
"spk_models",
"spk_models_adapt",
"steps",
"tools",
"utils",
"whisper_models",
"wespeaker-1.2.0",
"pretrained_model",
],
// Директорії, які повністю ігноруються
excludedDirectories: [
".circleci",
".git",
".gitignore",
".github",
".gitlab",
".nyc_output",
".vs",
".vscode-server",
".vscode-server-insiders",
".vscode-server-oss",
".vscode-server-oss-dev",
".vscode-test",
".vscode-test-remote",
"vscodeignore",
"_metadata",
"backup",
"backups",
"bin",
"include",
"lib",
"obj",
"output",
"private",
"project_copies",
"spec",
"specs",
"test",
"testing",
"tests",
"uploads",
],
// Файли, які повністю ігноруються за іменем
excludedFiles: [
".DS_Store",
".dockerignore",
".eslintignore",
".eslintrc.json",
".gitignore",
".jshintrc",
".mocharc.js",
".npmrc",
".prettierignore",
".prettierrc",
".stylelintrc",
"AUTHORS",
"CODE_OF_CONDUCT.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"Dockerfile",
"ISSUE_TEMPLATE.md",
"Jenkinsfile",
"LICENSE",
"MANIFEST.in",
"MicrosoftWebDriver",
"MicrosoftWebDriver.app",
"MicrosoftWebDriver.bat",
"MicrosoftWebDriver.class",
"MicrosoftWebDriver.command",
"MicrosoftWebDriver.deb",
"MicrosoftWebDriver.dll",
"MicrosoftWebDriver.dmg",
"MicrosoftWebDriver.framework",
"MicrosoftWebDriver.kext",
"MicrosoftWebDriver.msi",
"MicrosoftWebDriver.pkg",
"MicrosoftWebDriver.ps1",
"MicrosoftWebDriver.rpm",
"MicrosoftWebDriver.run",
"MicrosoftWebDriver.sh",
"MicrosoftWebDriver.so",
"MicrosoftWebDriver.sys",
"MicrosoftWebDriver.vbs",
"PULL_REQUEST_TEMPLATE.md",
"Pipfile",
"chromedriver",
"edgedriver",
"geckodriver",
"iedriver",
"karma.conf.js",
"msedgedriver",
"operadriver",
"setup.py",
"safaridriver",
"vite.config.js",
"vsc-extension-quickstart.md",
"webpack.config.js",
],
// Файли, які ігноруються за розширенням
excludedExtensions: [
".accdb",
".bak",
".backup",
".cfg",
".cjs",
".config",
".csv",
".env",
".env.ci",
".env.ci.local",
".env.development",
".env.development.local",
".env.local",
".env.production",
".env.production.local",
".env.test",
".env.test.local",
".flow",
".flow-typed",
".flow-typed.conf",
".flow-typed.config",
".flow-typed.ini",
".flow-typed.json",
".flow-typed.properties",
".flow-typed.toml",
".flow-typed.yaml",
".flow-typed.yml",
".flowconfig",
".ini",
".lock",
".log",
".map",
".mdb",
".min.css",
".min.js",
".mjs",
".new",
".old",
".options",
".orig",
".pid",
".pipe",
".prefs",
".properties",
".schema",
".schema.cfg",
".schema.conf",
".schema.config",
".schema.env",
".schema.env.ci",
".schema.env.ci.local",
".schema.env.development",
".schema.env.development.local",
".schema.env.local",
".schema.env.production",
".schema.env.production.local",
".schema.env.test",
".schema.env.test.local",
".schema.ini",
".schema.json",
".schema.prefs",
".schema.properties",
".schema.settings",
".schema.toml",
".schema.yaml",
".schema.yml",
".settings",
".sqlite",
".sqlite3",
".sql",
".swn",
".swo",
".swp",
".temp",
".tmp",
".toml",
".vsix",
".vsixaddon",
".vsixapp",
".vsixbundle",
".vsixextension",
".vsixext",
".vsixmacro",
".vsixmanifest",
".vsixmod",
".vsixpackage",
".vsixpkg",
".vsixplugin",
".vsixscript",
".vsixskin",
".vsixsnippet",
".vsixstyle",
".vsixtemplate",
".vsixtheme",
".xml",
".yaml",
".yml",
],
// Файли та розширення, для яких показується лише ім'я, без вмісту
filesWithoutContent: [
".DS_Store",
".ark",
".bin",
".scp",
".sh",
".ckpt",
".conf",
".db",
".dbf",
".dbx",
".eslintignore",
".eslintrc.cjs",
".eslintrc.json",
".env",
".gitignore",
".h5",
".jshintrc",
".keras",
".mocharc.js",
".mat",
".model",
".npmrc",
".mdl",
".npy",
".1",
".pb",
".pkl",
".pl",
".pt",
".prettierignore",
".prettierrc",
".pth",
".stylelintrc",
".yarnrc",
"Jenkinsfile",
"LICENSE",
"__init__.py",
"babel.config.js",
"jest.config.js",
"karma.conf.js",
"package-lock.json",
"rollup.config.js",
"vite.config.js",
"vsc-extension-quickstart.md",
".vec",
".vocab",
"text",
".safetensors",
"utt2dur",
"utt2spk",
"utt2uniq",
"utt2num_frames",
".int",
"COPYING",
".spm",
],
// Розширення медіа, архівів та документів, які не мають текстового вмісту
mediaExtensions: [
".appcache",
".manifest",
".webmanifest",
".vtt",
".webapp",
// Архіви
".7z",
".apk",
".bz2",
".gz",
".iso",
".jar",
".ova",
".ovf",
".qcow",
".qcow2",
".rar",
".tar",
".tar.bz2",
".tar.gz",
".tar.xz",
".tar.zst",
".tbz2",
".tgz",
".txz",
".tzst",
".war",
".xz",
".zip",
".zst",
// Відео
".3g2",
".3gp",
".amv",
".asf",
".avi",
".drc",
".f4a",
".f4b",
".f4p",
".f4v",
".flv",
".gifv",
".m2v",
".m4p",
".m4v",
".mkv",
".mov",
".mp2",
".mp4",
".mpg",
".mpeg",
".mpe",
".mpv",
".mng",
".mxf",
".nsv",
".ogv",
".ogg",
".qt",
".rm",
".rmvb",
".roq",
".svi",
".swf",
".vob",
".webm",
".wmv",
".yuv",
// Зображення
".ai",
".apng",
".bmp",
".cur",
".eps",
".gif",
".ico",
".icon",
".icloud",
".j2c",
".j2k",
".jif",
".jfif",
".jng",
".jpeg",
".jpg",
".heic",
".heif",
".jpe",
".jp2",
".jps",
".jxl",
".jxr",
".m2ts",
".m4v",
".mj2",
".mjp2",
".jpm",
".pbm",
".pcx",
".pgf",
".pic",
".pct",
".pict",
".pjp",
".pjpeg",
".png",
".psd",
".raw",
".rgb",
".svg",
".svgz",
".tif",
".tiff",
".wbmp",
".webp",
".xif",
".xpm",
".xwd",
// Аудіо
".aac",
".aiff",
".alac",
".amr",
".ape",
".au",
".flac",
".gsm",
".it",
".m3u",
".m4a",
".mid",
".mod",
".mp3",
".mpa",
".mpc",
".mtm",
".oga",
".ogg",
".opus",
".s3m",
".sid",
".spx",
".tta",
".umx",
".wav",
".weba",
".wma",
".xm",
// Документи
".csv",
".doc",
".docx",
".dot",
".dotx",
".odp",
".ods",
".odt",
".pdf",
".pps",
".ppsx",
".ppt",
".pptx",
".rtf",
".tex",
".tsv",
".txt",
".wpd",
".wps",
".xls",
".xlsm",
".xlsx",
".xml",
".xps",
".json",
],
// Патерни для пошуку секретів у файлах (використовують регулярні вирази)
secretsToHide: [
"REACT_APP_UNSPLASH_ACCESS_KEY=.*",
"API_KEY=.*",
"SECRET_TOKEN=.*",
"DATABASE_URL=.*",
"PASSWORD=.*",
],
};
// --- ЗАВАНТАЖЕННЯ КОНФІГУРАЦІЇ ---
function loadConfig() {
const globalConfigPath = path.join(os.homedir(), '.structurerc.json');
const localConfigPath = path.join(process.cwd(), '.structurerc.json');
let finalConfig = { ...defaultConfig };
// Функція для злиття конфігурацій
const mergeConfigs = (base, custom) => {
let merged = { ...base };
for (const key in custom) {
if (Array.isArray(merged[key]) && Array.isArray(custom[key])) {
merged[key] = [...new Set([...merged[key], ...custom[key]])];
} else if (typeof merged[key] === 'object' && merged[key] !== null && !Array.isArray(merged[key])) {
merged[key] = mergeConfigs(merged[key], custom[key]);
} else {
merged[key] = custom[key];
}
}
return merged;
};
// 1. Завантажуємо глобальну конфігурацію
if (fs.existsSync(globalConfigPath)) {
try {
const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
finalConfig = mergeConfigs(finalConfig, globalConfig);
} catch (e) {
console.error("Помилка читання глобального .structurerc.json. Будуть використані налаштування за замовчуванням.");
}
}
// 2. Завантажуємо локальну конфігурацію (вона має вищий пріоритет)
if (fs.existsSync(localConfigPath)) {
try {
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
finalConfig = mergeConfigs(finalConfig, localConfig);
console.log("Завантажено та застосовано локальну конфігурацію з .structurerc.json");
} catch (e) {
console.error("Помилка читання локального .structurerc.json.");
}
}
return finalConfig;
}
// Завантажуємо конфіг один раз на старті
const config = loadConfig();
// --- ІНІЦІАЛІЗАЦІЯ ---
async function runInit() {
const globalConfigPath = path.join(os.homedir(), '.structurerc.json');
console.log("Налаштування глобальної конфігурації...");
if (!fs.existsSync(globalConfigPath)) {
fs.writeFileSync(globalConfigPath, JSON.stringify(defaultConfig, null, 2));
console.log(`✅ Створено глобальний файл конфігурації: ${globalConfigPath}`);
} else {
console.log(`ℹ️ Глобальний файл конфігурації вже існує: ${globalConfigPath}`);
}
console.log("Відкриття файлу конфігурації для редагування...");
try {
if (open) {
await open(globalConfigPath);
} else {
console.log("Пакет 'open' не встановлено. Будь ласка, відкрийте файл вручну.");
}
} catch (err) {
console.error(`Не вдалося автоматично відкрити файл. Будь ласка, відкрийте його вручну:\n${globalConfigPath}`);
}
}
// --- ДОПОМІЖНІ ФУНКЦІЇ ---
function isException(entryName) {
return config.exceptions.includes(entryName);
}
function isTextFile(fileName) {
const textExtensions = ['.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.html', '.css', '.scss', '.py', '.sh', '.rb', '.php', '.txt', '.csv', '.yml', '.yaml', '.ini', '.env', '.log', '.rs', '.toml']; // <-- Додано .rs та .toml
const textFilesByName = ['Dockerfile', 'Makefile', 'Jenkinsfile', 'README', 'LICENSE'];
const ext = path.extname(fileName).toLowerCase();
return textExtensions.includes(ext) || textFilesByName.includes(fileName);
}
function ensureDirectoryExists(directory) {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
}
// --- ФУНКЦІЯ ГЕНЕРАЦІЇ ДЕРЕВА СТРУКТУРИ (ОНОВЛЕНА ВЕРСІЯ) ---
function generateStructureTree(dirPath, prefix = "", onlyStructureMode = false, isRoot = true) {
let entries;
try {
entries = fs.readdirSync(dirPath, { withFileTypes: true });
} catch (error) {
return `\n${prefix}└── [Не вдалося прочитати директорію: ${path.basename(dirPath)}]`;
}
let structure = isRoot ? "." : "";
const dirName = path.basename(dirPath);
const currentOnlyStructureMode = onlyStructureMode || config.onlyStructureDirectories.includes(dirName);
const visibleEntries = entries.filter(entry => {
if (isException(entry.name)) return true;
if (entry.isDirectory()) {
return !config.excludedDirectories.includes(entry.name);
} else {
const fileExtension = path.extname(entry.name).toLowerCase();
return !config.excludedFiles.includes(entry.name) && !config.excludedExtensions.includes(fileExtension);
}
});
// Розділяємо на папки та файли
const subdirectories = visibleEntries.filter(e => e.isDirectory());
const filesInDir = visibleEntries.filter(e => !e.isDirectory());
// Сортуємо
subdirectories.sort((a, b) => a.name.localeCompare(b.name));
filesInDir.sort((a, b) => a.name.localeCompare(b.name));
let filesToRender = filesInDir;
let summaryMarker = null;
// --- ЗМІНА: НОВА, НАДІЙНА ЛОГІКА СКОРОЧЕННЯ ---
const summaryConfig = config.largeDirectorySummary;
let useSummary = false;
if (!isRoot && summaryConfig.enabled && filesInDir.length > summaryConfig.threshold) {
// Скорочуємо, якщо папка явно позначена для цього
if (currentOnlyStructureMode) {
useSummary = true;
} else {
// Або якщо більшість файлів не є текстовими
if (filesInDir.length > 0) {
let nonTextFileCount = 0;
let textFileExamples = [];
for (const file of filesInDir) {
// Використовуємо вашу ж функцію isTextFile, щоб визначити, чи є у файлу потенційний вміст.
// Ми також перевіряємо, чи не є він винятком (exceptions), бо їх вміст показується.
if (!isTextFile(file.name) || !isException(file.name)) {
nonTextFileCount++;
} else {
if (textFileExamples.length < 5) { // Збираємо приклади для діагностики
textFileExamples.push(file.name);
}
}
}
const nonTextRatio = nonTextFileCount / filesInDir.length;
if (nonTextRatio > 0.9) { // Якщо понад 90% файлів не є текстовими, скорочуємо.
useSummary = true;
}
// Діагностичний вивід, якщо скорочення не спрацювало
else if (dirName === 'user_audio_library') { // Тільки для проблемної папки
console.log(`[DEBUG] Скорочення для '${dirName}' не застосовано. Співвідношення нетекстових файлів: ${nonTextRatio.toFixed(2)} (потрібно > 0.9)`);
console.log(`[DEBUG] Знайдені текстові файли:`, textFileExamples);
}
}
}
if (useSummary) {
const firstPart = filesInDir.slice(0, summaryConfig.first);
const lastPart = filesInDir.slice(-summaryConfig.last);
const hiddenCount = filesInDir.length - firstPart.length - lastPart.length;
if (hiddenCount > 0) {
summaryMarker = {
name: `... (і ще ${hiddenCount} файлів) ...`,
isSummaryMarker: true
};
filesToRender = [...firstPart, summaryMarker, ...lastPart];
}
}
}
const entriesToRender = [...subdirectories, ...filesToRender];
entriesToRender.forEach((entry, idx) => {
const isLast = idx === entriesToRender.length - 1;
const connector = isLast ? "└── " : "├── ";
const nextPrefix = prefix + (isLast ? " " : "│ ");
if (entry.isSummaryMarker) {
structure += `\n${prefix}${connector}${entry.name}`;
return;
}
const entryPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
// --- ЗМІНА: ПЕРЕВІРКА НА "ЗГОРНУТІ" ДИРЕКТОРІЇ ---
// Якщо папка є у списку collapsedDirectories, просто показуємо її назву і не йдемо в рекурсію.
if (config.collapsedDirectories.includes(entry.name)) {
structure += `\n${prefix}${connector}${entry.name}/`; // Показуємо назву і все
} else {
// Інакше продовжуємо як раніше
structure += `\n${prefix}${connector}${entry.name}/`;
structure += generateStructureTree(entryPath, nextPrefix, currentOnlyStructureMode, false);
}
} else { // Це файл
structure += `\n${prefix}${connector}${entry.name}`;
const fileExtension = path.extname(entry.name).toLowerCase();
const hasContent = !config.filesWithoutContent.includes(entry.name) &&
!config.filesWithoutContent.includes(fileExtension) &&
!config.mediaExtensions.includes(fileExtension);
if (!currentOnlyStructureMode && (hasContent || isException(entry.name))) {
try {
if (isTextFile(entry.name)) {
let fileContent = fs.readFileSync(entryPath, "utf8");
// --- Опціональне покращення: пропускаємо коментарі та порожні рядки ---
const fileLines = fileContent.split("\n");
let contentShown = false;
fileLines.forEach(line => {
const trimmedLine = line.trim();
if (trimmedLine === '' || trimmedLine.startsWith('#')) {
return; // Пропускаємо порожні рядки та рядки-коментарі
}
structure += `\n${nextPrefix}${line}`;
contentShown = true;
});
// Якщо після фільтрації вмісту не залишилося, можна додати позначку
if (!contentShown && fileLines.length > 0) {
structure += `\n${nextPrefix}[лише коментарі/порожні рядки]`;
}
}
} catch (e) {
structure += `\n${nextPrefix}[Не вдалося прочитати файл]`;
}
}
}
});
return structure;
}
// --- ОСНОВНІ КОМАНДИ ---
function getUniqueFilename(basePath, filename) {
const now = new Date();
const date = now.toISOString().split('T')[0]; // YYYY-MM-DD
const time = now.toTimeString().split(' ')[0].replace(/:/g, "-"); // HH-MM-SS
return path.join(basePath, `${filename}_D_${date}_H_${time}.txt`);
}
function createProjectStructureFile(basePath, type = "structure") {
const spinner = ora ? ora('Аналіз структури проекту...').start() : null;
try {
const structure = generateStructureTree(basePath);
if (spinner) {
spinner.text = 'Збереження файлу...';
}
const projectCopiesDir = path.join(basePath, "project_copies");
ensureDirectoryExists(projectCopiesDir);
const filenamePrefix = type === "structure" ? "project-structure" : "archive";
const uniqueFilename = getUniqueFilename(projectCopiesDir, filenamePrefix);
fs.writeFileSync(uniqueFilename, structure);
if (spinner) {
spinner.succeed(`Файл успішно створено: ${path.basename(uniqueFilename)}`);
} else {
console.log(`Файл успішно створено: ${path.basename(uniqueFilename)}`);
}
} catch (error) {
if (spinner) {
spinner.fail('Під час генерації файлу сталася помилка.');
} else {
console.error('Під час генерації файлу сталася помилка.');
}
console.error(error);
}
}
// --- ГОЛОВНА ЛОГІКА ЗАПУСКУ (ОНОВЛЕНО) ---
(async () => {
const args = process.argv.slice(2);
const command = args[0];
const basePath = ".";
if (command === 'init') {
await runInit();
return;
}
if (command === "structure" || command === "archive") {
createProjectStructureFile(basePath, command);
} else if (args.length > 0) {
console.log("Невідома команда. Доступні команди: 'init', 'structure', 'archive'.");
} else {
try {
const answers = await inquirer.prompt([
{
type: "list",
name: "operation",
message: "Оберіть операцію:",
choices: [
{ name: "Створити файл структури проекту", value: "structure" },
{ name: "Налаштувати глобальну конфігурацію", value: "init" },
],
},
]);
if (answers.operation === 'init') {
await runInit();
} else {
createProjectStructureFile(basePath, answers.operation);
}
} catch (error) {
console.error("\nСталася помилка під час роботи інтерактивного режиму.", error);
}
}
})();