UNPKG

@evitcastudio/kit

Version:

A single-player/multiplayer framework for the Vylocity Game Engine.

236 lines (235 loc) 9.89 kB
import { intro, outro, text, select, confirm, spinner, note, isCancel, cancel } from '@clack/prompts'; import chalk from 'chalk'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync, execSync } from 'node:child_process'; import os from 'node:os'; import packageJSON from '../../package.json'; /** * Gets the git user name and email from the local system. * @returns The formatted author string. */ function getGitUser() { try { const name = spawnSync('git', ['config', '--get', 'user.name'], { encoding: 'utf8' }).stdout.trim(); const email = spawnSync('git', ['config', '--get', 'user.email'], { encoding: 'utf8' }).stdout.trim(); if (name && email) { return `${name} <${email}>`; } return name || 'Author'; } catch { return 'Author'; } } function checkBun() { const result = spawnSync('bun', ['--version'], { stdio: 'ignore', shell: true }); return !result.error && result.status === 0; } /** * Installs Bun using the official installation scripts. * @returns True if the installation was successful. */ function installBun() { const isWindows = os.platform() === 'win32'; const command = isWindows ? 'powershell -c "irm bun.sh/install.ps1 | iex"' : 'curl -fsSL https://bun.sh/install | bash'; try { execSync(command, { stdio: 'inherit' }); return true; } catch { return false; } } /** * Recursively copy template files and replace placeholders. * @param pSrc - Source template directory. * @param pDest - Destination project directory. * @param pProjectName - Project name for placeholder replacement. * @param pVersion - Version number for placeholder replacement. * @param pAuthor - Author for placeholder replacement. */ function copyTemplate(pSrc, pDest, pProjectName, pVersion, pAuthor) { if (!fs.existsSync(pDest)) { fs.mkdirSync(pDest, { recursive: true }); } const files = fs.readdirSync(pSrc); for (const file of files) { const srcPath = path.join(pSrc, file); // Rename _gitignore back to .gitignore in the destination const destFile = file === '_gitignore' ? '.gitignore' : file; const destPath = path.join(pDest, destFile); const stats = fs.statSync(srcPath); if (stats.isDirectory()) { if (file === '.git') continue; // Do not copy .git directories copyTemplate(srcPath, destPath, pProjectName, pVersion, pAuthor); } else { const ext = path.extname(file).toLowerCase(); const textExtensions = ['.ts', '.js', '.json', '.html', '.css', '.md', '.txt']; if (textExtensions.includes(ext)) { let content = fs.readFileSync(srcPath, 'utf8'); // Replace placeholders content = content.replace(/{{PROJECT_NAME}}/g, pProjectName); content = content.replace(/{{VERSION}}/g, pVersion); content = content.replace(/{{PROJECT_AUTHOR}}/g, pAuthor); fs.writeFileSync(destPath, content); } else { // Binary-safe copy for other files (like .vyi, images, etc.) fs.copyFileSync(srcPath, destPath); } } } } /** * Process the initialization of a new project. * @param pOptions - Options for initialization. */ export async function processInit(pOptions) { intro(chalk.cyan(`Kit CLI v${packageJSON.version}`)); // Environment Check if (!checkBun()) { note(`Kit requires the Bun runtime to build and run projects.\nYou can download it manually at ${chalk.cyan('https://bun.sh/')}`, 'Bun Not Found'); const install = await confirm({ message: 'Would you like to install Bun automatically now?', initialValue: true, }); if (isCancel(install) || !install) { cancel(`Please install Bun manually to use Kit: ${chalk.cyan('https://bun.sh/')}\nNote: Kit projects cannot build or run without Bun.`); process.exit(1); } const sInstall = spinner(); sInstall.start('Installing Bun...'); const success = installBun(); if (!success) { sInstall.stop(chalk.red('Automatic installation failed.')); note(`Please install Bun manually: ${chalk.cyan('https://bun.sh/')}`, 'Manual Installation Required'); process.exit(1); } sInstall.stop(); } let projectName = pOptions.projectName; let gameType = 'single'; // Interactive Walkthrough if (!pOptions.single && !pOptions.multi && !projectName) { const name = await text({ message: 'What is the name of your project?', placeholder: 'my-amazing-project', validate(pValue) { if (!pValue || pValue.length === 0) return 'Project name is required'; if (!/^[a-z0-9-_]+$/i.test(pValue)) return 'Project name must be alphanumeric with dashes or underscores'; }, }); if (isCancel(name)) { cancel('Operation cancelled.'); process.exit(0); } projectName = name; const type = await select({ message: 'What type of game are you building?', options: [ { value: 'single', label: 'Single Player', hint: 'Legacy of Goku, Chrono Trigger, Final Fantasy, etc.' }, { value: 'multi', label: 'Multiplayer', hint: 'APEX Legends, DBZ Squadra, Valorant, etc.' }, { value: 'both', label: 'Single & Multiplayer', hint: 'Minecraft, Terraria, etc.' }, ], }); if (isCancel(type)) { cancel('Operation cancelled.'); process.exit(0); } gameType = type; } else { // Handle flags if (pOptions.single) gameType = 'single'; if (pOptions.multi) gameType = 'multi'; if (pOptions.single && pOptions.multi) gameType = 'both'; if (!projectName) projectName = 'kit-project'; } const projectDir = path.join(process.cwd(), projectName); if (fs.existsSync(projectDir)) { const overwrite = await confirm({ message: `Directory ${chalk.cyan(projectName)} already exists. Overwrite?`, initialValue: false, }); if (isCancel(overwrite) || !overwrite) { cancel('Installation aborted.'); process.exit(0); } } const s = spinner(); s.start(`Scaffolding ${chalk.cyan(projectName)}...`); try { const projectPath = path.join(process.cwd(), projectName); if (fs.existsSync(projectPath)) { // This case should ideally be handled by the confirm prompt above, // but good to have a fallback for non-interactive mode or race conditions. fs.rmSync(projectPath, { recursive: true, force: true }); } fs.mkdirSync(projectPath, { recursive: true }); // Determine the template type based on gameType let templateType = gameType; if (gameType === 'both') templateType = 'multi'; // Use multi for both for now // Resolve template path // When running from lib/bundle/cli/cli.js, templates are in ./kit-game-templates/ const templatesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'kit-game-templates', templateType); const author = getGitUser(); if (!fs.existsSync(templatesDir)) { // Fallback for local development (src/cli/init.ts) // Go up to root then templates/pType const localTemplatesDir = path.join(process.cwd(), 'kit-game-templates', templateType); if (!fs.existsSync(localTemplatesDir)) { console.error(chalk.red(`Error: Templates not found at ${templatesDir} or ${localTemplatesDir}`)); process.exit(1); } copyTemplate(localTemplatesDir, projectPath, projectName, packageJSON.version, author); } else { copyTemplate(templatesDir, projectPath, projectName, packageJSON.version, author); } // Initialize git and create initial commit try { spawnSync('git', ['init'], { cwd: projectPath }); spawnSync('git', ['add', '.'], { cwd: projectPath }); spawnSync('git', ['commit', '-m', 'Initial commit from Kit CLI'], { cwd: projectPath, env: { ...process.env, GIT_AUTHOR_NAME: author.split(' <')[0], GIT_AUTHOR_EMAIL: author.split('<')[1]?.slice(0, -1) || '' } }); } catch { // Silently fail if git is not installed or config is missing } s.stop(`Project ${chalk.green(projectName)} created!`); console.log(`${chalk.cyan('│')}`); console.log(`${chalk.cyan('│')} ${chalk.white.bold('Next steps:')}`); console.log(`${chalk.cyan('│')} ${chalk.dim('1.')} cd ${chalk.cyan(projectName)}`); console.log(`${chalk.cyan('│')} ${chalk.dim('2.')} bun install`); console.log(`${chalk.cyan('│')} ${chalk.dim('3.')} bun run build`); console.log(`${chalk.cyan('│')}`); outro(chalk.green.bold('Happy coding!')); } catch (pError) { s.stop(chalk.red('Scaffolding failed.')); // Cleanup partially created directory if (projectName) { const projectPath = path.join(process.cwd(), projectName); if (fs.existsSync(projectPath)) { fs.rmSync(projectPath, { recursive: true, force: true }); } } console.error(pError); process.exit(1); } }