@vxrn/takeout-cli
Version:
CLI tools for Takeout starter kit - interactive onboarding and project setup
321 lines (266 loc) • 8.38 kB
text/typescript
/**
* Onboard command - interactive setup for Takeout starter kit
*/
import { defineCommand } from 'citty'
import { execSync } from 'node:child_process'
import {
copyEnvFile,
createEnvLocal,
envFileExists,
generateSecret,
updateEnvVariable,
} from '../utils/env'
import { markOnboarded, updateAppConfig, updatePackageJson } from '../utils/files'
import { checkAllPorts, getConflictingPorts } from '../utils/ports'
import { checkAllPrerequisites, hasRequiredPrerequisites } from '../utils/prerequisites'
import {
confirmContinue,
displayOutro,
displayPortConflicts,
displayPrerequisites,
displayWelcome,
promptText,
showError,
showInfo,
showSpinner,
showStep,
showSuccess,
showWarning,
} from '../utils/prompts'
export const onboardCommand = defineCommand({
meta: {
name: 'onboard',
description: 'Interactive onboarding for Takeout starter kit',
},
args: {
skip: {
type: 'boolean',
description: 'Skip interactive prompts',
default: false,
},
},
async run({ args }) {
const cwd = process.cwd()
if (args.skip) {
showInfo('Skipping onboarding (--skip flag)')
return
}
// Phase 1: Welcome & Prerequisites
displayWelcome()
showStep('Checking prerequisites...')
console.info()
const checks = checkAllPrerequisites()
displayPrerequisites(checks)
const hasRequired = hasRequiredPrerequisites(checks)
if (!hasRequired) {
showWarning(
'Some required prerequisites are missing. You can continue, but setup may fail.'
)
const shouldContinue = await confirmContinue('Continue anyway?', false)
if (!shouldContinue) {
displayOutro('Setup cancelled. Install prerequisites and try again.')
return
}
}
console.info()
// Phase 2: Environment Setup
showStep('Setting up environment files...')
console.info()
// Check if .env already exists
const hasEnv = envFileExists(cwd, '.env')
if (hasEnv) {
showInfo('.env file already exists')
const shouldReconfigure = await confirmContinue('Reconfigure environment?', false)
if (!shouldReconfigure) {
showInfo('Skipping environment setup')
} else {
await setupEnvironment(cwd)
}
} else {
await setupEnvironment(cwd)
}
console.info()
// Phase 3: Project Identity
showStep('Configuring project identity...')
console.info()
const shouldCustomize = await confirmContinue(
'Customize project name and bundle identifier?',
false
)
if (shouldCustomize) {
await customizeProject(cwd)
} else {
showInfo('Keeping default project configuration')
}
console.info()
// Phase 4: Start Services
showStep('Starting development services...')
console.info()
const portChecks = checkAllPorts()
const conflicts = getConflictingPorts(portChecks)
if (conflicts.length > 0) {
displayPortConflicts(conflicts)
showWarning('Some ports are already in use. You may need to stop other services.')
}
const shouldStartServices = await confirmContinue(
'Start Docker services (PostgreSQL, Zero, MinIO)?',
true
)
if (shouldStartServices) {
await startServices(cwd)
} else {
showInfo('Skipping service startup')
showInfo("Run 'bun backend' to start services later")
}
console.info()
// Phase 5: Next Steps
showStep('Setup complete!')
console.info()
showSuccess('✓ Environment configured')
showSuccess('✓ Project ready for development')
// Mark as onboarded
markOnboarded(cwd)
console.info()
showInfo('Next steps:')
console.info()
console.info(' bun dev # Start development server')
console.info(' bun ios # Run iOS simulator')
console.info(' bun android # Run Android emulator')
console.info()
console.info('Documentation: /docs')
console.info()
const shouldStart = await confirmContinue('Start development server now?', true)
if (shouldStart) {
console.info()
showInfo('Starting development server...')
console.info()
try {
execSync('bun dev', { stdio: 'inherit', cwd })
} catch {
// User cancelled or error occurred
showInfo('Development server stopped')
}
}
displayOutro('Happy coding! 🚀')
},
})
async function setupEnvironment(cwd: string): Promise<void> {
// Copy .env.development to .env
const copyResult = copyEnvFile(cwd, '.env.development', '.env')
if (!copyResult.success) {
showError(`Failed to create .env: ${copyResult.error}`)
return
}
showSuccess('Created .env from .env.development')
// Generate auth secret
const useExisting = await confirmContinue(
'Use existing BETTER_AUTH_SECRET from .env.development?',
true
)
if (!useExisting) {
const secret = generateSecret()
updateEnvVariable(cwd, 'BETTER_AUTH_SECRET', secret)
showSuccess('Generated new BETTER_AUTH_SECRET')
}
// GitHub OAuth (optional)
const setupGithub = await confirmContinue('Set up GitHub OAuth?', false)
if (setupGithub) {
const clientId = await promptText(
'GitHub Client ID:',
undefined,
'Iv23liNaYfNSauaySodL'
)
if (clientId) {
updateEnvVariable(cwd, 'ONECHAT_GITHUB_CLIENT_ID', clientId)
}
const clientSecret = await promptText(
'GitHub Client Secret (optional):',
undefined,
'Leave empty to skip'
)
if (clientSecret) {
updateEnvVariable(cwd, 'ONECHAT_GITHUB_CLIENT_SECRET', clientSecret)
}
}
// Create .env.local
createEnvLocal(cwd)
showSuccess('Created .env.local for personal overrides')
}
async function customizeProject(cwd: string): Promise<void> {
const projectName = await promptText('Project name:', 'takeout', 'my-awesome-app')
const slug = await promptText(
'Project slug (URL-friendly):',
projectName.toLowerCase().replace(/\s+/g, '-'),
'my-awesome-app'
)
const bundleId = await promptText(
'Bundle identifier:',
`com.${slug}.app`,
'com.example.app'
)
const domain = await promptText(
'Development domain:',
'localhost:8081',
'localhost:8081'
)
// Update package.json
const pkgResult = updatePackageJson(cwd, {
name: projectName,
description: `${projectName} - Built with Takeout starter kit`,
})
if (pkgResult.success) {
showSuccess('Updated package.json')
} else {
showError(`Failed to update package.json: ${pkgResult.error}`)
}
// Update app.config.ts
const configResult = updateAppConfig(cwd, {
name: projectName,
slug,
bundleId,
})
if (configResult.success) {
showSuccess('Updated app.config.ts')
} else {
showError(`Failed to update app.config.ts: ${configResult.error}`)
}
// Update .env URLs
const serverUrl = `http://${domain}`
updateEnvVariable(cwd, 'BETTER_AUTH_URL', serverUrl)
updateEnvVariable(cwd, 'ONE_SERVER_URL', serverUrl)
showSuccess('Updated environment URLs')
}
async function startServices(cwd: string): Promise<void> {
const spinner = showSpinner('Starting Docker services...')
try {
// Start services in background
execSync('bun backend', {
stdio: 'ignore',
cwd,
})
// Wait a bit for services to start
await new Promise((resolve) => setTimeout(resolve, 3000))
spinner.stop('Docker services started')
showSuccess('✓ PostgreSQL running on port 5432')
showSuccess('✓ Zero sync running on port 4848')
showSuccess('✓ MinIO (S3) running on port 9090')
// Run migrations
const shouldMigrate = await confirmContinue('Run database migrations?', true)
if (shouldMigrate) {
const migrateSpinner = showSpinner('Running migrations...')
try {
execSync('bun migrate', { stdio: 'ignore', cwd })
migrateSpinner.stop('Database migrated')
showSuccess('✓ Database migrations complete')
} catch {
migrateSpinner.stop('Migration failed')
showError('Failed to run migrations')
showInfo("Try running 'bun migrate' manually")
}
}
} catch (error) {
spinner.stop('Failed to start services')
showError(error instanceof Error ? error.message : 'Unknown error')
showInfo("Try running 'bun backend' manually")
}
}