UNPKG

@sprouted/create-vibes

Version:
311 lines (277 loc) 8.57 kB
import * as path from 'path' import * as fs from 'fs/promises' import { fileURLToPath } from 'url' import { dirname } from 'path' import { TemplateEngine } from '../template-engine/engine.js' import inquirer from 'inquirer' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) export interface CreateAppOptions { type: 'web' | 'mobile' | 'api' | 'desktop' name?: string displayName?: string appId?: string framework?: string includeRouter?: boolean includeTailwind?: boolean includeNavigator?: boolean includeAuth?: boolean includeApiClient?: boolean port?: number includeDatabase?: boolean includeValidation?: boolean includeSwagger?: boolean includeGraphQL?: boolean includeAutoUpdater?: boolean includeSystemTray?: boolean includeMenuBar?: boolean includeIpc?: boolean includeDarkMode?: boolean } export class CreateAppCommand { private engine: TemplateEngine constructor(engine: TemplateEngine) { this.engine = engine } async execute(options: CreateAppOptions): Promise<void> { const finalOptions = await this.promptMissingOptions(options) const templatePath = path.join( __dirname, '../templates/typescript/apps', finalOptions.type ) const targetPath = path.join(process.cwd(), 'apps', finalOptions.name!) // Ensure the apps directory exists await fs.mkdir(path.join(process.cwd(), 'apps'), { recursive: true }) await this.engine.generateFromTemplate( templatePath, { appName: finalOptions.name, displayName: finalOptions.displayName, appId: finalOptions.appId, framework: finalOptions.framework, includeRouter: finalOptions.includeRouter, includeTailwind: finalOptions.includeTailwind, includeNavigator: finalOptions.includeNavigator, includeAuth: finalOptions.includeAuth, includeApiClient: finalOptions.includeApiClient, port: finalOptions.port, includeDatabase: finalOptions.includeDatabase, includeValidation: finalOptions.includeValidation, includeSwagger: finalOptions.includeSwagger, includeGraphQL: finalOptions.includeGraphQL, includeAutoUpdater: finalOptions.includeAutoUpdater, includeSystemTray: finalOptions.includeSystemTray, includeMenuBar: finalOptions.includeMenuBar, includeIpc: finalOptions.includeIpc, includeDarkMode: finalOptions.includeDarkMode }, { outputPath: targetPath, dryRun: false } ) console.log(`✨ Created ${finalOptions.type} app: ${finalOptions.name}`) console.log(`📁 Location: ${targetPath}`) console.log('\nNext steps:') console.log(` cd apps/${finalOptions.name}`) console.log(' pnpm install') console.log(finalOptions.type === 'mobile' ? ' pnpm start' : ' pnpm dev') } private async promptMissingOptions(options: CreateAppOptions): Promise<Required<CreateAppOptions>> { const questions = [] if (!options.name) { questions.push({ type: 'input', name: 'name', message: 'App name:', validate: (input: string) => { if (!input.trim()) return 'App name is required' if (!/^[a-z0-9-]+$/.test(input)) { return 'App name must be lowercase alphanumeric with dashes' } return true } }) } if (options.type === 'web') { if (options.framework === undefined) { questions.push({ type: 'list', name: 'framework', message: 'Choose a framework:', choices: [ { name: 'Vite + React', value: 'vite-react' }, { name: 'Next.js', value: 'nextjs' } ], default: 'vite-react' }) } if (options.includeRouter === undefined) { questions.push({ type: 'confirm', name: 'includeRouter', message: 'Include React Router?', default: true }) } if (options.includeTailwind === undefined) { questions.push({ type: 'confirm', name: 'includeTailwind', message: 'Include Tailwind CSS?', default: false }) } } if (options.type === 'mobile') { if (options.displayName === undefined) { questions.push({ type: 'input', name: 'displayName', message: 'Display name (shown to users):', default: options.name || 'Vibes App' }) } if (options.includeNavigator === undefined) { questions.push({ type: 'confirm', name: 'includeNavigator', message: 'Include React Navigation?', default: true }) } if (options.includeAuth === undefined) { questions.push({ type: 'confirm', name: 'includeAuth', message: 'Include authentication setup?', default: false }) } if (options.includeApiClient === undefined) { questions.push({ type: 'confirm', name: 'includeApiClient', message: 'Include API client?', default: true }) } } if (options.type === 'api') { if (options.port === undefined) { questions.push({ type: 'number', name: 'port', message: 'Port number:', default: 3001 }) } if (options.includeDatabase === undefined) { questions.push({ type: 'confirm', name: 'includeDatabase', message: 'Include database (PostgreSQL with Prisma)?', default: true }) } if (options.includeAuth === undefined) { questions.push({ type: 'confirm', name: 'includeAuth', message: 'Include authentication (JWT)?', default: true }) } if (options.includeValidation === undefined) { questions.push({ type: 'confirm', name: 'includeValidation', message: 'Include request validation (Zod)?', default: true }) } if (options.includeSwagger === undefined) { questions.push({ type: 'confirm', name: 'includeSwagger', message: 'Include Swagger documentation?', default: false }) } if (options.includeGraphQL === undefined) { questions.push({ type: 'confirm', name: 'includeGraphQL', message: 'Include GraphQL support?', default: false }) } } if (options.type === 'desktop') { if (options.displayName === undefined) { questions.push({ type: 'input', name: 'displayName', message: 'Display name (shown to users):', default: options.name || 'Vibes Desktop' }) } if (options.appId === undefined) { questions.push({ type: 'input', name: 'appId', message: 'Application ID:', default: `com.vibes.${options.name || 'desktop'}` }) } if (options.includeAutoUpdater === undefined) { questions.push({ type: 'confirm', name: 'includeAutoUpdater', message: 'Include auto-updater?', default: true }) } if (options.includeSystemTray === undefined) { questions.push({ type: 'confirm', name: 'includeSystemTray', message: 'Include system tray support?', default: false }) } if (options.includeMenuBar === undefined) { questions.push({ type: 'confirm', name: 'includeMenuBar', message: 'Include custom menu bar?', default: true }) } if (options.includeIpc === undefined) { questions.push({ type: 'confirm', name: 'includeIpc', message: 'Include IPC communication examples?', default: true }) } if (options.includeDarkMode === undefined) { questions.push({ type: 'confirm', name: 'includeDarkMode', message: 'Include dark mode support?', default: true }) } } const answers = questions.length > 0 ? await inquirer.prompt(questions) : {} return { ...options, ...answers } as Required<CreateAppOptions> } }