UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

1,294 lines (1,221 loc) • 45.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initMonorepo = initMonorepo; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const os = __importStar(require("os")); const child_process_1 = require("child_process"); const prompts_1 = __importDefault(require("prompts")); const chalk_1 = __importDefault(require("chalk")); const monorepo_1 = require("../utils/monorepo"); const submodule_1 = require("../utils/submodule"); const spinner_1 = require("../utils/spinner"); const async_pool_1 = require("../utils/async-pool"); const config_1 = require("../utils/config"); // Template definitions const TEMPLATES = { blank: { name: 'Blank monorepo', description: 'Empty monorepo structure', dependencies: [], }, ecommerce: { name: 'E-commerce starter', description: 'Shell + product catalog + checkout', dependencies: ['@stripe/stripe-js', 'zustand', 'react-query'], }, dashboard: { name: 'Dashboard starter', description: 'Shell + analytics + user management', dependencies: ['recharts', 'd3', '@tanstack/react-table'], }, saas: { name: 'SaaS starter', description: 'Shell + auth + billing + admin', dependencies: ['@clerk/nextjs', '@stripe/stripe-js', 'prisma'], }, }; // Helper functions async function checkSystemRequirements() { const errors = []; const warnings = []; // Check Node.js version const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.split('.')[0].substring(1)); if (majorVersion < 16) { errors.push(`Node.js 16 or higher is required. You have ${nodeVersion}`); } else if (majorVersion < 18) { warnings.push(`Node.js 18+ is recommended for best performance. You have ${nodeVersion}`); } // Check disk space (simplified for cross-platform compatibility) try { // Basic disk space check - this is simplified and may not work on all systems // In a real implementation, you'd use a cross-platform library like 'check-disk-space' // For now, we just skip this check to maintain cross-platform compatibility } catch (e) { // Ignore disk space check errors on platforms where it's not supported } // Check git installation try { (0, child_process_1.execSync)('git --version', { stdio: 'ignore' }); } catch { warnings.push('Git is not installed. Version control features will be unavailable'); } return { errors, warnings }; } async function detectPackageManager() { // Check for lockfiles in current directory (parallel checks) const lockfileChecks = await Promise.all([ fs.pathExists('bun.lockb').then(exists => (exists ? 'bun' : null)), fs.pathExists('pnpm-lock.yaml').then(exists => (exists ? 'pnpm' : null)), fs.pathExists('yarn.lock').then(exists => (exists ? 'yarn' : null)), fs.pathExists('package-lock.json').then(exists => (exists ? 'npm' : null)), ]); // Return first found lockfile preference for (const manager of lockfileChecks) { if (manager) return manager; } // Check which package managers are installed (parallel execution) const pool = new async_pool_1.AsyncPool(4); const managerChecks = await Promise.allSettled([ pool.add(() => new Promise(resolve => { try { (0, child_process_1.execSync)('bun --version', { stdio: 'ignore', timeout: 5000 }); resolve('bun'); } catch { resolve(null); } })), pool.add(() => new Promise(resolve => { try { (0, child_process_1.execSync)('pnpm --version', { stdio: 'ignore', timeout: 5000 }); resolve('pnpm'); } catch { resolve(null); } })), pool.add(() => new Promise(resolve => { try { (0, child_process_1.execSync)('yarn --version', { stdio: 'ignore', timeout: 5000 }); resolve('yarn'); } catch { resolve(null); } })), pool.add(() => new Promise(resolve => { // npm is always available with Node.js resolve('npm'); })), ]); // Extract successful results const availableManagers = managerChecks .filter((result) => result.status === 'fulfilled' && result.value !== null) .map(result => result.value); // Prefer pnpm for monorepos, then bun, then yarn, then npm if (availableManagers.includes('pnpm')) return 'pnpm'; if (availableManagers.includes('bun')) return 'bun'; if (availableManagers.includes('yarn')) return 'yarn'; return 'npm'; } async function savePreset(name, config) { const presetsDir = path.join(os.homedir(), '.re-shell', 'presets'); await fs.ensureDir(presetsDir); await fs.writeJSON(path.join(presetsDir, `${name}.json`), config, { spaces: 2 }); } async function loadPreset(name) { const presetsDir = path.join(os.homedir(), '.re-shell', 'presets'); const presetPath = path.join(presetsDir, `${name}.json`); if (await fs.pathExists(presetPath)) { return fs.readJSON(presetPath); } return null; } /** * Initialize a new monorepo workspace with Re-Shell CLI * * @param name - Name of the monorepo * @param options - Initialization options */ async function initMonorepo(name, options = {}) { // System requirements check const { errors, warnings } = await checkSystemRequirements(); if (errors.length > 0) { console.error(chalk_1.default.red('System requirements not met:')); errors.forEach(err => console.error(chalk_1.default.red(` • ${err}`))); return; } if (warnings.length > 0 && !options.yes) { console.warn(chalk_1.default.yellow('Warnings:')); warnings.forEach(warn => console.warn(chalk_1.default.yellow(` • ${warn}`))); } // Auto-detect package manager if not specified if (!options.packageManager) { options.packageManager = await detectPackageManager(); if (options.debug) { console.log(chalk_1.default.gray(`Auto-detected package manager: ${options.packageManager}`)); } } // Check if package manager is installed if (options.packageManager) { try { (0, child_process_1.execSync)(`${options.packageManager} --version`, { stdio: 'ignore' }); } catch { console.error(chalk_1.default.red(`Error: ${options.packageManager} is not installed`)); console.log(chalk_1.default.gray(`You can install it with: npm install -g ${options.packageManager}`)); return; } } // Load preset if specified let presetConfig = null; if (options.preset) { presetConfig = await loadPreset(options.preset); if (!presetConfig) { console.error(chalk_1.default.red(`Error: Preset "${options.preset}" not found`)); return; } // Merge preset with options (options take precedence) options = { ...presetConfig, ...options }; } // Validate project name format const validateProjectName = (value) => { if (!value || value.trim() === '') { return 'Project name cannot be empty'; } if (!/^[a-z0-9-]+$/.test(value.toLowerCase())) { return 'Project name can only contain lowercase letters, numbers, and hyphens'; } if (value.startsWith('-') || value.endsWith('-')) { return 'Project name cannot start or end with a hyphen'; } if (value.length > 214) { return 'Project name is too long (max 214 characters)'; } return true; }; // Validate the initial name const nameValidation = validateProjectName(name); if (nameValidation !== true) { console.error(chalk_1.default.red(`Error: ${nameValidation}`)); return; } let normalizedName = name.toLowerCase().replace(/\s+/g, '-'); let projectPath = path.resolve(process.cwd(), normalizedName); // Check if directory already exists while (fs.existsSync(projectPath) && !options.force) { const { action } = await (0, prompts_1.default)({ type: 'select', name: 'action', message: `Directory "${normalizedName}" already exists. What would you like to do?`, choices: [ { title: 'Enter a new project name', value: 'rename' }, { title: 'Overwrite existing directory', value: 'overwrite' }, { title: 'Cancel', value: 'cancel' }, ], initial: 0, }); if (action === 'cancel') { console.log(chalk_1.default.yellow('Operation cancelled.')); return; } if (action === 'overwrite') { break; } if (action === 'rename') { const { newName } = await (0, prompts_1.default)({ type: 'text', name: 'newName', message: 'Enter a new project name:', initial: normalizedName + '-2', validate: value => { const validation = validateProjectName(value); if (validation !== true) { return validation; } const normalized = value.toLowerCase().replace(/\s+/g, '-'); const newPath = path.resolve(process.cwd(), normalized); if (fs.existsSync(newPath)) { return `Directory "${normalized}" already exists`; } return true; }, }); if (!newName) { console.log(chalk_1.default.yellow('Operation cancelled.')); return; } normalizedName = newName.toLowerCase().replace(/\s+/g, '-'); projectPath = path.resolve(process.cwd(), normalizedName); } } // Interactive prompts for missing options (skip if --yes flag is used) let responses = {}; // Force non-interactive mode if not in a TTY environment or when --yes is used const forceNonInteractive = !process.stdout.isTTY || process.env.CI || options.yes; if (!forceNonInteractive) { // Only run prompts if in interactive mode and --yes flag is not used const promptsToRun = []; // Welcome message console.log(chalk_1.default.bold.cyan('\n🚀 Welcome to Re-Shell CLI!\n')); console.log(chalk_1.default.gray("Let's create your new monorepo workspace.\n")); // Project type selection (for future full-stack support) promptsToRun.push({ type: 'select', name: 'projectType', message: 'Select project type:', choices: [ { title: 'Microfrontend Monorepo', value: 'frontend', description: 'Frontend applications with module federation', }, { title: 'Full-Stack Monorepo (Coming Soon)', value: 'fullstack', description: 'Frontend + Backend services', disabled: true, }, { title: 'Microservices (Coming Soon)', value: 'backend', description: 'Backend services only', disabled: true, }, ], initial: 0, }); // Template selection promptsToRun.push({ type: 'select', name: 'template', message: 'Start with a template?', choices: [ { title: 'Blank monorepo', value: 'blank' }, { title: 'E-commerce starter', value: 'ecommerce', description: 'Shell + product catalog + checkout', }, { title: 'Dashboard starter', value: 'dashboard', description: 'Shell + analytics + user management', }, { title: 'SaaS starter', value: 'saas', description: 'Shell + auth + billing + admin' }, ], initial: 0, }); // TypeScript selection promptsToRun.push({ type: 'confirm', name: 'typescript', message: 'Use TypeScript?', initial: true, }); if (!options.packageManager) { const detectedPM = await detectPackageManager(); promptsToRun.push({ type: 'select', name: 'packageManager', message: 'Select a package manager:', choices: [ { title: `pnpm (recommended)${detectedPM === 'pnpm' ? ' ✓' : ''}`, value: 'pnpm' }, { title: `yarn${detectedPM === 'yarn' ? ' ✓' : ''}`, value: 'yarn' }, { title: `npm${detectedPM === 'npm' ? ' ✓' : ''}`, value: 'npm' }, { title: `bun (experimental)${detectedPM === 'bun' ? ' ✓' : ''}`, value: 'bun' }, ], initial: detectedPM === 'pnpm' ? 0 : detectedPM === 'yarn' ? 1 : detectedPM === 'npm' ? 2 : detectedPM === 'bun' ? 3 : 0, }); } if (options.git === undefined) { promptsToRun.push({ type: 'confirm', name: 'git', message: 'Initialize Git repository?', initial: true, }); } if (options.submodules === undefined) { promptsToRun.push({ type: 'confirm', name: 'submodules', message: 'Set up Git submodule support?', initial: true, }); } // Only ask about custom structure if not using --yes promptsToRun.push({ type: 'confirm', name: 'customStructure', message: 'Customize directory structure?', initial: false, }); // Ask about saving as preset promptsToRun.push({ type: 'confirm', name: 'saveAsPreset', message: 'Save this configuration as a preset for future use?', initial: false, }); if (promptsToRun.length > 0) { // Stop spinner before prompts to avoid interference if (options.spinner) { options.spinner.stop(); } responses = await (0, prompts_1.default)(promptsToRun); // Restart spinner after prompts if (options.spinner) { options.spinner.start(); } } } let customStructure = {}; if (responses.customStructure && !forceNonInteractive) { // Stop spinner for custom structure prompts if (options.spinner) { options.spinner.stop(); } const structureResponses = await (0, prompts_1.default)([ { type: 'text', name: 'apps', message: 'Applications directory name:', initial: 'apps', }, { type: 'text', name: 'packages', message: 'Packages directory name:', initial: 'packages', }, ]); customStructure = structureResponses; // Restart spinner if (options.spinner) { options.spinner.start(); } } // Merge options with responses const finalOptions = { packageManager: options.packageManager || responses.packageManager || 'pnpm', git: options.git !== undefined ? options.git : responses.git, submodules: options.submodules !== undefined ? options.submodules : responses.submodules, structure: { ...options.structure, ...customStructure }, template: options.template || responses.template || 'blank', typescript: responses.typescript !== undefined ? responses.typescript : true, skipInstall: options.skipInstall || false, }; // Save preset if requested if (responses.saveAsPreset && !forceNonInteractive) { if (options.spinner) { options.spinner.stop(); } const { presetName } = await (0, prompts_1.default)({ type: 'text', name: 'presetName', message: 'Enter a name for this preset:', initial: 'my-preset', validate: value => { if (!value || value.trim() === '') { return 'Preset name cannot be empty'; } if (!/^[a-z0-9-]+$/.test(value.toLowerCase())) { return 'Preset name can only contain lowercase letters, numbers, and hyphens'; } return true; }, }); if (presetName) { await savePreset(presetName, { packageManager: finalOptions.packageManager, git: finalOptions.git, submodules: finalOptions.submodules, structure: finalOptions.structure, template: finalOptions.template, typescript: finalOptions.typescript, }); console.log(chalk_1.default.green(`✓ Preset "${presetName}" saved successfully!`)); console.log(chalk_1.default.gray(`Use it next time with: rs init my-project --preset ${presetName}`)); } if (options.spinner) { options.spinner.start(); } } const spinner = options.spinner; try { // Remove existing directory if force option is used or overwrite was chosen if (fs.existsSync(projectPath)) { if (spinner) spinner.setText('Removing existing directory...'); (0, spinner_1.flushOutput)(); await fs.remove(projectPath); } // Initialize monorepo structure if (spinner) spinner.setText('Creating monorepo structure...'); (0, spinner_1.flushOutput)(); await (0, monorepo_1.initializeMonorepo)(normalizedName, finalOptions.packageManager, finalOptions.structure); // Initialize Git repository if (finalOptions.git) { if (spinner) spinner.setText('Initializing Git repository...'); (0, spinner_1.flushOutput)(); await (0, submodule_1.initializeGitRepository)(projectPath); // Add .gitignore const gitignore = `# Dependencies node_modules/ .pnp .pnp.js # Testing coverage/ *.lcov .nyc_output # Production dist/ build/ out/ .next/ .nuxt/ .cache/ # Misc .DS_Store *.log .env.local .env.development.local .env.test.local .env.production.local # IDE .vscode/* !.vscode/settings.json !.vscode/extensions.json .idea/ *.swp *.swo # Module Federation .webpack-cache/ .module-federation/ # Package managers .npm/ .yarn/ .pnpm-store/ `; await fs.writeFile(path.join(projectPath, '.gitignore'), gitignore); } // Set up submodule support if (finalOptions.submodules) { if (spinner) spinner.setText('Setting up submodule support...'); (0, spinner_1.flushOutput)(); // Submodule support is enabled but we don't create the helper files await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for UX } // Create additional configuration files if (spinner) spinner.setText('Creating configuration files...'); (0, spinner_1.flushOutput)(); await createAdditionalConfigs(projectPath, finalOptions); // Apply template if not blank if (finalOptions.template && finalOptions.template !== 'blank') { if (spinner) spinner.setText(`Applying ${finalOptions.template} template...`); (0, spinner_1.flushOutput)(); await applyTemplate(projectPath, finalOptions.template, finalOptions); } // Install dependencies if not skipped if (!finalOptions.skipInstall) { if (spinner) spinner.setText('Installing dependencies...'); (0, spinner_1.flushOutput)(); try { const installCmd = finalOptions.packageManager === 'npm' ? 'npm install' : `${finalOptions.packageManager} install`; (0, child_process_1.execSync)(installCmd, { cwd: projectPath, stdio: options.debug ? 'inherit' : 'ignore', }); // Run vulnerability scan if (spinner) spinner.setText('Scanning for vulnerabilities...'); (0, spinner_1.flushOutput)(); try { const auditCmd = finalOptions.packageManager === 'npm' ? 'npm audit --json' : finalOptions.packageManager === 'yarn' ? 'yarn audit --json' : finalOptions.packageManager === 'pnpm' ? 'pnpm audit --json' : null; if (auditCmd) { const auditResult = (0, child_process_1.execSync)(auditCmd, { cwd: projectPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], }); const audit = JSON.parse(auditResult); const vulnerabilities = audit.metadata?.vulnerabilities || {}; const total = Object.values(vulnerabilities).reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0); if (typeof total === 'number' && total > 0) { console.log(chalk_1.default.yellow(`\n⚠️ Found ${total} vulnerabilities`)); if ((typeof vulnerabilities.high === 'number' && vulnerabilities.high > 0) || (typeof vulnerabilities.critical === 'number' && vulnerabilities.critical > 0)) { console.log(chalk_1.default.red(` Critical: ${vulnerabilities.critical || 0}, High: ${vulnerabilities.high || 0}`)); } console.log(chalk_1.default.gray(` Run '${finalOptions.packageManager} audit fix' to fix them\n`)); } } } catch { // Ignore audit errors } } catch (error) { console.warn(chalk_1.default.yellow('\nWarning: Failed to install dependencies')); console.log(chalk_1.default.gray(`You can install them manually with: ${finalOptions.packageManager} install\n`)); } } // Initialize git and make initial commit if (finalOptions.git) { if (spinner) spinner.setText('Creating initial commit...'); (0, spinner_1.flushOutput)(); try { (0, child_process_1.execSync)('git add -A', { cwd: projectPath, stdio: 'ignore' }); (0, child_process_1.execSync)('git commit -m "Initial commit from Re-Shell CLI"', { cwd: projectPath, stdio: 'ignore', }); } catch { // Ignore git errors } } // All operations completed successfully - the main CLI will handle the success message // Store success info for the CLI to display global.__RE_SHELL_INIT_SUCCESS__ = { name: normalizedName, packageManager: finalOptions.packageManager, submodules: finalOptions.submodules, template: finalOptions.template, nextSteps: [ `cd ${normalizedName}`, finalOptions.skipInstall ? `${finalOptions.packageManager} install` : null, 'rs create shell-app --framework react-ts', 'rs create my-app --framework vue-ts', `${finalOptions.packageManager} run dev`, ].filter(Boolean), }; } catch (error) { // Clean up on failure if (fs.existsSync(projectPath)) { try { await fs.remove(projectPath); } catch { // Ignore cleanup errors } } // Provide helpful error messages if (error.code === 'EACCES') { console.error(chalk_1.default.red('Error: Permission denied. Try running with sudo or check directory permissions.')); } else if (error.code === 'ENOSPC') { console.error(chalk_1.default.red('Error: Not enough disk space.')); } else { console.error(chalk_1.default.red('Error initializing monorepo:'), error.message || error); } throw error; } } async function applyTemplate(projectPath, template, options) { const templateConfig = TEMPLATES[template]; if (!templateConfig) return; // Create template-specific structure const appsDir = path.join(projectPath, options.structure?.apps || 'apps'); const packagesDir = path.join(projectPath, options.structure?.packages || 'packages'); switch (template) { case 'ecommerce': // Create e-commerce specific apps await fs.ensureDir(path.join(appsDir, 'shell')); await fs.ensureDir(path.join(appsDir, 'product-catalog')); await fs.ensureDir(path.join(appsDir, 'checkout')); await fs.ensureDir(path.join(packagesDir, 'shared-ui')); await fs.ensureDir(path.join(packagesDir, 'cart-state')); break; case 'dashboard': // Create dashboard specific apps await fs.ensureDir(path.join(appsDir, 'shell')); await fs.ensureDir(path.join(appsDir, 'analytics')); await fs.ensureDir(path.join(appsDir, 'user-management')); await fs.ensureDir(path.join(packagesDir, 'chart-components')); await fs.ensureDir(path.join(packagesDir, 'data-utils')); break; case 'saas': // Create SaaS specific apps await fs.ensureDir(path.join(appsDir, 'shell')); await fs.ensureDir(path.join(appsDir, 'auth')); await fs.ensureDir(path.join(appsDir, 'billing')); await fs.ensureDir(path.join(appsDir, 'admin')); await fs.ensureDir(path.join(packagesDir, 'auth-utils')); await fs.ensureDir(path.join(packagesDir, 'payment-integration')); break; } // Add template-specific dependencies to root package.json if (templateConfig.dependencies.length > 0) { const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = await fs.readJSON(packageJsonPath); if (!packageJson.dependencies) { packageJson.dependencies = {}; } // Add template dependencies (versions would be fetched from registry in real implementation) templateConfig.dependencies.forEach(dep => { packageJson.dependencies[dep] = 'latest'; }); await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 }); } // Create template-specific README const readmePath = path.join(projectPath, 'README.md'); let existingReadme = ''; try { existingReadme = await fs.readFile(readmePath, 'utf8'); } catch { // README doesn't exist yet, create a basic one existingReadme = `# ${path.basename(projectPath)} A Re-Shell monorepo project created with the CLI. ## Getting Started 1. Install dependencies: \`\`\`bash ${options.packageManager} install \`\`\` 2. Start development: \`\`\`bash ${options.packageManager} run dev \`\`\` `; } const templateReadme = `${existingReadme} ## Template: ${templateConfig.name} ${templateConfig.description} ### Template Structure ${template === 'ecommerce' ? ` - \`apps/shell\` - Main application shell - \`apps/product-catalog\` - Product browsing and search - \`apps/checkout\` - Shopping cart and checkout flow - \`packages/shared-ui\` - Shared UI components - \`packages/cart-state\` - Cart state management ` : template === 'dashboard' ? ` - \`apps/shell\` - Main application shell - \`apps/analytics\` - Analytics and reporting - \`apps/user-management\` - User administration - \`packages/chart-components\` - Reusable chart components - \`packages/data-utils\` - Data processing utilities ` : template === 'saas' ? ` - \`apps/shell\` - Main application shell - \`apps/auth\` - Authentication and authorization - \`apps/billing\` - Subscription and billing management - \`apps/admin\` - Admin dashboard - \`packages/auth-utils\` - Authentication utilities - \`packages/payment-integration\` - Payment processing ` : ''} ### Getting Started with ${templateConfig.name} 1. Create the shell application: \`\`\`bash rs create shell --framework react-ts \`\`\` 2. Create additional applications based on the template 3. Start the development server: \`\`\`bash ${options.packageManager} run dev \`\`\` `; await fs.writeFile(readmePath, templateReadme); } async function createAdditionalConfigs(projectPath, options) { // Create .nvmrc for Node.js version await fs.writeFile(path.join(projectPath, '.nvmrc'), '18.17.0\n'); // Create .env.example const envExample = `# Re-Shell Configuration NODE_ENV=development PORT=3000 # Module Federation FEDERATION_REMOTE_URL=http://localhost:3001 # API Configuration (for future full-stack support) API_BASE_URL=http://localhost:4000 API_TIMEOUT=30000 # Feature Flags ENABLE_HOT_RELOAD=true ENABLE_ANALYTICS=false `; await fs.writeFile(path.join(projectPath, '.env.example'), envExample); await fs.writeFile(path.join(projectPath, '.env'), envExample); // Create .editorconfig const editorConfig = `root = true [*] charset = utf-8 end_of_line = lf indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 [*.json] indent_size = 2 `; await fs.writeFile(path.join(projectPath, '.editorconfig'), editorConfig); // Create VSCode settings const vscodeDir = path.join(projectPath, '.vscode'); await fs.ensureDir(vscodeDir); const vscodeSettings = { 'typescript.preferences.includePackageJsonAutoImports': 'on', 'typescript.suggest.autoImports': true, 'editor.formatOnSave': true, 'editor.codeActionsOnSave': { 'source.fixAll.eslint': true, }, 'files.associations': { '*.json': 'jsonc', }, 'search.exclude': { '**/node_modules': true, '**/dist': true, '**/build': true, '**/.next': true, '**/.nuxt': true, }, }; await fs.writeFile(path.join(vscodeDir, 'settings.json'), JSON.stringify(vscodeSettings, null, 2)); const vscodeExtensions = { recommendations: [ 'esbenp.prettier-vscode', 'dbaeumer.vscode-eslint', 'bradlc.vscode-tailwindcss', 'ms-vscode.vscode-typescript-next', 'ms-vscode.vscode-json', 'redhat.vscode-yaml', 'ms-vscode.vscode-npm-script', ], }; await fs.writeFile(path.join(vscodeDir, 'extensions.json'), JSON.stringify(vscodeExtensions, null, 2)); // Create GitHub workflows if Git is enabled if (options.git) { const githubDir = path.join(projectPath, '.github', 'workflows'); await fs.ensureDir(githubDir); const ciWorkflow = `name: CI on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x] steps: - uses: actions/checkout@v3 with: submodules: recursive - name: Use Node.js \${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: \${{ matrix.node-version }} cache: '${options.packageManager}' - name: Install dependencies run: ${options.packageManager} install - name: Run linting run: ${options.packageManager} run lint - name: Run tests run: ${options.packageManager} run test - name: Build run: ${options.packageManager} run build `; await fs.writeFile(path.join(githubDir, 'ci.yml'), ciWorkflow); } // Create prettier configuration const prettierConfig = { semi: true, trailingComma: 'es5', singleQuote: true, printWidth: 100, tabWidth: 2, useTabs: false, bracketSpacing: true, arrowParens: 'always', endOfLine: 'lf', }; await fs.writeFile(path.join(projectPath, '.prettierrc'), JSON.stringify(prettierConfig, null, 2)); const prettierIgnore = `# Dependencies node_modules/ # Build outputs dist/ build/ .next/ .nuxt/ out/ # Package manager files pnpm-lock.yaml yarn.lock package-lock.json # Generated files *.generated.* `; await fs.writeFile(path.join(projectPath, '.prettierignore'), prettierIgnore); // Create ESLint configuration const eslintConfig = { root: true, env: { browser: true, es2021: true, node: true, }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 'latest', sourceType: 'module', }, plugins: ['react', '@typescript-eslint'], rules: { 'react/react-in-jsx-scope': 'off', 'react/prop-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'warn', }, settings: { react: { version: 'detect', }, }, }; await fs.writeFile(path.join(projectPath, '.eslintrc.json'), JSON.stringify(eslintConfig, null, 2)); // Create commitlint configuration const commitlintConfig = `module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [ 2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert', ], ], }, }; `; await fs.writeFile(path.join(projectPath, 'commitlint.config.js'), commitlintConfig); // Create husky pre-commit hook if (options.git) { const huskyDir = path.join(projectPath, '.husky'); await fs.ensureDir(huskyDir); const preCommit = `#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" ${options.packageManager} run lint-staged `; await fs.writeFile(path.join(huskyDir, 'pre-commit'), preCommit, { mode: 0o755 }); const commitMsg = `#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" ${options.packageManager} run commitlint --edit $1 `; await fs.writeFile(path.join(huskyDir, 'commit-msg'), commitMsg, { mode: 0o755 }); } // Create lint-staged configuration const lintStagedConfig = { '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], '*.{json,md,yml,yaml}': ['prettier --write'], }; await fs.writeFile(path.join(projectPath, '.lintstagedrc.json'), JSON.stringify(lintStagedConfig, null, 2)); // Create turborepo configuration const turboConfig = { $schema: 'https://turbo.build/schema.json', pipeline: { build: { dependsOn: ['^build'], outputs: ['dist/**', '.next/**', 'build/**'], }, test: { dependsOn: ['build'], outputs: ['coverage/**'], }, lint: { outputs: [], }, dev: { cache: false, persistent: true, }, }, }; await fs.writeFile(path.join(projectPath, 'turbo.json'), JSON.stringify(turboConfig, null, 2)); // Create Docker support files const dockerfile = `# Multi-stage build for production FROM node:18-alpine AS base # Install dependencies only when needed FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app # Copy package files COPY package*.json ./ COPY ${options.packageManager === 'pnpm' ? 'pnpm-lock.yaml' : options.packageManager === 'yarn' ? 'yarn.lock' : options.packageManager === 'bun' ? 'bun.lockb' : 'package-lock.json'} ./ # Install dependencies RUN ${options.packageManager === 'pnpm' ? 'npm install -g pnpm && pnpm install --frozen-lockfile' : options.packageManager === 'yarn' ? 'yarn install --frozen-lockfile' : options.packageManager === 'bun' ? 'npm install -g bun && bun install --frozen-lockfile' : 'npm ci'} # Build stage FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN ${options.packageManager} run build # Production stage FROM base AS runner WORKDIR /app ENV NODE_ENV production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["node", "dist/index.js"] `; await fs.writeFile(path.join(projectPath, 'Dockerfile'), dockerfile); const dockerignore = `node_modules npm-debug.log .next .git .gitignore README.md Dockerfile .dockerignore .env.local .env.development.local .env.test.local .env.production.local `; await fs.writeFile(path.join(projectPath, '.dockerignore'), dockerignore); // Create docker-compose.yml const dockerCompose = `version: '3.8' services: app: build: context: . dockerfile: Dockerfile ports: - "3000:3000" environment: - NODE_ENV=production volumes: - .:/app - /app/node_modules command: ${options.packageManager} run dev # Add more services as needed (database, cache, etc.) `; await fs.writeFile(path.join(projectPath, 'docker-compose.yml'), dockerCompose); // Create CONTRIBUTING.md const contributing = `# Contributing to ${path.basename(projectPath)} We love your input! We want to make contributing to this project as easy and transparent as possible. ## Development Process 1. Fork the repo and create your branch from \`main\` 2. If you've added code that should be tested, add tests 3. If you've changed APIs, update the documentation 4. Ensure the test suite passes 5. Make sure your code lints 6. Issue that pull request! ## Code Style - 2 spaces for indentation - Use TypeScript for new code - Follow the existing code style - Run \`${options.packageManager} run lint\` before committing ## Commit Messages We use [Conventional Commits](https://www.conventionalcommits.org/): - \`feat:\` new feature - \`fix:\` bug fix - \`docs:\` documentation changes - \`style:\` formatting, missing semicolons, etc. - \`refactor:\` code change that neither fixes a bug nor adds a feature - \`test:\` adding missing tests - \`chore:\` maintain ## License By contributing, you agree that your contributions will be licensed under the same license as the project. `; await fs.writeFile(path.join(projectPath, 'CONTRIBUTING.md'), contributing); // Create SECURITY.md const security = `# Security Policy ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to [security@example.com](mailto:security@example.com). You should receive a response within 48 hours. If for some reason you do not, please follow up via email to ensure we received your original message. ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.x.x | :white_check_mark: | | < 1.0 | :x: | ## Preferred Languages We prefer all communications to be in English. `; await fs.writeFile(path.join(projectPath, 'SECURITY.md'), security); // Create renovate.json for automated dependency updates const renovateConfig = { extends: ['config:base'], packageRules: [ { matchUpdateTypes: ['minor', 'patch', 'pin', 'digest'], automerge: true, }, ], prConcurrentLimit: 3, prHourlyLimit: 2, }; await fs.writeFile(path.join(projectPath, 'renovate.json'), JSON.stringify(renovateConfig, null, 2)); // Create jest.config.js const jestConfig = `module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/apps', '<rootDir>/packages'], testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, collectCoverageFrom: [ '**/*.{js,jsx,ts,tsx}', '!**/*.d.ts', '!**/node_modules/**', '!**/.next/**', '!**/dist/**', '!**/coverage/**', ], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }, }; `; await fs.writeFile(path.join(projectPath, 'jest.config.js'), jestConfig); // Create Re-Shell project configuration const projectName = path.basename(projectPath); await config_1.configManager.createProjectConfig(projectName, { type: 'monorepo', packageManager: options.packageManager, framework: 'react-ts', template: options.template || 'blank', workspaces: { root: '.', patterns: [ `${options.structure?.apps || 'apps'}/*`, `${options.structure?.packages || 'packages'}/*` ], types: ['app', 'package'] }, git: { submodules: options.submodules, hooks: true, conventionalCommits: true } }, projectPath); }