@sprouted/create-vibes
Version:
Create a new Vibes monorepo
311 lines (277 loc) • 8.57 kB
text/typescript
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>
}
}