@sprouted/create-vibes
Version:
Create a new Vibes monorepo
378 lines (325 loc) • 11.2 kB
JavaScript
#!/usr/bin/env node
import { program } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora from 'ora';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const TEMPLATES_DIR = path.join(__dirname, 'templates');
// ASCII Art Logo
const logo = `
╭──────────────╮
│ VIBES │
│ ∿ │
│ ∿ ∿ │
│ ∿ ∿ ∿ │
╰──────────────╯
`;
async function createVibesProject(projectName, options) {
const isCI = process.env.CI === 'true';
if (!isCI) {
console.log(chalk.blue(logo));
console.log(chalk.blue.bold('Welcome to Vibes! 🌊\n'));
}
// Determine project path
const projectPath = path.resolve(projectName);
// Check if directory exists
if (fs.existsSync(projectPath)) {
console.error(chalk.red(`Error: Directory ${projectName} already exists!`));
process.exit(1);
}
// Interactive prompts if needed
const answers = await inquirer.prompt([
{
type: 'list',
name: 'packageManager',
message: 'Which package manager do you want to use?',
choices: ['pnpm', 'npm', 'yarn'],
default: 'pnpm',
when: !options.packageManager
},
{
type: 'list',
name: 'setupType',
message: 'How would you like to start?',
choices: [
{ name: 'Bare (just the structure)', value: 'bare' },
{ name: 'With example features', value: 'examples' },
{ name: 'With starter apps', value: 'apps' },
{ name: 'Full setup (apps + examples)', value: 'full' }
],
default: 'bare',
when: !options.bare && !options.examples && !options.apps
}
]);
// Handle the setup type
let setupType = 'bare';
if (options.bare) setupType = 'bare';
else if (options.examples && options.apps) setupType = 'full';
else if (options.examples) setupType = 'examples';
else if (options.apps) setupType = 'apps';
else setupType = answers.setupType;
// Additional prompts based on setup type
if (setupType === 'apps' || setupType === 'full') {
const appAnswers = await inquirer.prompt([
{
type: 'checkbox',
name: 'apps',
message: 'Which starter apps would you like?',
choices: [
{ name: 'Mobile (Expo)', value: 'mobile' },
{ name: 'Web (Vite + React)', value: 'web' },
{ name: 'API (Go)', value: 'api' }
],
default: ['mobile', 'api'],
when: !options.apps
}
]);
answers.apps = appAnswers.apps;
}
// Merge options with answers
const config = {
projectName,
projectPath,
packageManager: options.packageManager || answers.packageManager,
setupType,
includeExamples: setupType === 'examples' || setupType === 'full',
apps: setupType === 'apps' || setupType === 'full'
? (options.apps ? options.apps.split(',') : answers.apps)
: [],
skipInstall: options.skipInstall || false,
git: options.git !== false
};
// Create project
const spinner = isCI
? { text: '', start: () => console.log('Creating your Vibes project...'), succeed: (msg) => console.log(msg), fail: (msg) => console.error(msg) }
: ora('Creating your Vibes project...').start();
try {
// Create base structure
await createBaseStructure(config);
// Install dependencies (only for root)
if (!config.skipInstall) {
spinner.text = 'Installing dependencies...';
await installDependencies(config);
}
// Create selected apps
if (config.apps.length > 0) {
for (const app of config.apps) {
spinner.text = `Creating ${app} app...`;
await createApp(app, config);
}
}
// Create example features if requested
if (config.includeExamples) {
spinner.text = 'Creating example features...';
await createExampleFeatures(config);
}
// Initialize git
if (config.git) {
spinner.text = 'Initializing git...';
await initializeGit(config);
}
spinner.succeed(chalk.green('Project created successfully!'));
// Show next steps (simpler in CI)
if (!isCI) {
console.log('\n' + chalk.blue.bold('🎉 Your Vibes project is ready!'));
console.log('\nNext steps:');
console.log(chalk.cyan(` cd ${projectName}`));
console.log(chalk.cyan(` ./vibe create <feature-name> # Create your first feature`));
console.log(chalk.cyan(` ./vibe add <app-type> # Add an app when ready`));
if (config.setupType !== 'bare') {
console.log(chalk.cyan(` ${config.packageManager} dev # Start development`));
}
console.log('\n' + chalk.blue('Happy vibing! 🌊'));
} else {
console.log('Project created successfully!');
}
} catch (error) {
spinner.fail(chalk.red('Failed to create project'));
console.error(error);
process.exit(1);
}
}
async function createBaseStructure(config) {
const { projectPath } = config;
// Create directories
const dirs = [
'apps',
'features',
'shared',
'tools',
'docs',
'examples'
];
for (const dir of dirs) {
await fs.ensureDir(path.join(projectPath, dir));
}
// Copy base templates
const baseTemplate = path.join(TEMPLATES_DIR, 'base');
await fs.copy(baseTemplate, projectPath);
// Rename dotfiles that were renamed to avoid npm exclusion
const dotfiles = [
{ from: 'gitignore', to: '.gitignore' },
{ from: 'cursorrules', to: '.cursorrules' }
];
for (const file of dotfiles) {
const fromPath = path.join(projectPath, file.from);
const toPath = path.join(projectPath, file.to);
if (await fs.pathExists(fromPath)) {
await fs.rename(fromPath, toPath);
}
}
// Write vibes config with package manager choice
const vibesrcPath = path.join(projectPath, '.vibesrc');
const vibesConfig = {
packageManager: config.packageManager
};
await fs.writeJson(vibesrcPath, vibesConfig, { spaces: 2 });
// Create README files for empty directories
const readmeContent = {
apps: '# Apps\n\nApplications go here. Use `vibe add <app-type>` to create one.',
features: '# Features\n\nFeatures go here. Use `vibe create <feature-name>` to create one.',
shared: '# Shared\n\nShared utilities and components go here.',
examples: '# Examples\n\nExample implementations for reference.'
};
for (const [dir, content] of Object.entries(readmeContent)) {
await fs.writeFile(
path.join(projectPath, dir, 'README.md'),
content
);
}
// Update package.json with project name and package manager
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = `@${config.projectName}/root`;
// Set packageManager field based on user choice
if (config.packageManager === 'pnpm') {
packageJson.packageManager = 'pnpm@10.10.0';
} else if (config.packageManager === 'npm') {
// npm doesn't require a packageManager field
delete packageJson.packageManager;
} else if (config.packageManager === 'yarn') {
packageJson.packageManager = 'yarn@1.22.19';
}
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
async function installDependencies(config) {
const { projectPath, packageManager } = config;
const installCommand = {
npm: 'npm install',
yarn: 'yarn install',
pnpm: 'pnpm install'
};
execSync(installCommand[packageManager], {
cwd: projectPath,
stdio: 'inherit'
});
}
async function createApp(appType, config) {
const { projectPath } = config;
const appsDir = path.join(projectPath, 'apps');
switch (appType) {
case 'mobile':
execSync('npx create-expo-app todo-mobile', {
cwd: appsDir,
stdio: 'inherit'
});
// Copy our custom metro config
const metroConfig = path.join(TEMPLATES_DIR, 'apps', 'mobile', 'metro.config.js');
await fs.copy(metroConfig, path.join(appsDir, 'todo-mobile', 'metro.config.js'));
break;
case 'web':
execSync('npm create vite@latest todo-web -- --template react-ts', {
cwd: appsDir,
stdio: 'inherit'
});
break;
case 'api':
// Copy our Go API template
const apiTemplate = path.join(TEMPLATES_DIR, 'apps', 'api');
await fs.copy(apiTemplate, path.join(appsDir, 'todo-api'));
break;
}
}
async function createExampleFeatures(config) {
const { projectPath } = config;
const featuresDir = path.join(projectPath, 'features');
// Copy example features
const exampleFeatures = path.join(TEMPLATES_DIR, 'features');
await fs.copy(exampleFeatures, featuresDir);
}
async function initializeGit(config) {
const { projectPath } = config;
const isCI = process.env.CI === 'true';
try {
execSync('git init', {
cwd: projectPath,
stdio: 'inherit'
});
// Set git config in CI environments
if (isCI) {
execSync('git config user.email "ci@vibes.dev"', {
cwd: projectPath,
stdio: 'inherit'
});
execSync('git config user.name "Vibes CI"', {
cwd: projectPath,
stdio: 'inherit'
});
}
// Initial commit
execSync('git add -A', {
cwd: projectPath,
stdio: 'inherit'
});
execSync('git commit -m "Initial Vibes setup 🌊"', {
cwd: projectPath,
stdio: 'inherit'
});
} catch (error) {
// If git init fails in CI, just continue
if (isCI) {
console.log('Git initialization skipped in CI');
} else {
throw error;
}
}
}
// CLI Setup
program
.name('create-vibes')
.description('Create a new Vibes monorepo')
.version('0.1.0')
.argument('[project-name]', 'Name of your project')
.option('-p, --package-manager <manager>', 'Package manager to use (npm, yarn, pnpm)', 'pnpm')
.option('--bare', 'Create bare monorepo structure (no apps or examples)')
.option('--examples', 'Include example features')
.option('--apps <apps>', 'Comma-separated list of apps to create (mobile,web,api)')
.option('--no-git', 'Skip git initialization')
.option('--skip-install', 'Skip installing dependencies')
.action(async (projectName, options) => {
if (!projectName) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'What is your project name?',
default: 'my-vibes-app',
validate: (input) => {
if (!input.trim()) return 'Project name is required';
if (!/^[a-z0-9-]+$/.test(input)) {
return 'Project name can only contain lowercase letters, numbers, and hyphens';
}
return true;
}
}
]);
projectName = answers.projectName;
}
await createVibesProject(projectName, options);
});
program.parse();