fig-folder
Version:
A CLI tool to organize configuration files into a fig/ folder with safe mode option
280 lines (249 loc) • 9.8 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import chalk from 'chalk';
// Configuration file categories
const FILE_CATEGORIES = {
// Linting
'eslint.config.mjs': 'Linting',
'eslint.config.js': 'Linting',
'.eslintrc': 'Linting',
'.eslintrc.js': 'Linting',
'.eslintrc.json': 'Linting',
'.eslintrc.yaml': 'Linting',
'.eslintrc.yml': 'Linting',
// Formatting
'prettier.config.mjs': 'Formatting',
'prettier.config.js': 'Formatting',
'.prettierrc': 'Formatting',
'.prettierrc.js': 'Formatting',
'.prettierrc.json': 'Formatting',
'.prettierrc.yaml': 'Formatting',
'.prettierrc.yml': 'Formatting',
// TypeScript
'tsconfig.json': 'TypeScript',
'tsconfig.base.json': 'TypeScript',
// Build Tools
'next.config.mjs': 'Build Tools',
'next.config.js': 'Build Tools',
'vite.config.ts': 'Build Tools',
'vite.config.js': 'Build Tools',
'webpack.config.js': 'Build Tools',
'rollup.config.js': 'Build Tools',
'parcel.config.js': 'Build Tools',
'tailwind.config.js': 'Build Tools',
'tailwind.config.ts': 'Build Tools',
'postcss.config.js': 'Build Tools',
'postcss.config.mjs': 'Build Tools',
// Environment
'.env': 'Environment',
'.env.local': 'Environment',
'.env.development': 'Environment',
'.env.production': 'Environment',
'.env.test': 'Environment',
// Package Managers
'package-lock.json': 'Package Managers',
'yarn.lock': 'Package Managers',
'pnpm-lock.yaml': 'Package Managers',
'bun.lockb': 'Package Managers',
// Git
'.gitignore': 'Git',
'.gitattributes': 'Git',
// Editor
'.editorconfig': 'Editor',
'.vscode/settings.json': 'Editor',
// Testing
'jest.config.js': 'Testing',
'jest.config.ts': 'Testing',
'vitest.config.ts': 'Testing',
'vitest.config.js': 'Testing',
'cypress.config.js': 'Testing',
'cypress.config.ts': 'Testing',
// Documentation
'README.md': 'Documentation',
'CHANGELOG.md': 'Documentation',
'LICENSE': 'Documentation',
'CONTRIBUTING.md': 'Documentation'
};
// Files that are commonly imported and should NOT be moved (only symlinked)
const COMMONLY_IMPORTED_FILES = [
'tailwind.config.js',
'tailwind.config.ts',
'postcss.config.js',
'postcss.config.mjs',
'next.config.js',
'next.config.mjs',
'vite.config.js',
'vite.config.ts',
'webpack.config.js',
'rollup.config.js',
'parcel.config.js',
'jest.config.js',
'jest.config.ts',
'vitest.config.js',
'vitest.config.ts',
'cypress.config.js',
'cypress.config.ts',
'prettier.config.js',
'prettier.config.mjs',
'eslint.config.js',
'eslint.config.mjs',
'tsconfig.json',
'tsconfig.base.json'
];
// Files to organize
const CONFIG_FILES = Object.keys(FILE_CATEGORIES);
export async function organize(safe = false) {
// ASCII Fig Art
console.log(chalk.hex('#8B5CF6')(`
███ ██
███████ ██ ███
█████ █████ ██ ██
██████ ██ ██ ███ ████ ███
██ ███ ██ ███ ███ ███ ███ ███
██ ███ █████ ███ ███ ██
█ ██ ███ ███ ██ ██ █
██ ███ ███ █ █ ██ ███
███ ██ ███ ██ ██ ██ ██
██ ███ ██ ███ ██ ██ ███
██ ███ ███ ██ ██ ███
██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ███ █ ██ ██
██ ██ ██ ██ ███ ███ ██ ██
██ ██ ███ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██
██ ███ ██ ██ ██ ██ ██ ██
███ ██ ██ ████ ████ ████ ██
██ ██ ███ ████████ ████████ ███
███ ██ ██████ ████
████████ █████████████ ██████
███████████ ████████████
`));
const figDir = 'fig';
// Create fig/ directory if it doesn't exist
try {
await fs.mkdir(figDir, { recursive: true });
console.log(chalk.white(`🍑 Created ${figDir}/ directory`));
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
const organizedFiles = [];
const categorizedFiles = {
'Linting': [],
'Formatting': [],
'TypeScript': [],
'Build Tools': [],
'Environment': [],
'Package Managers': [],
'Git': [],
'Editor': [],
'Testing': [],
'Documentation': []
};
// Process each config file if it exists
for (const file of CONFIG_FILES) {
try {
const sourcePath = file;
const destPath = path.join(figDir, file);
// Check if file exists
await fs.access(sourcePath);
// Create directory structure if needed (for nested files like .vscode/settings.json)
const destDir = path.dirname(destPath);
if (destDir !== figDir) {
await fs.mkdir(destDir, { recursive: true });
}
if (safe) {
// Safe mode - never move or symlink, just copy
try {
await fs.copyFile(sourcePath, destPath);
console.log(chalk.blue(`📋 Copied ${file} → ${destPath}`));
} catch (error) {
if (error.code === 'EEXIST') {
// File already exists in fig/, skip
console.log(chalk.gray(`⚠️ File already exists in fig/: ${file}`));
} else {
throw error;
}
}
} else if (COMMONLY_IMPORTED_FILES.includes(file)) {
// Create symlink for commonly imported files to avoid breaking imports
try {
await fs.symlink(path.resolve(sourcePath), destPath);
console.log(chalk.yellow(`🔗 Symlinked ${file} → ${destPath}`));
} catch (error) {
if (error.code === 'EEXIST') {
// Symlink already exists, skip
console.log(chalk.gray(`⚠️ Symlink already exists: ${destPath}`));
} else {
throw error;
}
}
} else {
// Move safe-to-move files
try {
await fs.rename(sourcePath, destPath);
console.log(chalk.white(`📄 Moved ${file} → ${destPath}`));
} catch (error) {
if (error.code === 'EEXIST') {
// File already exists in fig/, skip
console.log(chalk.gray(`⚠️ File already exists in fig/: ${file}`));
} else {
throw error;
}
}
}
// Track for README generation
organizedFiles.push(file);
const category = FILE_CATEGORIES[file];
categorizedFiles[category].push(file);
} catch (error) {
if (error.code === 'ENOENT') {
// File doesn't exist, skip it
continue;
} else {
console.error(chalk.red(`❌ Error processing ${file}: ${error.message}`));
}
}
}
// Generate README.txt
await generateReadme(figDir, categorizedFiles, safe);
console.log(chalk.white(`📝 Generated ${figDir}/README.txt`));
if (organizedFiles.length === 0) {
console.log(chalk.hex('#7C3AED')('🍑 No configuration files found to organize.'));
} else {
console.log(chalk.hex('#7C3AED')(`✅ Enjoy your Figs! ${organizedFiles.length} configuration file(s).`));
if (safe) {
console.log(chalk.gray('💡 Safe mode: Original files remain untouched.'));
} else {
console.log(chalk.gray('💡 Files that might be imported are symlinked to avoid breaking imports.'));
}
}
}
async function generateReadme(figDir, categorizedFiles, safe = false) {
let content = '🍑 Your Figs\n\n';
content += 'This directory contains your project\'s configuration files.\n';
if (safe) {
content += 'All files are copies - original files remain in your project root.\n\n';
} else {
content += 'Files that are commonly imported are symlinked to avoid breaking imports.\n\n';
}
// Add each category with its files
for (const [category, files] of Object.entries(categorizedFiles)) {
if (files.length > 0) {
content += `## ${category}\n`;
for (const file of files) {
if (safe) {
content += `- ${file} (copy)\n`;
} else {
const isSymlinked = COMMONLY_IMPORTED_FILES.includes(file);
content += `- ${file}${isSymlinked ? ' (symlinked)' : ''}\n`;
}
}
content += '\n';
}
}
// Write the README file
const readmePath = path.join(figDir, 'README.txt');
await fs.writeFile(readmePath, content, 'utf8');
}