UNPKG

mocktail-cli

Version:

**Craft your data cocktail — realistic mock data, shaken not stirred.**

1,166 lines (1,017 loc) 50.7 kB
#!/usr/bin/env npx tsx // import { Command } from "commander"; import * as path from "path"; import * as fs from "fs"; import * as os from "os"; import ora from "ora"; import chalk from "chalk"; import { spawn } from "child_process"; import { writeMockDataToFile } from "../src/utils/writeMockDataToFile"; import { generateMockData } from "../src/generators/generateMockData"; import { SchemaRegistry } from "../src/schema-parsers/schemaRegistry"; import { errorHandler } from "../src/utils/errorHandler"; import { performanceOptimizer } from "../src/utils/performanceOptimizer"; import { pluginManager } from "../src/plugins/pluginManager"; import { outputFormatter } from "../src/utils/outputFormatter"; import { circularDependencyResolver } from "../src/utils/circularDependencyResolver"; import { createLocalizedFaker, setGlobalLocale, setGlobalSeed } from "../src/utils/localeManager"; import { relationPresets } from '../src/constants/relationPresets'; // Logo import printMocktailLogo from '../src/printMocktailLogo'; // Read version from package.json const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')); const version = packageJson.version; import type { Model, ModelsMap, GeneratedData, RelationPresets, MockConfig, SeedData, SchemaValidation, GenerateCommandOptions, GlobalOptions, SpawnOptions, ChildProcess, OraSpinner } from '../src/types'; let loadMockConfig: ((path: string) => MockConfig) | null = null; try { loadMockConfig = require("../src/utils/loadMockConfig").default; } catch {} const SEEN_FILE = path.join(os.homedir(), ".mocktail-cli-seen"); function loadEnvFile(envPath: string): void { try { if (!fs.existsSync(envPath)) return; const contents = fs.readFileSync(envPath, 'utf8'); for (const line of contents.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const idx = trimmed.indexOf('='); if (idx === -1) continue; const key = trimmed.slice(0, idx).trim(); let value = trimmed.slice(idx + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } if (!(key in process.env)) process.env[key] = value; } } catch {} } async function shouldShowLogo(forceLogo: boolean, noLogo: boolean, noSubcommand: boolean): Promise<boolean> { if (noLogo) return false; if (forceLogo) return true; if (!noSubcommand) return false; try { if (fs.existsSync(SEEN_FILE)) { return false; // Already seen, don't show again } else { fs.writeFileSync(SEEN_FILE, "seen", { encoding: "utf-8" }); return true; // Show logo first time and create seen file } } catch { return true; // On error, show logo once } } const program = new Command(); program .name("mocktail-cli") .description("Schema-aware mock data generator") .version(version) .option('--no-logo', 'Suppress logo output globally') .option('-q, --quiet', 'Suppress output except errors globally') .option('--force-logo', 'Force show the logo animation even if shown before') .option('--enable-plugins', 'Enable plugin system') .option('--plugin-dir <path>', 'Directory to load plugins from') .option('--performance-mode', 'Enable performance optimizations for large datasets') .option('--memory-limit <mb>', 'Set memory limit in MB', '1024') .option('--batch-size <size>', 'Set batch size for processing', '1000') .option('--enable-advanced-relations', 'Enable advanced relation detection') .option('--relation-confidence <threshold>', 'Set relation detection confidence threshold', '0.5') .option('--verbose', 'Enable verbose output with detailed information') .option('--timestamp', 'Add timestamps to output'); // Initialize schema registry lazily let schemaRegistry: SchemaRegistry | null = null; function getSchemaRegistry(): SchemaRegistry { if (!schemaRegistry) { schemaRegistry = new SchemaRegistry(); } return schemaRegistry; } // Enhanced schema detection functions function findSchemas(basePath: string = process.cwd()): string[] { return getSchemaRegistry().getDetector().findSchemaFiles(basePath); } // Process a single schema file async function processSingleSchema(schemaPath: string, schemaType: string, opts: GenerateCommandOptions, _globalOpts: GlobalOptions): Promise<void> { const resolvedPath = path.resolve(process.cwd(), schemaPath); // Validate schema const schemaValidation = await validateSchema(resolvedPath, schemaType); if (!schemaValidation.valid) { throw new Error(`Invalid schema: ${schemaValidation.errors.join(', ')}`); } // Parse schema const models: ModelsMap = await getSchemaRegistry().parseSchema(resolvedPath, schemaType); if (Object.keys(models).length === 0) { throw new Error('No models found in schema'); } // Filter models if specified let filteredModels = models; if (opts.models) { const modelNames = opts.models.split(',').map(name => name.trim()); filteredModels = Object.fromEntries( Object.entries(models).filter(([name]) => modelNames.includes(name)) ); } // Generate output path const outputPath = opts.out ? path.join(opts.out, path.basename(schemaPath, path.extname(schemaPath))) : undefined; // Run generation - using the existing generate command logic const { runGenerate } = await import('../src/commands/generate'); await runGenerate({ schema: resolvedPath, count: parseInt(opts.count, 10), seed: opts.seed || false, output: outputPath || '', models: filteredModels, format: opts.format, depth: parseInt(opts.depth, 10), nest: opts.nest || false, relations: opts.relations || false, dedupe: opts.dedupe || false, pretty: opts.pretty || false, noLog: opts.noLog || false, seedValue: opts.seedValue || undefined, preset: opts.preset || undefined }); } async function validateSchema(schemaPath: string, schemaType?: string): Promise<SchemaValidation> { return await getSchemaRegistry().validateSchema(schemaPath, schemaType); } function autoDetectSchema(): { path: string; type: string } | null { const schemas = findSchemas(); if (schemas.length === 0) { return null; } // Validate all found schemas const validSchemas: Array<{ path: string; type: string; validation: SchemaValidation }> = []; for (const schemaPath of schemas) { const detectedType = getSchemaRegistry().getDetector().detectSchemaType(schemaPath); if (detectedType) { // Note: This is a synchronous check for auto-detection // In a real implementation, you might want to make this async const parser = getSchemaRegistry().getParser(detectedType); if (parser) { try { const validation = parser.validateSchema(schemaPath); if (validation.valid) { validSchemas.push({ path: schemaPath, type: detectedType, validation }); } } catch (err) { // Skip invalid schemas } } } } if (validSchemas.length === 0) { console.warn('⚠️ Found schema files but none are valid:'); schemas.forEach(schema => { const detectedType = getSchemaRegistry().getDetector().detectSchemaType(schema); if (detectedType) { const parser = getSchemaRegistry().getParser(detectedType); if (parser) { try { const validation = parser.validateSchema(schema); if (!validation.valid) { console.warn(` ${schema} (${detectedType}): ${validation.errors.join(', ')}`); } } catch (err) { console.warn(` ${schema} (${detectedType}): Invalid schema`); } } } }); return null; } // Prefer Prisma schema in default location const defaultPrismaSchema = validSchemas.find(s => s.type === 'prisma' && s.path.includes('/prisma/schema.prisma') ); if (defaultPrismaSchema) { return { path: defaultPrismaSchema.path, type: defaultPrismaSchema.type }; } // Return the first valid schema const firstValid = validSchemas[0]; if (firstValid) { return { path: firstValid.path, type: firstValid.type }; } return null; } // GENERATE command const generateCommand = program .command("generate") .description("Generate mock data for any schema (Prisma, GraphQL, JSON Schema, OpenAPI)") .option("-c, --count <number>", "Number of records per model", "5") .option("-o, --out <directory>", "Output directory") .option("-f, --format <type>", "Output format: json, sql, ts, csv", "json") .option("-s, --schema <path>", "Schema path (auto-detected if not specified)", "./prisma/schema.prisma") .option("-t, --type <type>", "Schema type: prisma, graphql, json-schema, openapi, typescript, protobuf, avro, xml-schema, sql-ddl, mongoose, sequelize, joi, yup, zod (auto-detected if not specified)") .option("--batch", "Process multiple schema files in the current directory") .option("-m, --models <models>", "Comma-separated list of models (optional)") .option("--mock-config <path>", "Path to mocktail-cli.config.js") .option("-d, --depth <number>", "Nested relation depth (depth > 1 enables relations)", "1") .option("--no-nest", "Disable nested relations (flat structure)") .option("--relations", "Enable automatic relation generation (works with any depth)") .option("--dedupe", "Enable deduplication of records") .option("--pretty", "Pretty-print JSON output") .option("--no-pretty", "Disable pretty-printing JSON output") .option("--no-log", "Suppress console logs during mock generation") .option("--seed", "Insert mock data into DB") .option("--seed-value <number>", "Seed value for reproducible data generation") .option("--locale <locale>", "Locale for generating culturally appropriate data (e.g., en, es, fr, ja)", "en") .option("--preset <type>", "Relation preset: blog, ecommerce, social"); generateCommand.action(async (opts: GenerateCommandOptions) => { const spinner: OraSpinner = ora({ spinner: "dots" }); try { const globalOpts: GlobalOptions = program.opts(); // Initialize enhanced features if ((globalOpts as any).enablePlugins) { if ((globalOpts as any).pluginDir) { await pluginManager.loadPluginsFromDirectory((globalOpts as any).pluginDir); } else { // Load default plugins const defaultPluginDir = path.join(__dirname, '../src/plugins/examples'); if (fs.existsSync(defaultPluginDir)) { await pluginManager.loadPluginsFromDirectory(defaultPluginDir); } } outputFormatter.success('Plugin system enabled'); outputFormatter.info(pluginManager.getPluginInfo()); } // Initialize performance optimizer if ((globalOpts as any).performanceMode) { performanceOptimizer.startGeneration(); outputFormatter.success('Performance mode enabled'); } // Batch processing if (opts.batch) { const schemas = findSchemas(); if (schemas.length === 0) { console.error(chalk.red('❌ No schema files found for batch processing')); process.exit(1); } console.log(chalk.blue(`🔄 Processing ${schemas.length} schema files...`)); for (const schemaPath of schemas) { const detectedType = getSchemaRegistry().getDetector().detectSchemaType(schemaPath); if (detectedType) { console.log(chalk.gray(`\n📄 Processing: ${schemaPath} (${detectedType})`)); try { await processSingleSchema(schemaPath, detectedType, opts, globalOpts); } catch (error: any) { console.error(chalk.red(`❌ Failed to process ${schemaPath}: ${error.message}`)); } } } return; } // Enhanced schema detection and validation let schemaPath: string = opts.schema; let schemaType: string | undefined = opts.type; // Auto-detect schema if not specified or if default doesn't exist if (!opts.schema || opts.schema === './prisma/schema.prisma') { const detectedSchema = autoDetectSchema(); if (detectedSchema) { schemaPath = detectedSchema.path; schemaType = detectedSchema.type; if (!globalOpts.quiet) console.log(`🔍 Auto-detected schema: ${schemaPath} (${schemaType})`); } } schemaPath = path.resolve(process.cwd(), schemaPath); // Enhanced schema validation const schemaValidation = await validateSchema(schemaPath, schemaType); if (!schemaValidation.valid) { console.error(`❌ Invalid ${schemaType || 'schema'}: ${schemaPath}`); console.error(`Errors: ${schemaValidation.errors.join(', ')}`); console.error(chalk.yellow('💡 Supported schema types:')); console.error(chalk.gray(' - prisma, graphql, json-schema, openapi')); console.error(chalk.gray(' - typescript, protobuf, avro, xml-schema')); console.error(chalk.gray(' - sql-ddl, mongoose, sequelize')); console.error(chalk.gray(' - joi, yup, zod')); process.exit(1); } if (!fs.existsSync(schemaPath)) { console.error(`❌ Schema file not found: ${schemaPath}`); // Try to help user find schemas const schemas = findSchemas(); if (schemas.length > 0) { console.log('\n Found these schema files:'); schemas.forEach(s => { const detectedType = getSchemaRegistry().getDetector().detectSchemaType(s); console.log(` ${s} (${detectedType || 'unknown'})`); }); console.log('\n💡 Try using one of these paths with --schema'); } process.exit(1); } // Load .env from the Prisma project root if present, without extra deps const envPath = path.join(path.dirname(schemaPath), '..', '.env'); loadEnvFile(envPath); const count = parseInt(opts.count, 10); const depth = parseInt(opts.depth, 10); if (isNaN(count) || count <= 0) { console.error("❌ --count must be a positive integer."); process.exit(1); } if (isNaN(depth) || depth <= 0) { console.error("❌ --depth must be a positive integer."); process.exit(1); } const supportedFormats = ["json", "csv", "ts", "sql"]; if (!supportedFormats.includes(opts.format)) { console.error(`❌ Unsupported format: ${opts.format}`); console.error(`Supported formats: ${supportedFormats.join(", ")}`); process.exit(1); } const mockConfig: MockConfig | null = loadMockConfig && opts.mockConfig ? loadMockConfig(opts.mockConfig) : null; const modelsObject: ModelsMap = await getSchemaRegistry().parseSchema(schemaPath, schemaType); const allModels: Model[] = Object.values(modelsObject); let filteredModels: Model[] = allModels; if (opts.models) { const allowed = opts.models.split(",").map(m => m.trim()); filteredModels = allModels.filter(m => allowed.includes(m.name)); } if (filteredModels.length === 0) { console.error("❌ No models found after filtering."); process.exit(1); } const outputDir = opts.out ? path.resolve(process.cwd(), opts.out) : null; if (outputDir && !fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } if (!globalOpts.quiet && !opts.noLog) spinner.start("Preparing generation order and state..."); const generatedDataMap: Record<string, Record<string, any>[]> = {}; // Order models so that those without required inbound relations are seeded first const modelNameToModel: Record<string, Model> = Object.fromEntries(allModels.map(m => [m.name, m])); function hasRequiredInboundRelations(m: Model): boolean { for (const field of m.fields) { // If this field is a required foreign key to another model, this model depends on others if (field.relationFromFields && field.relationFromFields.length > 0) { // If FK field is optional, we can seed without the parent existing if (!field.isOptional) return true; } } return false; } function topologicalOrder(models: Model[]): Model[] { const incoming = new Map<string, number>(); const dependsOn = new Map<string, Set<string>>(); for (const m of models) { const deps = new Set<string>(); for (const f of m.fields) { if (f.relationFromFields && f.relationFromFields.length > 0) { // This model references f.type deps.add(f.type); } } dependsOn.set(m.name, deps); incoming.set(m.name, deps.size); } const queue: string[] = []; for (const m of models) { if (!hasRequiredInboundRelations(m)) queue.push(m.name); } const ordered: Model[] = []; const visited = new Set<string>(); while (queue.length) { const name = queue.shift(); if (!name || visited.has(name)) continue; visited.add(name); const model = modelNameToModel[name]; if (model) ordered.push(model); for (const [n, deps] of dependsOn) { if (deps.has(name)) { deps.delete(name); incoming.set(n, deps.size); if (deps.size === 0) queue.push(n); } } } for (const m of models) { if (!visited.has(m.name)) ordered.push(m); } return ordered; } // Use advanced circular dependency resolution if (!globalOpts.quiet && !opts.noLog) spinner.start("Analyzing dependencies and resolving cycles..."); const dependencyResult = circularDependencyResolver.resolveDependencies(filteredModels); const seedingOrder: Model[] = dependencyResult.generationOrder; if (dependencyResult.cycles.length > 0) { if (!globalOpts.quiet && !opts.noLog) { console.log(`\n🔄 Detected ${dependencyResult.cycles.length} circular dependencies:`); dependencyResult.cycles.forEach(cycle => { console.log(` • ${cycle.type} cycle: ${cycle.nodes.join(' → ')} (${cycle.strength})`); }); if (dependencyResult.resolutionPlan) { console.log(`\n🛠️ Applied resolution strategy: ${dependencyResult.resolutionPlan.strategy}`); if (dependencyResult.resolutionPlan.deferredRelations.length > 0) { console.log(` • Deferred ${dependencyResult.resolutionPlan.deferredRelations.length} relations for later population`); } } } } if (!globalOpts.quiet && !opts.noLog) spinner.succeed("Dependencies analyzed and cycles resolved."); // Set faker locale if provided (for main data generation) const localeConfig = await createLocalizedFaker(opts.locale || 'en'); setGlobalLocale(localeConfig); if (!globalOpts.quiet && !opts.noLog && opts.locale) { console.log(`🌍 Using locale: ${localeConfig.locale}`); } // Set faker seed if provided (for reproducible data generation) if (opts.seedValue) { const seedValue = parseInt(opts.seedValue, 10); if (isNaN(seedValue)) { console.error("❌ --seed-value must be a valid integer."); process.exit(1); } setGlobalSeed(seedValue); if (!globalOpts.quiet && !opts.noLog) { console.log(`🎲 Using seed value: ${seedValue}`); } } const seedDataByModel: Record<string, Record<string, any>[]> = {}; if (!globalOpts.quiet && !opts.noLog) spinner.succeed("Order prepared. Starting data generation..."); // First pass: generate all models without relations for (const model of seedingOrder) { if (!globalOpts.quiet && !opts.noLog) spinner.start(`📦 Generating data for model: ${model.name}`); const config = mockConfig?.[model.name]; // Generate raw records without relations first const rawGen: GeneratedData = generateMockData(model, { count, relationData: {}, config: { ...config, sqlMode: false }, preset: opts.preset || null }); const rawRecords: Record<string, any>[] = rawGen.records; // Update performance metrics if ((globalOpts as any).performanceMode) { performanceOptimizer.updateMetrics(rawRecords.length, 0); } // Store raw records so downstream relations use plain values (not SQL-safe strings) generatedDataMap[model.name] = rawRecords; // File writing will be done after relations are populated if (opts.seed) { const scalarFieldNames = new Set(model.fields.filter(f => f.isScalar || f.isId).map(f => f.name)); const cleanRecords = rawRecords.map(rec => { const out: Record<string, any> = {}; for (const key of Object.keys(rec)) { if (scalarFieldNames.has(key)) out[key] = rec[key]; } return out; }); seedDataByModel[model.name] = cleanRecords; } if (!globalOpts.quiet && !opts.noLog) spinner.succeed(`Finished processing model: ${model.name}`); } // Second pass: populate relations based on depth (unless --no-nest is specified) // Generate relations if --relations flag is passed OR if depth > 1 // Commander maps --no-nest to opts.nest === false const shouldGenerateRelations = (opts.relations || depth > 1) && opts.nest !== false; if (shouldGenerateRelations) { if (!globalOpts.quiet && !opts.noLog) spinner.start("🔗 Populating relations..."); // If --relations is used alone (depth=1), use depth=2 for meaningful nesting const effectiveDepth = opts.relations && depth === 1 ? 2 : depth; const buildRelationData = (currentModel: Model, currentDepth: number, visited: Set<string> = new Set()): Record<string, any> => { if (currentDepth > effectiveDepth) return {}; if (visited.has(currentModel.name)) return {}; visited.add(currentModel.name); const relationData: Record<string, any> = {}; for (const field of currentModel.fields) { if (field.isRelation) { const relatedModelName = field.type; const relatedModel = allModels.find(m => m.name === relatedModelName); if (relatedModel && generatedDataMap[relatedModelName]) { // Only include relations if we haven't exceeded effective depth if (currentDepth < effectiveDepth) { const relatedRecords = generatedDataMap[relatedModelName] || []; if (field.isArray) { // For array relations, create nested objects with preset count let recordsToUse = relatedRecords; // Apply preset count if specified if (opts.preset && relationPresets[opts.preset as keyof RelationPresets] && relationPresets[opts.preset as keyof RelationPresets][currentModel.name]) { const presetConfig = relationPresets[opts.preset as keyof RelationPresets]?.[currentModel.name]?.[field.name]; if (presetConfig && presetConfig.count) { const maxCount = Math.min(relatedRecords.length, presetConfig.count.max); const minCount = presetConfig.count.min; const count = Math.floor(Math.random() * (maxCount - minCount + 1)) + minCount; recordsToUse = relatedRecords.slice(0, count); if (!globalOpts.quiet && !opts.noLog) { console.log(`🎯 Preset ${opts.preset}: ${currentModel.name}.${field.name} = ${count} records (min: ${minCount}, max: ${maxCount})`); } } } relationData[field.name] = recordsToUse.map(record => { const nestedRecord = { ...record }; // Recursively add nested relations const nestedData = buildRelationData(relatedModel, currentDepth + 1, new Set(visited)); Object.assign(nestedRecord, nestedData); return nestedRecord; }); } else { // For single relations, pick one record and nest it const selectedRecord = relatedRecords[Math.floor(Math.random() * relatedRecords.length)] || null; if (selectedRecord) { const nestedRecord = { ...selectedRecord }; const nestedData = buildRelationData(relatedModel, currentDepth + 1, new Set(visited)); Object.assign(nestedRecord, nestedData); relationData[field.name] = nestedRecord; } else { relationData[field.name] = null; } } } else { // At max depth, just include empty arrays/null relationData[field.name] = field.isArray ? [] : null; } } } } return relationData; }; // Regenerate data with relations for each model for (const model of seedingOrder) { const relationData = buildRelationData(model, 1); const config = mockConfig?.[model.name]; if (Object.keys(relationData).length > 0) { if (!globalOpts.quiet && !opts.noLog) console.log(`🔗 Adding relations to ${model.name}:`, Object.keys(relationData)); // Regenerate with relations const rawGen: GeneratedData = generateMockData(model, { count, relationData, config: { ...config, sqlMode: false }, preset: opts.preset || null }); generatedDataMap[model.name] = rawGen.records; } } // Generic nested relation handling for any schema (only if effective depth > 1 and not --no-nest) if (effectiveDepth > 1 && opts.nest !== false) { // Handle one-to-many relationships (posts, comments, etc.) if (generatedDataMap['Post'] && generatedDataMap['User']) { const posts = generatedDataMap['Post']; const users = generatedDataMap['User']; if (!globalOpts.quiet && !opts.noLog) console.log(`🔗 Processing ${posts.length} posts`); // Update posts to reference actual User IDs and fix field conflicts generatedDataMap['Post'] = posts.map((post, index) => { const user = users[index % users.length]; return { ...post, authorId: user?.['id'], // Use authorId for posts (matches schema) userId: undefined, // Remove conflicting userId field author: { id: user?.['id'], name: user?.['name'], email: user?.['email'] }, user: undefined // Remove conflicting user field }; }); // Update User posts with nested post objects generatedDataMap['User'] = users.map(user => { const userPosts = generatedDataMap['Post']?.filter(p => p['authorId'] === user['id']) || []; // Apply preset count for posts if specified let postsToUse = userPosts; if (opts.preset && relationPresets[opts.preset as keyof RelationPresets] && relationPresets[opts.preset as keyof RelationPresets]['User'] && relationPresets[opts.preset as keyof RelationPresets]['User']?.['posts']) { const presetConfig = relationPresets[opts.preset as keyof RelationPresets]['User']?.['posts']; if (presetConfig?.count) { const maxCount = Math.min(userPosts.length, presetConfig.count.max); const minCount = presetConfig.count.min; const count = Math.floor(Math.random() * (maxCount - minCount + 1)) + minCount; postsToUse = userPosts.slice(0, count); if (!globalOpts.quiet && !opts.noLog) { console.log(`🎯 Preset ${opts.preset}: User.posts = ${count} records (min: ${minCount}, max: ${maxCount})`); } } } return { ...user, posts: postsToUse.map(post => ({ id: post['id'], title: post['title'], content: post['content'], authorId: post['authorId'], author: post['author'], comments: post['comments'] })) }; }); } // Handle comments if (generatedDataMap['Comment'] && generatedDataMap['User'] && generatedDataMap['Post']) { const comments = generatedDataMap['Comment']; const users = generatedDataMap['User']; const posts = generatedDataMap['Post']; if (!globalOpts.quiet && !opts.noLog) console.log(`🔗 Processing ${comments.length} comments`); // Update comments to reference actual User and Post IDs generatedDataMap['Comment'] = comments.map((comment, index) => { const user = users[index % users.length]; const post = posts[index % posts.length]; return { ...comment, authorId: user?.['id'], postId: post?.['id'], author: { id: user?.['id'], name: user?.['name'], email: user?.['email'] }, post: { id: post?.['id'], title: post?.['title'] } }; }); // Update User comments with nested comment objects generatedDataMap['User'] = generatedDataMap['User'].map(user => { const userComments = generatedDataMap['Comment']?.filter(c => c['authorId'] === user['id']) || []; // Apply preset count for comments if specified let commentsToUse = userComments; if (opts.preset && relationPresets[opts.preset as keyof RelationPresets] && relationPresets[opts.preset as keyof RelationPresets]['User'] && relationPresets[opts.preset as keyof RelationPresets]['User']?.['comments']) { const presetConfig = relationPresets[opts.preset as keyof RelationPresets]['User']?.['comments']; if (presetConfig?.count) { const maxCount = Math.min(userComments.length, presetConfig.count.max); const minCount = presetConfig.count.min; const count = Math.floor(Math.random() * (maxCount - minCount + 1)) + minCount; commentsToUse = userComments.slice(0, count); if (!globalOpts.quiet && !opts.noLog) { console.log(`🎯 Preset ${opts.preset}: User.comments = ${count} records (min: ${minCount}, max: ${maxCount})`); } } } return { ...user, comments: commentsToUse }; }); // Update Post comments with nested comment objects generatedDataMap['Post'] = generatedDataMap['Post'].map(post => { const postComments = generatedDataMap['Comment']?.filter(c => c['postId'] === post['id']) || []; return { ...post, comments: postComments }; }); } // Handle many-to-many relationships (User-Team) if (generatedDataMap['Team'] && generatedDataMap['User']) { const teams = generatedDataMap['Team']; const users = generatedDataMap['User']; if (!globalOpts.quiet && !opts.noLog) console.log(`🔗 Processing User-Team relationships`); // Create user-team associations const userTeamAssociations: Array<{ userId: any; teamId: any }> = []; users.forEach((user) => { // Each user can be in 0-2 teams const teamCount = Math.floor(Math.random() * 3); for (let i = 0; i < teamCount; i++) { const team = teams[i % teams.length]; userTeamAssociations.push({ userId: user['id'], teamId: team?.['id'] }); } }); // Update User teams with nested team objects generatedDataMap['User'] = users.map(user => { const userTeamIds = userTeamAssociations.filter(a => a.userId === user['id']).map(a => a.teamId); const userTeams = teams.filter(team => userTeamIds.includes(team['id'])); return { ...user, teams: userTeams }; }); // Update Team users with nested user objects generatedDataMap['Team'] = teams.map(team => { const teamUserIds = userTeamAssociations.filter(a => a.teamId === team['id']).map(a => a.userId); const teamUsers = users.filter(user => teamUserIds.includes(user['id'])); return { ...team, users: teamUsers.map(user => ({ id: user['id'], name: user['name'], email: user['email'] })) }; }); } } if (!globalOpts.quiet && !opts.noLog) spinner.succeed("Relations populated."); } // Show final console output if no output directory specified if (!outputDir) { for (const model of seedingOrder) { if (!globalOpts.quiet && !opts.noLog) { console.log(`\n📦 Final data for model: ${model.name}`); console.dir(generatedDataMap[model.name], { depth: null }); } } } // Apply deduplication if --dedupe flag is set if (opts.dedupe) { if (!globalOpts.quiet && !opts.noLog) spinner.start("🔄 Deduplicating records..."); for (const model of seedingOrder) { const records = generatedDataMap[model.name]; if (records && records.length > 0) { // Simple deduplication based on ID field const seen = new Set(); const dedupedRecords = records.filter(record => { const id = record['id']; if (seen.has(id)) { return false; } seen.add(id); return true; }); generatedDataMap[model.name] = dedupedRecords; } } if (!globalOpts.quiet && !opts.noLog) spinner.succeed("Records deduplicated."); } // Write files with final data (including relations) if (outputDir) { for (const model of seedingOrder) { const finalRecords = generatedDataMap[model.name]; if (finalRecords && finalRecords.length > 0) { // Handle pretty printing: default is true, unless --no-pretty is specified const shouldPretty = opts.pretty !== false && !opts.noPretty; const written = writeMockDataToFile(model.name, finalRecords, outputDir, opts.format, shouldPretty); if (!globalOpts.quiet && !opts.noLog) console.log(`✅ Saved data for ${model.name} → ${written}`); } } } if (opts.seed) { // Seed and locale are already set above, so no need to set them again here // Write seed JSON into the Prisma project const prismaProject = path.resolve(path.dirname(schemaPath), ".."); const seedFile = path.join(prismaProject, "__mocktail_seed.json"); const payload: SeedData = { order: seedingOrder.map(m => m.name), data: seedDataByModel, }; fs.writeFileSync(seedFile, JSON.stringify(payload, null, 2), "utf8"); if (!globalOpts.quiet && !opts.noLog) console.log(`\n✅ Mock data JSON saved to: ${seedFile}`); // Ensure Prisma Client is generated in the target project const run = (cmd: string, args: string[], cwd: string): Promise<number> => new Promise((resolve) => { const child: ChildProcess = spawn(cmd, args, { cwd, stdio: 'inherit', shell: true } as SpawnOptions); child.on('exit', (code: number) => resolve(code)); }); if (!globalOpts.quiet && !opts.noLog) console.log("\n🧩 Generating Prisma Client in target project..."); const genCode = await run('npx', ['--yes', 'prisma', 'generate'], prismaProject); if (genCode !== 0) { console.error('❌ Failed to generate Prisma Client in target project.'); process.exit(1); } // Create a temporary seed runner inside the target project to ensure correct module resolution const runnerPath = path.join(prismaProject, '.mocktail_seed_runner.cjs'); const runnerSrc = [ "const fs = require('fs');", "const path = require('path');", "const seedFile = path.join(process.cwd(), '__mocktail_seed.json');", "if (!fs.existsSync(seedFile)) { console.error('❌ Mock data JSON not found. Run the CLI first.'); process.exit(1); }", "const payload = JSON.parse(fs.readFileSync(seedFile, 'utf8'));", "const order = Array.isArray(payload.order) ? payload.order : Object.keys(payload.data);", "const data = payload.data || {};", "const { PrismaClient } = require('@prisma/client');", "const prisma = new PrismaClient();", "(async () => {", " try {", " for (const modelName of order) {", " const modelKey = modelName.charAt(0).toLowerCase() + modelName.slice(1);", " if (typeof prisma[modelKey]?.createMany === 'function') {", " await prisma[modelKey].createMany({ data: data[modelName] });", " console.log(`✅ Seeded ${data[modelName].length} records into ${modelName}`);", " } else {", " console.warn(`⚠️ No createMany method found for model: ${modelName}`);", " }", " }", " await prisma.$disconnect();", " } catch (err) {", " console.error('❌ Error during seeding:', err);", " process.exit(1);", " }", "})();", '' ].join('\n'); fs.writeFileSync(runnerPath, runnerSrc, 'utf8'); if (!globalOpts.quiet && !opts.noLog) console.log("\n🌱 Spawning seeding process in Prisma project..."); const seedCode = await run('node', [runnerPath], prismaProject); try { fs.unlinkSync(runnerPath); } catch {} if (seedCode === 0) { if (!globalOpts.quiet && !opts.noLog) console.log('🌱 Seeding complete!'); if (!globalOpts.quiet && !opts.noLog) console.log("\n✅ Mock data generation completed."); // Display performance metrics if enabled if ((globalOpts as any).performanceMode) { performanceOptimizer.stopGeneration(); const report = performanceOptimizer.getPerformanceReport(); console.log('\n' + report); } process.exit(0); } else { console.error('❌ Seeding failed!'); process.exit(1); } } else { if (!globalOpts.quiet && !opts.noLog) console.log("\n✅ Mock data generation completed."); // Display performance metrics if enabled if ((globalOpts as any).performanceMode) { performanceOptimizer.stopGeneration(); const report = performanceOptimizer.getPerformanceReport(); console.log('\n' + report); } process.exit(0); } } catch (error: any) { spinner.fail("Failed!"); // Enhanced error handling const enhancedError = errorHandler.createEnhancedError( error, error.code || 'GENERATION_ERROR', { command: 'generate', schemaPath: opts.schema, schemaType: opts.type || 'unknown' } ); errorHandler.logError(enhancedError); // Stop performance monitoring if ((program.opts() as any).performanceMode) { performanceOptimizer.stopGeneration(); console.log(performanceOptimizer.getPerformanceReport()); } process.exit(1); } }); // Show README content in terminal const docsCommand = program .command("docs") .description("Show full README.md documentation in terminal"); docsCommand.action(() => { const readmePath = path.resolve(__dirname, "../../README.md"); if (!fs.existsSync(readmePath)) { console.error("❌ README.md file not found."); process.exit(1); } const readmeContent = fs.readFileSync(readmePath, "utf-8"); console.log(readmeContent); }); // Plugin management commands const pluginsCommand = program .command("plugins") .description("Manage plugins"); pluginsCommand .command("list") .description("List all loaded plugins") .action(() => { console.log(pluginManager.getPluginInfo()); }); pluginsCommand .command("enable <name>") .description("Enable a plugin") .action((name: string) => { pluginManager.enablePlugin(name); console.log(`✅ Plugin ${name} enabled`); }); pluginsCommand .command("disable <name>") .description("Disable a plugin") .action((name: string) => { pluginManager.disablePlugin(name); console.log(`❌ Plugin ${name} disabled`); }); pluginsCommand .command("load <path>") .description("Load a plugin from file or directory") .action(async (pluginPath: string) => { try { if (fs.statSync(pluginPath).isDirectory()) { await pluginManager.loadPluginsFromDirectory(pluginPath); } else { await pluginManager.loadPlugin(pluginPath); } console.log(`✅ Plugin(s) loaded from ${pluginPath}`); } catch (error: any) { console.error(`❌ Failed to load plugin: ${error.message}`); process.exit(1); } }); // Performance monitoring command const performanceCommand = program .command("performance") .description("Performance monitoring and optimization"); performanceCommand .command("status") .description("Show current performance metrics") .action(() => { console.log(performanceOptimizer.getPerformanceReport()); }); performanceCommand .command("optimize") .description("Optimize settings for current system") .action(() => { const memoryUsage = performanceOptimizer.getMemoryUsage(); const recommendedBatchSize = performanceOptimizer.calculateOptimalBatchSize(10000); console.log(` 🔧 Performance Optimization Recommendations: 💾 Current Memory Usage: ${memoryUsage.used.toFixed(2)}MB (${memoryUsage.percentage.toFixed(1)}%) 📦 Recommended Batch Size: ${recommendedBatchSize} ⚡ Performance Mode: ${performanceOptimizer.getMetrics().totalBatches > 0 ? 'Active' : 'Inactive'} 💡 Suggested CLI options: --performance-mode --batch-size ${recommendedBatchSize} --memory-limit ${Math.max(1024, Math.round(memoryUsage.total * 0.8))} `); }); // Enhanced relation detection command const relationsCommand = program .command("relations") .description("Analyze and detect relations in schemas"); relationsCommand .command("analyze <schema>") .description("Analyze relations in a schema file") .option("-t, --type <type>", "Schema type (auto-detected if not specified)") .option("-c, --confidence <threshold>", "Confidence threshold (0-1)", "0.5") .action(async (schemaPath: string, opts: any) => { try { const { RelationDetector } = await import('../src/relations/relationDetector'); const { SchemaRegistry } = await import('../src/schema-parsers/schemaRegistry'); const schemaRegistry = new SchemaRegistry(); const models = await schemaRegistry.parseSchema(schemaPath, opts.type); const detector = new RelationDetector({ confidenceThreshold: parseFloat(opts.confidence) }); const relations = detector.detectRelations(models); const averageConfidence = relations.reduce((sum, r) => sum + r.confidence, 0) / relations.length; console.log(` 🔗 Relation Analysis Results: 📄 Schema: ${schemaPath} 🔍 Relations Found: ${relations.length} 📊 Average Confidence: ${(averageConfidence * 100).toFixed(1)}% 📋 Detected Relations: ${relations.map(r => ` ${r.from} → ${r.to} (${r.field}) [${r.type}] - ${(r.confidence * 100).toFixed(1)}% confidence` ).join('\n')} `); } catch (error: any) { console.error(`❌ Failed to analyze relations: ${error.message}`); process.exit(1); } }); // === Here is the key fix: print logo ONLY here once, before parsing commands === (async () => { // Parse global options manually, since program.opts() isn't ready yet const args = process.argv.slice(2); const noLogo = args.includes('--no-logo') || args.includes('-q') || args.includes('--quiet'); const forceLogo = args.includes('--force-logo'); // Detect subcommand presence const knownCommands = ['generate', 'docs', 'help']; const firstArg = args[0]; const isSubcommand = firstArg && knownCommands.includes(firstArg); // Show logo only if no subcommand is given (just running `mocktail-cli` alone) // OR if running help/version commands // OR if forced by --force-logo // Otherwise don't show logo const noSubcommand = !isSubcommand && (args.length === 0 || ['-h', '--help', '-V', '--version'].some(cmd => args.includes(cmd))); if (await shouldShowLogo(forceLogo, noLogo, noSubcommand)) { await printMocktailLogo(); } // Custom colorful main help output program.helpInformation = function (): string { return ` ${chalk.hex('#00d8c9').bold('Usage:')} ${chalk.greenBright('mocktail-cli')} ${chalk.yellow('[options]')} ${chalk.yellow('[command]')} ${chalk.cyan('Schema-aware mock data generator')} ${chalk.magenta('Options:')} ${chalk.green('-V, --version')} ${chalk.gray('output the version number')} ${chalk.green('--no-logo')} ${chalk.gray('Suppress logo output globally')} ${chalk.green('-q, --quiet')} ${chalk.gray('Suppress output except errors globally')} ${chalk.green('--force-logo')} ${chalk.gray('Force show the logo animation even if shown before')} ${chalk.green('-h, --help')} ${chalk.gray('display help for command')} ${chalk.magenta('Commands:')} ${chalk.yellow('generate [options]')} ${chalk.gray('Generate mock data for any schema (Prisma, GraphQL, JSON Schema, OpenAPI)')} ${chalk.yellow('plugins [command]')} ${chalk.gray('Manage plugins (list, enable, disable, load)')} ${chalk.yellow('performance [cmd]')} ${chalk.gray('Performance monitoring and optimization')} ${chalk.yellow('relations [cmd]')} ${chalk.gray('Analyze and detect relations in schemas')} ${chalk.yellow('docs')} ${chalk.gray('Show full README.md documentation in terminal')} ${chalk.yellow('help [command]')} ${chalk.gray('display help for command')} ${chalk.cyan('For detailed documentation, run:')} ${chalk.green('mocktail-cli docs')} ${chalk.cyan('Or visit')} ${chalk.underline.blue('https://github.com/mockilo/mocktail-cli')} `; }; // Override generate command help with colorful custom output const generateCmd = program.commands.find(cmd => cmd.name() === "generate"); if (generateCmd) { generateCmd.helpInformation = function (): string { return ` ${chalk.hex('#00d8c9').bold('Usage:')} ${chalk.greenBright('mocktail-cli generate')} ${chalk.yellow('[options]')} ${chalk.cyan('Generate mock data for any schema (Prisma, GraphQL, JSON Schema, OpenAPI)')} ${chalk.magenta('Options:')} ${chalk.green('-c, --count <number>')} ${chalk.gray(`Number of records per model (default: "5")`)} ${chalk.green('-o, --out <directory>')} ${chalk.gray('Output directory')} ${chalk.green('-f, --format <type>')} ${chalk.gray(`Output format: json, sql, ts, csv (default: "json")`)} ${chalk.green('-s, --schema <path>')} ${chalk.gray(`Schema path (default: "./prisma/schema.prisma")`)} ${chalk.green('-t, --type <type>')} ${chalk.gray(`Schema type: prisma, graphql, json-schema, openapi, typescript, protobuf, avro, xml-schema, sql-ddl, mongoose, sequelize, joi, yup, zod (auto-detected if not specified)`)} ${chalk.green('-m, --models <models>')} ${chalk.gray('Comma-separated list of models (optional)')} ${chalk.green('--mock-config <path>')} ${chalk.gray('Path to mocktail-cli.config.js')} ${chalk.green('-d, --depth <number>')} ${chalk.gray(`Nested relation depth - depth > 1 enables relations (default: "1")`)} ${chalk.green('--no-nest')} ${chalk.gray('Disable nested relations (flat structure)')} ${chalk.green('--relations')} ${chalk.gray('Enable automatic relation generation (works with any depth)')} ${chalk.green('--dedupe')} ${chalk.gray('Enable deduplication of records')} ${chalk.green('--pretty')} ${chalk.gray('Pretty-print JSON output (default: true)')} ${chalk.green('--no-log')} ${chalk.gray('Suppress console logs during mock generation')} ${chalk.green('--seed')} ${chalk.gray('Insert mock data into DB')} ${chalk.green('--seed-value <number>')} ${chalk.gray('Seed value for reproducible data generation')} ${chalk.green('--loc