vibecode-party-starter
Version:
A Next.js starter project for vibecoding Saas apps with auth, payments, email, and more
459 lines (397 loc) • 14.2 kB
JavaScript
#!/usr/bin/env node
import fs from 'fs-extra';
import { execSync, spawn } from 'child_process';
import { fileURLToPath } from 'url';
import open from 'open';
import inquirer from 'inquirer';
import { dirname, join } from 'path';
// Get the directory name in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Get the project root directory (where the script is located)
const projectRoot = join(__dirname);
// Function to check if pnpm is installed
function checkPnpm() {
try {
execSync('pnpm --version', { stdio: 'ignore' });
return true;
} catch (err) {
return false;
}
}
// Function to install pnpm globally
async function installPnpm() {
console.log('\npnpm is not installed. Installing pnpm globally...');
try {
execSync('npm install -g pnpm', { stdio: 'inherit' });
console.log('\npnpm installed successfully! 🎉');
return true;
} catch (err) {
console.error('\nError installing pnpm. Please try installing it manually:');
console.error(' npm install -g pnpm\n');
return false;
}
}
// Function to find an available port
function findAvailablePort(startPort = 3000) {
try {
execSync(`lsof -i :${startPort}`, { stdio: 'ignore' });
return findAvailablePort(startPort + 1);
} catch (err) {
return startPort;
}
}
// Function to check if server is ready
async function isServerReady(port) {
try {
const response = await fetch(`http://localhost:${port}/get-started`);
return response.ok;
} catch (err) {
return false;
}
}
// Function to wait for server to be ready
async function waitForServer(port, maxAttempts = 30) {
console.log('\nWaiting for server to start...');
for (let i = 0; i < maxAttempts; i++) {
if (await isServerReady(port)) {
console.log('Server is ready!');
return true;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error('Server failed to start within the timeout period');
}
// Function to convert slug to title case
const slugToTitle = (slug) => {
return slug
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Function to slugify a string
const slugify = (str) => {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
};
async function customizePackageJson(defaultName = 'temp-vibecode-app') {
// Get project name first
const { name } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'What is your project directory name?',
default: defaultName
}
]);
// Get description and author
const { description, author, license } = await inquirer.prompt([
{
type: 'input',
name: 'description',
message: 'What is your project description?',
default: ''
},
{
type: 'input',
name: 'author',
message: 'What is the author name for this project?',
default: ''
},
{
type: 'input',
name: 'license',
message: 'What license would you like to use?',
default: 'None'
}
]);
// Get site configuration with defaults from project description
const answers = await inquirer.prompt([
{
type: 'input',
name: 'siteTitle',
message: 'What is your site title?',
default: slugToTitle(name)
},
{
type: 'input',
name: 'siteDescription',
message: 'What is your site description?',
default: description
},
{
type: 'input',
name: 'siteShortDescription',
message: 'What is your site short description?',
default: description
},
{
type: 'input',
name: 'siteUrl',
message: 'What is your site URL?',
default: `${slugify(name)}.vercel.app`
},
{
type: 'input',
name: 'siteX',
message: 'What is your X (Twitter) profile URL? (press enter to skip)',
default: ''
}
]);
// Combine the answers
const finalAnswers = {
name,
description,
author,
license,
...answers
};
return finalAnswers;
}
// Function to generate README content
function generateReadmeContent(projectName, description) {
return `# ${projectName}
${description}
---
This project was generated with [Vibecode Party Starter](https://starter.vibecode.party), a modern Next.js starter with authentication, database, storage, AI, and more.
`;
}
async function createProject(projectName, customValues) {
try {
// Create project directory in the current working directory (parent)
fs.mkdirSync(projectName);
process.chdir(projectName);
// Copy starter files from the project root
console.log('Copying starter files...');
fs.copySync(join(projectRoot, 'starter'), '.', {
overwrite: true,
filter: (src) => {
// Exclude .git directory
return !src.includes('.git');
}
});
// Generate and write README.md
const readmeContent = generateReadmeContent(customValues.name, customValues.description);
fs.writeFileSync('README.md', readmeContent);
// Copy configuration files from project root
console.log('Copying configuration files...');
const configFiles = [
'.gitignore',
'.cursor',
'.prettierignore',
'.prettierrc',
'.vscode'
];
for (const file of configFiles) {
const sourcePath = join(projectRoot, file);
if (fs.existsSync(sourcePath)) {
if (fs.lstatSync(sourcePath).isDirectory()) {
fs.copySync(sourcePath, join(process.cwd(), file), { overwrite: true });
} else {
fs.copyFileSync(sourcePath, join(process.cwd(), file));
}
}
}
// Read the starter package.json
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
// Update with custom values
const updatedPackageJson = {
...packageJson,
name: customValues.name,
version: '0.1.0',
description: customValues.description || packageJson.description,
author: customValues.author || packageJson.author,
license: customValues.license
};
// Write the updated package.json
fs.writeFileSync('package.json', JSON.stringify(updatedPackageJson, null, 2));
// Update LICENSE file based on user input
if (customValues.license === 'MIT') {
const currentYear = new Date().getFullYear();
const licenseContent = `MIT License
Copyright (c) ${currentYear} ${customValues.author || customValues.name}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`;
fs.writeFileSync('LICENSE', licenseContent);
} else if (customValues.license !== 'None') {
console.log(`\nNote: You selected the "${customValues.license}" license.`);
console.log('Please add your own LICENSE file with the appropriate license text.');
console.log('You can find standard license texts at: https://choosealicense.com/licenses/');
}
// Create default config object
const defaultConfig = {
title: "Vibecode Party Starter",
description: "A modern Next.js starter with authentication, database, storage, AI, and more.",
shortDescription: "Next.js Starter with Clerk, Supabase, AWS, AI, and more",
url: "",
shareImage: "",
x: "",
};
// Update config with custom values
const configContent = `export const siteConfig = {
title: "${customValues.siteTitle || defaultConfig.title}",
description: "${customValues.siteDescription || defaultConfig.description}",
shortDescription: "${customValues.siteShortDescription || defaultConfig.shortDescription}",
url: "${customValues.siteUrl || defaultConfig.url}",
shareImage: "${customValues.siteShareImage || defaultConfig.shareImage}",
x: "${customValues.siteX || defaultConfig.x}",
github: "",
logo: ""
} as const
export type SiteConfig = {
title: string
description: string
shortDescription: string
url: string
shareImage: string
x: string
github: string
logo: string
}`;
fs.writeFileSync('lib/config.ts', configContent);
// Install dependencies
console.log('Installing dependencies...');
execSync('pnpm install', { stdio: 'inherit' });
// Initialize Convex
console.log('\nInitializing Convex...');
try {
// Create a new Convex project using the new recommended command
execSync('npx convex dev --once --configure=new', {
stdio: 'inherit',
env: { ...process.env, FORCE_COLOR: '1' }
});
// Generate Convex types
console.log('\nGenerating Convex types...');
execSync('npx convex codegen', {
stdio: 'inherit',
env: { ...process.env, FORCE_COLOR: '1' }
});
} catch (err) {
console.log('\nNote: Convex initialization requires interactive input.');
console.log('Please run the following commands manually after the project is created:');
console.log('1. npx convex dev --once --configure=new');
console.log('2. npx convex codegen');
console.log('\nAfter initialization is complete, you can start the development server with:');
console.log('pnpm dev');
}
// Find an available port
const port = findAvailablePort();
console.log(`\nStarting the development server on port ${port}...`);
// Start the dev server
const devServer = spawn('pnpm', ['dev', '-p', port.toString()], {
stdio: 'inherit',
shell: true
});
// Wait for server to be ready
await waitForServer(port);
// Open browser
console.log('\nOpening browser...');
await open(`http://localhost:${port}/get-started`);
// Open new terminal in project directory
openNewTerminal(projectName);
// Open project in Cursor
openInCursor(projectName);
// Handle process termination
process.on('SIGINT', () => {
devServer.kill();
process.exit(0);
});
// Keep the script running
await new Promise(() => {});
} catch (err) {
console.error('Error:', err);
process.exit(1);
}
}
// Function to open new terminal in project directory
function openNewTerminal(projectDir) {
console.log(`Attempting to open new terminal in: ${projectDir}`);
// Get the absolute path to the project directory
// We need to go up one level from the current directory since we're already in the project directory
const absolutePath = join(process.cwd());
console.log(`Absolute path: ${absolutePath}`);
const command = process.platform === 'win32'
? `start cmd /k "cd ${absolutePath}"`
: `osascript -e 'tell app "Terminal" to do script "cd ${absolutePath}"'`;
console.log(`Running command: ${command}`);
try {
execSync(command, { stdio: 'inherit' });
console.log('\nOpened new terminal window in project directory');
} catch (err) {
console.error('\nError opening new terminal:', err);
console.log('\nCould not open new terminal window automatically');
console.log('Please cd into the project directory manually:');
console.log(` cd ${absolutePath}`);
}
}
// Function to open project in Cursor
function openInCursor(projectDir) {
console.log(`Attempting to open project in Cursor: ${projectDir}`);
// Get the absolute path to the project directory
// We need to use the current directory since we're already in the project directory
const absolutePath = join(process.cwd());
console.log(`Absolute path: ${absolutePath}`);
try {
if (process.platform === 'darwin') {
// macOS
console.log('Using macOS command to open Cursor');
execSync(`open -a Cursor "${absolutePath}"`, { stdio: 'inherit' });
console.log('Project opened in Cursor.');
} else {
// Linux/Windows
console.log('Using Linux/Windows command to open Cursor');
execSync(`cursor "${absolutePath}"`, { stdio: 'inherit' });
console.log('Project opened in Cursor.');
}
} catch (err) {
console.error('\nError opening Cursor:', err);
if (process.platform === 'darwin') {
console.log('⚠️ Could not open Cursor. Please ensure Cursor is installed in your Applications folder.');
} else {
console.log('⚠️ Cursor command not found. Please install Cursor or open the project manually.');
}
}
}
async function run() {
try {
// Check if pnpm is installed
if (!checkPnpm()) {
const installed = await installPnpm();
if (!installed) {
process.exit(1);
}
}
// Get the target directory from command line arguments or use a temporary name
const defaultDir = process.argv[2] || 'my-vibecode-app';
// Get custom package.json values first
const customValues = await customizePackageJson(defaultDir);
// Use the user's chosen name for the directory
const targetDir = customValues.name;
// Ensure the target directory doesn't already exist
if (fs.existsSync(targetDir)) {
console.error(`Error: Directory ${targetDir} already exists.`);
process.exit(1);
}
// Create the project with customization
await createProject(targetDir, customValues);
} catch (err) {
console.error('Error:', err);
process.exit(1);
}
}
run();