fake-it-til-you-git
Version:
A modern CLI tool to generate fake Git commit history for your GitHub/GitLab profile
298 lines (281 loc) • 13.6 kB
JavaScript
import chalk from 'chalk';
import { Command } from 'commander';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { main } from '../src/main.js';
// Get package.json version dynamically
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// When compiled, we need to go up one more level from dist/bin/ to root
const packageJsonPath = join(__dirname, '../../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version;
const program = new Command();
program
.name('fake-it-til-you-git')
.description('A modern CLI tool to generate fake Git commit history for your GitHub profile')
.version(version)
.addHelpText('after', `
${chalk.cyan('Available Commands:')}
• fake-it-til-you-git (full name)
• fityg (shortcut)
${chalk.cyan('Examples:')}
${chalk.dim('# Generate 30 days of commits with default settings')}
fityg --days 30
${chalk.dim('# Generate commits for a specific date range')}
fityg --start-date 2023-01-01 --end-date 2023-12-31
${chalk.dim('# Generate commits with custom author and distribution')}
fityg --author-name "John Doe" --author-email "john@example.com" --distribution uniform
${chalk.dim('# Preview commits without creating them')}
fityg --preview --verbose
${chalk.dim('# Use a custom configuration file')}
fityg --config my-config.json
${chalk.dim('# Generate reproducible results with seed')}
fityg --seed "my-seed-123" --commits 5
${chalk.dim('# Development mode (use test-repo directory)')}
fityg --dev --commits 10 --days 30
${chalk.dim('# Use a specific repository path')}
fityg --repo-path /path/to/your/repo --commits 5
${chalk.dim('# Auto-accept all prompts (non-interactive mode)')}
fityg --yes --days 30 --commits 5
${chalk.cyan('Configuration:')}
You can use a JSON configuration file to set defaults. CLI arguments will override config file values.
See test-configs/fake-git.config.json for an example.
${chalk.cyan('Distribution Types:')}
• uniform - Evenly distributed commits across the date range
• random - Random distribution (default)
• gaussian - Bell curve distribution (more commits in the middle)
• custom - Custom distribution pattern
• pattern - Create visual patterns in your contribution graph
${chalk.cyan('Pattern Examples:')}
${chalk.dim('# Draw a heart pattern')}
fityg --pattern heart --days 365
${chalk.dim('# Write text')}
fityg --pattern-text "HELLO" --days 365
${chalk.dim('# Custom ASCII pattern')}
fityg --custom-pattern " ██ \\n ████ \\n ██ " --days 365
${chalk.dim('# Pattern with custom intensity and scale')}
fityg --pattern star --pattern-scale 2 --pattern-intensity high
${chalk.cyan('Available Patterns:')}
• heart - Heart shape
• star - Star shape
• wave - Sine wave pattern
• square - Square shape
• triangle - Triangle shape
• diamond - Diamond shape
• cross - Cross/plus shape
• text - Use with --pattern-text for custom text
${chalk.cyan('Message Styles:')}
• default - Realistic commit messages (default)
• lorem - Lorem ipsum style messages
• emoji - Messages with emojis
`);
// Date and Range Options
program
.option('-d, --days <number>', 'Number of days to go back from today (e.g., 365 for one year)', validateDays)
.option('--start-date <date>', 'Start date in YYYY-MM-DD format (e.g., 2023-01-01)', validateDate)
.option('--end-date <date>', 'End date in YYYY-MM-DD format (e.g., 2023-12-31)', validateDate);
// Commit Configuration
program
.option('-c, --commits <number>', 'Maximum commits per day (1-100, default: 10)', validateCommits)
.option('--distribution <type>', 'Distribution type: uniform, random, gaussian, custom, pattern (default: random)', validateDistribution)
.option('--message-style <style>', 'Message style: default, lorem, emoji (default: default)', validateMessageStyle);
// Pattern Configuration (only when using pattern distribution)
program
.option('--pattern <name>', 'Preset pattern: heart, star, wave, text, square, triangle, diamond, cross (requires --distribution pattern or auto-sets it)', validatePattern)
.option('--custom-pattern <pattern>', 'Custom pattern as ASCII art string (use spaces for gaps, any character for commits)')
.option('--pattern-text <text>', 'Text to render as pattern (letters/numbers)')
.option('--pattern-scale <number>', 'Pattern scale factor (1-5, default: 1)', validatePatternScale)
.option('--pattern-intensity <level>', 'Pattern commit intensity: low, medium, high (default: medium)', validatePatternIntensity)
.option('--pattern-repeat <number>', 'Repeat pattern horizontally across the timeline (1-10, default: 1)', validatePatternRepeat);
// Author Configuration
program
.option('--author-name <name>', 'Git author name (overrides config file)')
.option('--author-email <email>', 'Git author email (overrides config file)', validateEmail);
// Behavior Options
program
.option('--preview', 'Preview commits without creating them (safe mode)', false)
.option('--config <path>', 'Path to JSON configuration file (default: ./test-configs/fake-git.config.json)')
.option('--push', 'Push commits to remote repository after creation', false)
.option('--seed <string>', 'Random seed for reproducible results (any string)')
.option('--dev', 'Development mode: use test-repo directory instead of current directory', false)
.option('--repo-path <path>', 'Path to the git repository (default: current directory)')
.option('-y, --yes', 'Automatically answer yes to all prompts (non-interactive mode)', false)
.option('-v, --verbose', 'Enable verbose output for debugging', false);
// Validation functions
function validateDays(value) {
const num = Number.parseInt(value, 10);
if (Number.isNaN(num) || num < 1 || num > 3650) {
throw new Error(`Invalid days value: ${value}. Must be between 1 and 3650.`);
}
return value;
}
function validateCommits(value) {
const num = Number.parseInt(value, 10);
if (Number.isNaN(num) || num < 1 || num > 100) {
throw new Error(`Invalid commits value: ${value}. Must be between 1 and 100.`);
}
return value;
}
function validateDate(value) {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(value)) {
throw new Error(`Invalid date format: ${value}. Use YYYY-MM-DD format.`);
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid date: ${value}. Please provide a valid date.`);
}
// Check if date is not too far in the future (more than 1 year)
const oneYearFromNow = new Date();
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
if (date > oneYearFromNow) {
throw new Error(`Date too far in the future: ${value}. Please use a date within one year from now.`);
}
return value;
}
function validateDistribution(value) {
const validDistributions = ['uniform', 'random', 'gaussian', 'custom', 'pattern'];
if (!validDistributions.includes(value)) {
throw new Error(`Invalid distribution: ${value}. Valid options: ${validDistributions.join(', ')}`);
}
return value;
}
function validatePattern(value) {
const validPatterns = ['heart', 'star', 'wave', 'text', 'square', 'triangle', 'diamond', 'cross'];
if (!validPatterns.includes(value)) {
throw new Error(`Invalid pattern: ${value}. Valid options: ${validPatterns.join(', ')}`);
}
return value;
}
function validatePatternScale(value) {
const num = Number.parseInt(value, 10);
if (Number.isNaN(num) || num < 1 || num > 5) {
throw new Error(`Invalid pattern scale: ${value}. Must be between 1 and 5.`);
}
return value;
}
function validatePatternIntensity(value) {
const validIntensities = ['low', 'medium', 'high'];
if (!validIntensities.includes(value)) {
throw new Error(`Invalid pattern intensity: ${value}. Valid options: ${validIntensities.join(', ')}`);
}
return value;
}
function validatePatternRepeat(value) {
const num = Number.parseInt(value, 10);
if (Number.isNaN(num) || num < 1 || num > 10) {
throw new Error(`Invalid pattern repeat: ${value}. Must be between 1 and 10.`);
}
return value;
}
function validateMessageStyle(value) {
const validStyles = ['default', 'lorem', 'emoji'];
if (!validStyles.includes(value)) {
throw new Error(`Invalid message style: ${value}. Valid options: ${validStyles.join(', ')}`);
}
return value;
}
function validateEmail(value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error(`Invalid email format: ${value}. Please provide a valid email address.`);
}
return value;
}
// Custom error handling for argument validation
program.exitOverride();
try {
program.parse();
}
catch (error) {
// Handle Commander.js specific errors
if (error.code === 'commander.version' || error.code === 'commander.helpDisplayed') {
// These are normal exit cases, not errors
process.exit(0);
}
if (error instanceof Error) {
if (error.message.includes('unknown option')) {
// Commander.js unknown option error
console.error(chalk.red('❌ CLI Error:'), error.message);
console.log(chalk.dim('\nUse --help for usage information.'));
}
else {
// Validation error
console.error(chalk.red('❌ Validation Error:'), error.message);
console.log(chalk.dim('\nUse --help for usage information.'));
}
process.exit(1);
}
throw error;
}
const options = program.opts();
// Additional validation for option combinations
try {
validateOptionCombinations(options);
}
catch (error) {
console.error(chalk.red('❌ Configuration Error:'), error instanceof Error ? error.message : 'Unknown error');
console.log(chalk.dim('\nUse --help for usage information.'));
process.exit(1);
}
// Enhanced error handling for main function
main(options).catch((error) => {
console.error(chalk.red('❌ Error:'), error.message);
if (options.verbose) {
console.error(chalk.dim('\nStack trace:'));
console.error(chalk.dim(error.stack));
}
console.log(chalk.dim('\nUse --verbose for more details or --help for usage information.'));
process.exit(1);
});
/**
* Validates combinations of options to ensure they make sense together
*/
function validateOptionCombinations(options) {
// Validate date range logic
if (options.startDate && options.endDate) {
const startDate = new Date(options.startDate);
const endDate = new Date(options.endDate);
if (startDate >= endDate) {
throw new Error('Start date must be before end date.');
}
// Check if date range is too large (more than 10 years)
const maxRange = 10 * 365 * 24 * 60 * 60 * 1000; // 10 years in milliseconds
if (endDate.getTime() - startDate.getTime() > maxRange) {
throw new Error('Date range too large. Maximum supported range is 10 years.');
}
}
// Validate that days option doesn't conflict with date range
// Only check if days was explicitly provided (not just default value)
const daysWasExplicitlyProvided = process.argv.includes('--days') || process.argv.includes('-d');
if (daysWasExplicitlyProvided && (options.startDate || options.endDate)) {
throw new Error('Cannot use --days option together with --start-date or --end-date. Choose one approach.');
}
// Validate seed is not empty if provided
if (options.seed && typeof options.seed === 'string' && options.seed.trim().length === 0) {
throw new Error('Seed cannot be empty. Provide a meaningful string value.');
}
// Warn about potentially problematic combinations
if (options.commits && Number.parseInt(options.commits, 10) > 50 && !options.preview) {
console.warn(chalk.yellow('⚠️ Warning: High commits per day (>50) may create unrealistic patterns. Consider using --preview first.'));
}
if (options.push && options.preview) {
console.warn(chalk.yellow('⚠️ Warning: --push option ignored in preview mode.'));
}
// Validate pattern-related options
const hasPattern = options.pattern || options.customPattern || options.patternText;
const isPatternDistribution = options.distribution === 'pattern';
if (options.patternIntensity && !hasPattern && !isPatternDistribution) {
console.warn(chalk.yellow('⚠️ Warning: --pattern-intensity is only effective when using patterns (--pattern, --custom-pattern, or --pattern-text).'));
}
if (options.patternRepeat && !hasPattern && !isPatternDistribution) {
console.warn(chalk.yellow('⚠️ Warning: --pattern-repeat is only effective when using patterns (--pattern, --custom-pattern, or --pattern-text).'));
}
if (options.patternScale && !hasPattern && !isPatternDistribution) {
console.warn(chalk.yellow('⚠️ Warning: --pattern-scale is only effective when using patterns (--pattern, --custom-pattern, or --pattern-text).'));
}
}
//# sourceMappingURL=cli.js.map